From d40718e05740921b7328350abd018c417d8515a2 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:40:43 +0800 Subject: [PATCH] feat: add engineering standards and adopt QPK runtime_config helpers - Add coverage config to pyproject.toml - Add pytest-cov to CI workflow - Import common env helpers from quant_platform_kit.common.runtime_config instead of defining them locally Co-Authored-By: Claude --- .github/workflows/ci.yml | 4 +- runtime_config_support.py | 140 +++++++------------------------------- 2 files changed, 28 insertions(+), 116 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11443e4..5463bc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | @@ -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 diff --git a/runtime_config_support.py b/runtime_config_support.py index 90939b0..c77c72b 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -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 ( @@ -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( @@ -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( @@ -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") @@ -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")), @@ -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")), @@ -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") @@ -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) @@ -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: @@ -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"