Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
run: |
set -euo pipefail
python -m pip install --upgrade pip
python -m pip install -r requirements.txt pytest ruff
python -m pip install -r requirements.txt pytest pytest-cov ruff

- name: Smoke import pinned shared packages
run: |
Expand All @@ -92,4 +92,4 @@ jobs:
- name: Run tests
run: |
set -euo pipefail
PYTHONPATH=. PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest -q tests
PYTHONPATH=. PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest -q tests --cov --cov-report=term --cov-report=xml
140 changes: 26 additions & 114 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@
from quant_platform_kit.common.runtime_config import (
resolve_bool_value,
resolve_cash_only_execution_env,
resolve_optional_bool_env,
resolve_optional_dca_mode_env,
resolve_optional_float_env,
resolve_optional_ibit_zscore_exit_mode_env,
resolve_optional_positive_float_env,
resolve_optional_ratio_env,
resolve_optional_symbol_env,
resolve_split_env_list,
resolve_strategy_runtime_path_settings,
)
from quant_platform_kit.common.runtime_target import (
Expand Down Expand Up @@ -211,19 +218,19 @@ def load_platform_runtime_settings(
debug_position_snapshot=resolve_bool_value(os.getenv("FIRSTRADE_DEBUG_POSITION_SNAPSHOT")),
income_threshold_usd=resolve_optional_float_env(os.environ, "INCOME_THRESHOLD_USD"),
qqqi_income_ratio=_qqqi_income_ratio_env(),
income_layer_enabled=_optional_bool_env("INCOME_LAYER_ENABLED"),
income_layer_enabled=resolve_optional_bool_env("INCOME_LAYER_ENABLED"),
income_layer_start_usd=_optional_non_negative_float_env("INCOME_LAYER_START_USD"),
income_layer_max_ratio=_optional_ratio_env("INCOME_LAYER_MAX_RATIO"),
dca_mode=_optional_dca_mode_env("DCA_MODE"),
dca_base_investment_usd=_optional_positive_float_env("DCA_BASE_INVESTMENT_USD"),
ibit_zscore_exit_enabled=_optional_bool_env("IBIT_ZSCORE_EXIT_ENABLED"),
ibit_zscore_exit_mode=_optional_ibit_zscore_exit_mode_env("IBIT_ZSCORE_EXIT_MODE"),
ibit_zscore_exit_parking_symbol=_optional_symbol_env("IBIT_ZSCORE_EXIT_PARKING_SYMBOL"),
ibit_zscore_exit_risk_reduced_exposure=_optional_ratio_env(
income_layer_max_ratio=resolve_optional_ratio_env("INCOME_LAYER_MAX_RATIO"),
dca_mode=resolve_optional_dca_mode_env("DCA_MODE"),
dca_base_investment_usd=resolve_optional_positive_float_env("DCA_BASE_INVESTMENT_USD"),
ibit_zscore_exit_enabled=resolve_optional_bool_env("IBIT_ZSCORE_EXIT_ENABLED"),
ibit_zscore_exit_mode=resolve_optional_ibit_zscore_exit_mode_env("IBIT_ZSCORE_EXIT_MODE"),
ibit_zscore_exit_parking_symbol=resolve_optional_symbol_env("IBIT_ZSCORE_EXIT_PARKING_SYMBOL"),
ibit_zscore_exit_risk_reduced_exposure=resolve_optional_ratio_env(
"IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE"
),
ibit_zscore_exit_risk_off_exposure=_optional_ratio_env("IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE"),
ibit_zscore_exit_allow_outside_execution_window=_optional_bool_env(
ibit_zscore_exit_risk_off_exposure=resolve_optional_ratio_env("IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE"),
ibit_zscore_exit_allow_outside_execution_window=resolve_optional_bool_env(
"IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW"
),
runtime_execution_window_trading_days=_runtime_execution_window_trading_days_env(
Expand All @@ -244,11 +251,11 @@ def load_platform_runtime_settings(
),
market_signal_required=_resolve_market_signal_required(
strategy_profile=strategy_definition.profile,
dca_mode=_optional_dca_mode_env("DCA_MODE"),
dca_mode=resolve_optional_dca_mode_env("DCA_MODE"),
),
market_signal_fallback_mode=_resolve_market_signal_fallback_mode(
strategy_profile=strategy_definition.profile,
dca_mode=_optional_dca_mode_env("DCA_MODE"),
dca_mode=resolve_optional_dca_mode_env("DCA_MODE"),
),
market_signal_max_stale_days=_optional_int(
_first_non_empty(
Expand Down Expand Up @@ -282,8 +289,8 @@ def load_platform_runtime_settings(
os.getenv("FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON")
or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON")
),
strategy_plugin_alert_channels=_split_env_list(os.getenv("STRATEGY_PLUGIN_ALERT_CHANNELS")),
strategy_plugin_alert_email_recipients=_split_env_list(os.getenv("STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS")),
strategy_plugin_alert_channels=resolve_split_env_list("STRATEGY_PLUGIN_ALERT_CHANNELS"),
strategy_plugin_alert_email_recipients=resolve_split_env_list("STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS"),
strategy_plugin_alert_email_sender_email=_first_non_empty(os.getenv("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_EMAIL")),
strategy_plugin_alert_email_sender_password=_first_non_empty(
os.getenv("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD")
Expand All @@ -293,7 +300,7 @@ def load_platform_runtime_settings(
strategy_plugin_alert_email_smtp_security=_first_non_empty(
os.getenv("STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_SECURITY")
),
strategy_plugin_alert_sms_recipients=_split_env_list(os.getenv("STRATEGY_PLUGIN_ALERT_SMS_RECIPIENTS")),
strategy_plugin_alert_sms_recipients=resolve_split_env_list("STRATEGY_PLUGIN_ALERT_SMS_RECIPIENTS"),
strategy_plugin_alert_sms_provider=_first_non_empty(os.getenv("STRATEGY_PLUGIN_ALERT_SMS_PROVIDER")),
strategy_plugin_alert_sms_account_id=_first_non_empty(os.getenv("STRATEGY_PLUGIN_ALERT_SMS_ACCOUNT_ID")),
strategy_plugin_alert_sms_auth_token=_first_non_empty(os.getenv("STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN")),
Expand All @@ -305,7 +312,7 @@ def load_platform_runtime_settings(
strategy_plugin_alert_sms_body_max_chars=_first_non_empty(
os.getenv("STRATEGY_PLUGIN_ALERT_SMS_BODY_MAX_CHARS")
),
strategy_plugin_alert_push_recipients=_split_env_list(os.getenv("STRATEGY_PLUGIN_ALERT_PUSH_RECIPIENTS")),
strategy_plugin_alert_push_recipients=resolve_split_env_list("STRATEGY_PLUGIN_ALERT_PUSH_RECIPIENTS"),
strategy_plugin_alert_push_provider=_first_non_empty(os.getenv("STRATEGY_PLUGIN_ALERT_PUSH_PROVIDER")),
strategy_plugin_alert_push_app_token=_first_non_empty(os.getenv("STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN")),
strategy_plugin_alert_push_access_token=_first_non_empty(os.getenv("STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN")),
Expand All @@ -316,8 +323,8 @@ def load_platform_runtime_settings(
strategy_plugin_alert_push_body_max_chars=_first_non_empty(
os.getenv("STRATEGY_PLUGIN_ALERT_PUSH_BODY_MAX_CHARS")
),
strategy_plugin_alert_telegram_chat_ids=_split_env_list(
os.getenv("STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS")
strategy_plugin_alert_telegram_chat_ids=resolve_split_env_list(
"STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS"
),
strategy_plugin_alert_telegram_bot_token=_first_non_empty(
os.getenv("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN")
Expand Down Expand Up @@ -374,33 +381,11 @@ def _qqqi_income_ratio_env() -> float | None:
return value


def _optional_bool_env(name: str) -> bool | None:
raw_value = os.getenv(name)
if raw_value is None or str(raw_value).strip() == "":
return None
value = str(raw_value).strip().lower()
if value in {"1", "true", "yes", "y", "on"}:
return True
if value in {"0", "false", "no", "n", "off"}:
return False
raise ValueError(f"{name} must be boolean, got {raw_value!r}")


def _runtime_target_enabled_env() -> bool:
value = _optional_bool_env("RUNTIME_TARGET_ENABLED")
value = resolve_optional_bool_env("RUNTIME_TARGET_ENABLED")
return True if value is None else value


def _optional_ratio_env(name: str) -> float | None:
value = resolve_optional_float_env(os.environ, name)
if value is None:
return None
if not math.isfinite(value):
raise ValueError(f"{name} must be finite, got {value}")
if not (0.0 <= value <= 1.0):
raise ValueError(f"{name} must be in [0,1], got {value}")
return value


def _optional_non_negative_float_env(name: str) -> float | None:
value = resolve_optional_float_env(os.environ, name)
Expand All @@ -413,65 +398,6 @@ def _optional_non_negative_float_env(name: str) -> float | None:
return float(value)


def _optional_positive_float_env(name: str) -> float | None:
value = resolve_optional_float_env(os.environ, name)
if value is None:
return None
if not math.isfinite(value):
raise ValueError(f"{name} must be finite, got {value}")
if value <= 0:
raise ValueError(f"{name} must be positive, got {value}")
return float(value)


def _optional_dca_mode_env(name: str) -> str | None:
raw_value = os.getenv(name)
if raw_value is None or str(raw_value).strip() == "":
return None
value = str(raw_value).strip().lower()
aliases = {
"ordinary": "fixed",
"ordinary_dca": "fixed",
"fixed_dca": "fixed",
"smart_dca": "smart",
}
mode = aliases.get(value, value)
if mode not in {"fixed", "smart"}:
raise ValueError(f"{name} must be fixed or smart, got {raw_value!r}")
return mode


def _optional_ibit_zscore_exit_mode_env(name: str) -> str | None:
raw_value = os.getenv(name)
if raw_value is None or str(raw_value).strip() == "":
return None
value = str(raw_value).strip().lower()
aliases = {
"off": "disabled",
"none": "disabled",
"false": "disabled",
"disable": "disabled",
"enabled": "live",
"shadow": "paper",
"dry_run": "paper",
"dry-run": "paper",
}
mode = aliases.get(value, value)
if mode not in {"disabled", "paper", "live"}:
raise ValueError(f"{name} must be disabled, paper, or live, got {raw_value!r}")
return mode


def _optional_symbol_env(name: str) -> str | None:
raw_value = os.getenv(name)
if raw_value is None or str(raw_value).strip() == "":
return None
value = str(raw_value).strip().upper()
if len(value) > 16 or not value.replace(".", "").replace("-", "").isalnum():
raise ValueError(f"{name} must be a symbol")
return value


def _resolve_non_negative_float_env(name: str, *, default: float) -> float:
value = resolve_optional_float_env(os.environ, name)
if value is None:
Expand Down Expand Up @@ -557,20 +483,6 @@ def _optional_int(raw_value: str | None) -> int | None:
return int(value)


def _split_env_list(raw_value: str | None) -> tuple[str, ...]:
if raw_value is None:
return ()
items = []
seen = set()
for value in str(raw_value).replace(";", ",").replace("\n", ",").split(","):
item = value.strip()
if not item or item in seen:
continue
items.append(item)
seen.add(item)
return tuple(items)


def _runtime_execution_window_trading_days_env(strategy_profile: str) -> int | None:
raw_value = os.getenv("FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS")
env_name = "FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS"
Expand Down
Loading