From ab63794761055d94e829528ee8c490f82c56aab4 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:48:43 +0800 Subject: [PATCH 1/3] feat(risk): parse small_account_hold and pin QPK/UES for default hold Bind optional small_account_hold overrides, stamp cash_only into capabilities, and adopt QPK a9093762 + UES 4a394388 so default non-worsening RRL hold applies for small NAV accounts. Co-authored-by: Cursor --- Dockerfile | 1 + pyproject.toml | 6 +- qsl.toml | 4 +- strategy_runtime.py | 231 ++++++++++++++++++++++++++------- tests/test_strategy_runtime.py | 160 ++++++++++++++++++++++- uv.lock | 10 +- 6 files changed, 353 insertions(+), 59 deletions(-) diff --git a/Dockerfile b/Dockerfile index 114fd54..90559cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ RUN apt-get update \ COPY . . RUN python -m pip install --upgrade pip uv \ && uv sync --frozen --no-dev \ + && python -c 'from importlib import metadata as m; import json; from pathlib import Path; payload=json.loads(m.distribution("us-equity-strategies").read_text("direct_url.json") or "{}"); rev=(payload.get("vcs_info") or {}).get("commit_id"); assert isinstance(rev, str) and rev.strip(); Path("/app/UES_REVISION").write_text(rev.strip() + "\n", encoding="utf-8")' \ && python scripts/validate_cloud_run_startup.py \ && apt-get purge -y git \ && apt-get autoremove -y --purge \ diff --git a/pyproject.toml b/pyproject.toml index d389cf2..ef46b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,8 @@ dependencies = [ "google-cloud-secret-manager", "google-cloud-storage", "yfinance", - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@68c51590da8a5097b7de6d75b4ccb6a175318b48", - "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@e2258223310913f6db9f40b810756db0ee2cfd68", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@a90937621a7a05c9f72d0ca8a29be3fcb18a327c", + "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@4a3943883cd6b5bbfe32a559e56a91b40a81b7ce", "hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@709e5e1cde7841aed538d94eb26b552b46cb7806", ] @@ -64,5 +64,5 @@ include = [ [tool.uv] override-dependencies = [ - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@68c51590da8a5097b7de6d75b4ccb6a175318b48", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@a90937621a7a05c9f72d0ca8a29be3fcb18a327c", ] diff --git a/qsl.toml b/qsl.toml index 9db86fc..2bcb6fe 100644 --- a/qsl.toml +++ b/qsl.toml @@ -5,8 +5,8 @@ upgrade_ring = "ring_d" allow_legacy = false [qsl.requires] -quant_platform_kit = "68c51590da8a5097b7de6d75b4ccb6a175318b48" -us_equity_strategies = "e2258223310913f6db9f40b810756db0ee2cfd68" +quant_platform_kit = "a90937621a7a05c9f72d0ca8a29be3fcb18a327c" +us_equity_strategies = "4a3943883cd6b5bbfe32a559e56a91b40a81b7ce" hk_equity_strategies = "709e5e1cde7841aed538d94eb26b552b46cb7806" [qsl.compat] diff --git a/strategy_runtime.py b/strategy_runtime.py index a824312..ac2d43b 100644 --- a/strategy_runtime.py +++ b/strategy_runtime.py @@ -4,6 +4,7 @@ from importlib import metadata as importlib_metadata import json from dataclasses import dataclass, field, replace +from pathlib import Path from typing import Any import pandas as pd @@ -27,8 +28,8 @@ build_market_history_inputs, build_semiconductor_rotation_inputs, fetch_option_chain_snapshot, - fetch_portfolio_snapshot, ) +from application.ibkr_portfolio import fetch_portfolio_snapshot from quant_platform_kit.common.strategy_contracts import ( StrategyDecision, StrategyEntrypoint, @@ -42,7 +43,7 @@ build_portfolio_snapshot_from_account_state, build_strategy_evaluation_inputs, ) -from quant_platform_kit.risk.contracts import RuntimeRiskLimits +from quant_platform_kit.risk.contracts import RuntimeRiskLimits, SmallAccountRiskHoldPolicy from runtime_config_support import PlatformRuntimeSettings from us_equity_strategies.signals import resolve_external_market_signal_inputs from strategy_loader import ( @@ -53,20 +54,133 @@ +def _parse_small_account_hold_policy(raw: Any) -> SmallAccountRiskHoldPolicy | None: + """Parse optional deployment hold policy; invalid shapes return None.""" + if raw is None: + return None + if not isinstance(raw, Mapping): + return None + try: + return SmallAccountRiskHoldPolicy( + enabled=raw["enabled"], + hold_below_nav=raw["hold_below_nav"], + require_cash_only=raw.get("require_cash_only", True), + ) + except (KeyError, TypeError, ValueError): + return None + + def _installed_ues_revision() -> str | None: """Read the VCS revision of the installed UES distribution.""" + candidates: list[str] = [] try: distribution = importlib_metadata.distribution("us-equity-strategies") raw_direct_url = distribution.read_text("direct_url.json") - if not raw_direct_url: - return None - payload = json.loads(raw_direct_url) - revision = payload.get("vcs_info", {}).get("commit_id") + if raw_direct_url: + candidates.append(raw_direct_url) + locate_file = getattr(distribution, "locate_file", None) + if callable(locate_file): + direct_url_path = Path(str(locate_file("direct_url.json"))) + if direct_url_path.is_file(): + candidates.append(direct_url_path.read_text(encoding="utf-8")) except (ImportError, OSError, TypeError, ValueError, AttributeError): - return None - if not isinstance(revision, str) or not revision.strip(): - return None - return revision.strip() + pass + stamped = Path("/app/UES_REVISION") + if stamped.is_file(): + stamped_revision = stamped.read_text(encoding="utf-8").strip() + if stamped_revision: + return stamped_revision + for raw_direct_url in candidates: + try: + payload = json.loads(raw_direct_url) + except (TypeError, ValueError): + continue + revision = payload.get("vcs_info", {}).get("commit_id") + if isinstance(revision, str) and revision.strip(): + return revision.strip() + return None + + +def _runtime_risk_binding_mismatch_reasons( + *, + policy_binding: Mapping[str, Any], + policy: Mapping[str, Any], + account_scope: str, + runtime_scope: str, + actual_account_hash: str, + profile: str, + target_release: Any, + actual_ues_revision: str | None, + execution_mode: str, + cash_only_execution: bool, + reserved_cash_ratio: Any, + merged_runtime_config: Mapping[str, Any], + actual_exit_buffer: Any, +) -> tuple[str, ...]: + """Return stable reason codes for SOXL runtime-risk binding mismatches.""" + reasons: list[str] = [] + if not account_scope: + reasons.append("missing_account_scope") + if not runtime_scope: + reasons.append("missing_runtime_scope") + if not actual_account_hash: + reasons.append("missing_account_hash") + if target_release is None: + reasons.append("missing_strategy_release") + if str(policy_binding.get("account_scope") or "").strip() != account_scope: + reasons.append("account_scope_mismatch") + if str(policy_binding.get("runtime_scope") or "").strip() != runtime_scope: + reasons.append("runtime_scope_mismatch") + if str(policy_binding.get("account_hash") or "").strip() != actual_account_hash: + reasons.append("account_hash_mismatch") + if str(policy_binding.get("strategy_profile") or "").strip() != profile: + reasons.append("strategy_profile_mismatch") + expected_ues = str(policy_binding.get("ues_revision") or "").strip() + release_ues = ( + str(getattr(target_release, "strategy_revision", "") or "").strip() + if target_release is not None + else "" + ) + if expected_ues != release_ues: + reasons.append("ues_revision_release_mismatch") + if actual_ues_revision is None: + reasons.append("ues_revision_unavailable") + elif actual_ues_revision != expected_ues: + reasons.append("ues_revision_installed_mismatch") + if str(policy_binding.get("execution_mode") or "").strip().lower() != execution_mode: + reasons.append("execution_mode_mismatch") + if policy_binding.get("cash_only_execution") is not True: + reasons.append("policy_cash_only_mismatch") + if cash_only_execution is not True: + reasons.append("settings_cash_only_mismatch") + if policy_binding.get("reserved_cash_ratio") != merged_runtime_config.get("cash_reserve_ratio"): + reasons.append("reserved_cash_merged_mismatch") + if policy_binding.get("reserved_cash_ratio") != reserved_cash_ratio: + reasons.append("reserved_cash_settings_mismatch") + if policy_binding.get("reserved_cash_ratio") != 0.03: + reasons.append("reserved_cash_ratio_mismatch") + if policy_binding.get("options_enabled") is not False: + reasons.append("options_enabled_mismatch") + if any( + merged_runtime_config.get(key) is not False + for key in ( + "option_overlay_enabled", + "option_growth_overlay_enabled", + "option_income_overlay_enabled", + ) + ): + reasons.append("option_overlay_mismatch") + if not isinstance(policy.get("exit_parameters"), Mapping): + reasons.append("exit_parameters_missing") + if actual_exit_buffer is None: + reasons.append("trend_exit_buffer_missing") + elif actual_exit_buffer != 0.02: + reasons.append("trend_exit_buffer_mismatch") + if dict(policy.get("exit_parameters") or {}) != {"trend_exit_buffer": 0.02}: + reasons.append("exit_parameters_mismatch") + elif dict(policy.get("exit_parameters") or {}) != {"trend_exit_buffer": actual_exit_buffer}: + reasons.append("exit_parameters_buffer_mismatch") + return tuple(reasons) DEFAULT_CASH_RESERVE_RATIO = 0.0 @@ -191,14 +305,20 @@ def _fetch_portfolio_snapshot_for_context(self, ib, *, required: bool) -> Any | if ib is None and not required: return None account_ids = tuple(self.runtime_settings.account_ids or ()) + snapshot_kwargs = { + "currency": str(getattr(self.runtime_settings, "market_currency", "USD") or "USD"), + "cash_only_execution": bool( + getattr(self.runtime_settings, "cash_only_execution", True) + ), + } if required: if account_ids: - return fetch_portfolio_snapshot(ib, account_ids=account_ids) - return fetch_portfolio_snapshot(ib) + return fetch_portfolio_snapshot(ib, account_ids=account_ids, **snapshot_kwargs) + return fetch_portfolio_snapshot(ib, **snapshot_kwargs) try: if account_ids: - return fetch_portfolio_snapshot(ib, account_ids=account_ids) - return fetch_portfolio_snapshot(ib) + return fetch_portfolio_snapshot(ib, account_ids=account_ids, **snapshot_kwargs) + return fetch_portfolio_snapshot(ib, **snapshot_kwargs) except Exception as exc: self.logger( "strategy_dashboard_portfolio_snapshot_failed | " @@ -330,7 +450,13 @@ def _build_runtime_risk_capabilities( "max_positions", "exit_parameters", } - if set(policy) != expected_policy_keys or not isinstance(policy.get("binding"), Mapping): + optional_policy_keys = {"small_account_hold"} + policy_keys = set(policy) + if ( + not expected_policy_keys.issubset(policy_keys) + or (policy_keys - expected_policy_keys - optional_policy_keys) + or not isinstance(policy.get("binding"), Mapping) + ): return {**capabilities, "runtime_risk_limits": object()}, "unavailable:invalid_runtime_risk_policy" target_release = runtime_target.strategy_release @@ -354,39 +480,30 @@ def _build_runtime_risk_capabilities( actual_account_hash = str(metadata.get("account_hash") or "").strip() if isinstance(metadata, Mapping) else "" actual_ues_revision = _installed_ues_revision() actual_exit_buffer = self.merged_runtime_config.get("trend_exit_buffer") - if ( - not account_scope - or not runtime_scope - or not actual_account_hash - or target_release is None - or str(policy_binding["account_scope"]).strip() != account_scope - or str(policy_binding["runtime_scope"]).strip() != runtime_scope - or str(policy_binding["account_hash"]).strip() != actual_account_hash - or str(policy_binding["strategy_profile"]).strip() != self.profile - or str(policy_binding["ues_revision"]).strip() != str(target_release.strategy_revision).strip() - or actual_ues_revision is None - or actual_ues_revision != str(policy_binding["ues_revision"]).strip() - or str(policy_binding["execution_mode"]).strip().lower() != runtime_target.execution_mode - or policy_binding["cash_only_execution"] is not True - or self.runtime_settings.cash_only_execution is not True - or policy_binding["reserved_cash_ratio"] != self.merged_runtime_config.get("cash_reserve_ratio") - or policy_binding["reserved_cash_ratio"] != self.runtime_settings.reserved_cash_ratio - or policy_binding["reserved_cash_ratio"] != 0.03 - or policy_binding["options_enabled"] is not False - or any( - self.merged_runtime_config.get(key) is not False - for key in ( - "option_overlay_enabled", - "option_growth_overlay_enabled", - "option_income_overlay_enabled", - ) + mismatch_reasons = _runtime_risk_binding_mismatch_reasons( + policy_binding=policy_binding, + policy=policy, + account_scope=account_scope, + runtime_scope=runtime_scope, + actual_account_hash=actual_account_hash, + profile=self.profile, + target_release=target_release, + actual_ues_revision=actual_ues_revision, + execution_mode=str(runtime_target.execution_mode or ""), + cash_only_execution=self.runtime_settings.cash_only_execution, + reserved_cash_ratio=self.runtime_settings.reserved_cash_ratio, + merged_runtime_config=self.merged_runtime_config, + actual_exit_buffer=actual_exit_buffer, + ) + if mismatch_reasons: + self.logger( + "strategy_runtime_binding_mismatch | " + f"profile={self.profile} reasons={','.join(mismatch_reasons)} " + f"installed_ues_revision={actual_ues_revision!r} " + f"policy_ues_revision={str(policy_binding.get('ues_revision') or '').strip()!r} " + f"account_hash={actual_account_hash!r} " + f"trend_exit_buffer={actual_exit_buffer!r}" ) - or not isinstance(policy.get("exit_parameters"), Mapping) - or actual_exit_buffer is None - or actual_exit_buffer != 0.02 - or dict(policy["exit_parameters"]) != {"trend_exit_buffer": 0.02} - or dict(policy["exit_parameters"]) != {"trend_exit_buffer": actual_exit_buffer} - ): return {**capabilities, "runtime_risk_limits": object()}, "unavailable:runtime_binding_mismatch" try: limits = RuntimeRiskLimits( @@ -399,7 +516,25 @@ def _build_runtime_risk_capabilities( ) except (TypeError, ValueError): return {**capabilities, "runtime_risk_limits": object()}, "unavailable:invalid_runtime_risk_limits" - return {**capabilities, "runtime_risk_limits": limits}, "verified:runtime_risk_limits" + capability_payload: dict[str, Any] = { + **capabilities, + "runtime_risk_limits": limits, + "cash_only_execution": bool(self.runtime_settings.cash_only_execution), + } + hold_policy = _parse_small_account_hold_policy(policy.get("small_account_hold")) + if isinstance(policy.get("small_account_hold"), Mapping) and hold_policy is None: + return { + **capabilities, + "runtime_risk_limits": object(), + }, "unavailable:invalid_small_account_hold" + if hold_policy is not None: + if hold_policy.require_cash_only and self.runtime_settings.cash_only_execution is not True: + return { + **capabilities, + "runtime_risk_limits": object(), + }, "unavailable:small_account_hold_cash_only" + capability_payload["small_account_hold_policy"] = hold_policy + return capability_payload, "verified:runtime_risk_limits" def _build_context_capabilities( self, diff --git a/tests/test_strategy_runtime.py b/tests/test_strategy_runtime.py index 6459383..067a273 100644 --- a/tests/test_strategy_runtime.py +++ b/tests/test_strategy_runtime.py @@ -1399,6 +1399,159 @@ def _soxl_runtime_policy(*, account_hash: str = "account-hash") -> dict[str, obj } +def test_soxl_runtime_attaches_small_account_hold_policy(monkeypatch): + from quant_platform_kit.risk.contracts import SmallAccountRiskHoldPolicy + + policy = _soxl_runtime_policy() + policy["small_account_hold"] = { + "enabled": True, + "hold_below_nav": 1000.0, + "require_cash_only": True, + } + entrypoint = SimpleNamespace(manifest=SimpleNamespace(profile="soxl_soxx_trend_income")) + runtime = strategy_runtime_module.LoadedStrategyRuntime( + entrypoint=entrypoint, + runtime_settings=replace( + _build_runtime_settings(profile="soxl_soxx_trend_income"), + runtime_target=build_runtime_target( + platform_id="ibkr", + strategy_profile="soxl_soxx_trend_income", + dry_run_only=False, + account_scope="paper-account-scope", + service_name="ibkr-paper-service", + strategy_release={ + "release_id": "soxl-release", + "manifest_sha256": "a" * 64, + "strategy_revision": "ues-revision", + "config_sha256": "b" * 64, + "risk_policy_sha256": "c" * 64, + "evidence_sha256": "d" * 64, + "plugin_bundle_sha256": "e" * 64, + "effective_session": "2026-09-17", + }, + ), + trusted_runtime_risk_policy=policy, + cash_only_execution=True, + reserved_cash_ratio=0.03, + ), + runtime_adapter=StrategyRuntimeAdapter(), + merged_runtime_config={ + "cash_reserve_ratio": 0.03, + "trend_exit_buffer": 0.02, + "option_overlay_enabled": False, + "option_growth_overlay_enabled": False, + "option_income_overlay_enabled": False, + }, + logger=lambda _message: None, + ) + snapshot = PortfolioSnapshot( + as_of=strategy_runtime_module.pd.Timestamp("2026-08-27", tz="UTC").to_pydatetime(), + total_equity=472.0, + metadata={ + "account_hash": "account-hash", + "total_equity_source": "broker_net_liquidation", + "source_digest_sha256": "b" * 64, + "broker_net_liquidation": 472.0, + }, + ) + monkeypatch.setattr(strategy_runtime_module, "_installed_ues_revision", lambda: "ues-revision") + capabilities = runtime._build_context_capabilities(ib=None, portfolio_snapshot=snapshot) + assert runtime._last_capability_status["runtime_risk_status"] == "verified:runtime_risk_limits" + hold = capabilities["small_account_hold_policy"] + assert isinstance(hold, SmallAccountRiskHoldPolicy) + assert hold.enabled is True + assert hold.hold_below_nav == 1000.0 + assert capabilities["cash_only_execution"] is True + + +def test_soxl_runtime_rejects_invalid_small_account_hold(monkeypatch): + policy = _soxl_runtime_policy() + policy["small_account_hold"] = {"enabled": True, "hold_below_nav": -1} + entrypoint = SimpleNamespace(manifest=SimpleNamespace(profile="soxl_soxx_trend_income")) + runtime = strategy_runtime_module.LoadedStrategyRuntime( + entrypoint=entrypoint, + runtime_settings=replace( + _build_runtime_settings(profile="soxl_soxx_trend_income"), + runtime_target=build_runtime_target( + platform_id="ibkr", + strategy_profile="soxl_soxx_trend_income", + dry_run_only=False, + account_scope="paper-account-scope", + service_name="ibkr-paper-service", + strategy_release={ + "release_id": "soxl-release", + "manifest_sha256": "a" * 64, + "strategy_revision": "ues-revision", + "config_sha256": "b" * 64, + "risk_policy_sha256": "c" * 64, + "evidence_sha256": "d" * 64, + "plugin_bundle_sha256": "e" * 64, + "effective_session": "2026-09-17", + }, + ), + trusted_runtime_risk_policy=policy, + cash_only_execution=True, + reserved_cash_ratio=0.03, + ), + runtime_adapter=StrategyRuntimeAdapter(), + merged_runtime_config={ + "cash_reserve_ratio": 0.03, + "trend_exit_buffer": 0.02, + "option_overlay_enabled": False, + "option_growth_overlay_enabled": False, + "option_income_overlay_enabled": False, + }, + logger=lambda _message: None, + ) + snapshot = PortfolioSnapshot( + as_of=strategy_runtime_module.pd.Timestamp("2026-08-27", tz="UTC").to_pydatetime(), + total_equity=472.0, + metadata={ + "account_hash": "account-hash", + "total_equity_source": "broker_net_liquidation", + "source_digest_sha256": "b" * 64, + "broker_net_liquidation": 472.0, + }, + ) + monkeypatch.setattr(strategy_runtime_module, "_installed_ues_revision", lambda: "ues-revision") + capabilities = runtime._build_context_capabilities(ib=None, portfolio_snapshot=snapshot) + assert runtime._last_capability_status["runtime_risk_status"] == ( + "unavailable:invalid_small_account_hold" + ) + assert "small_account_hold_policy" not in capabilities + + +def test_fetch_portfolio_snapshot_for_context_uses_account_scoped_helper(monkeypatch): + observed = {} + + def fake_fetch(ib, **kwargs): + observed["ib"] = ib + observed["kwargs"] = kwargs + return PortfolioSnapshot( + as_of=strategy_runtime_module.pd.Timestamp("2026-09-17", tz="UTC").to_pydatetime(), + total_equity=500.0, + metadata={"account_hash": "U15998061"}, + ) + + runtime = strategy_runtime_module.LoadedStrategyRuntime( + entrypoint=SimpleNamespace(manifest=SimpleNamespace(profile="soxl_soxx_trend_income")), + runtime_settings=replace( + _build_runtime_settings(profile="soxl_soxx_trend_income"), + account_ids=("U15998061",), + market_currency="USD", + cash_only_execution=True, + ), + runtime_adapter=StrategyRuntimeAdapter(), + logger=lambda _message: None, + ) + monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", fake_fetch) + snapshot = runtime._fetch_portfolio_snapshot_for_context(object(), required=True) + assert snapshot.metadata["account_hash"] == "U15998061" + assert observed["kwargs"]["account_ids"] == ("U15998061",) + assert observed["kwargs"]["currency"] == "USD" + assert observed["kwargs"]["cash_only_execution"] is True + + def test_soxl_runtime_binds_explicit_limits_to_broker_and_installed_ues(monkeypatch): from quant_platform_kit.risk.contracts import RuntimeRiskLimits @@ -1461,6 +1614,7 @@ def test_soxl_runtime_rejects_policy_bound_to_wrong_account(monkeypatch): policy = _soxl_runtime_policy(account_hash="other-account") entrypoint = SimpleNamespace(manifest=SimpleNamespace(profile="soxl_soxx_trend_income")) + logs: list[str] = [] runtime = strategy_runtime_module.LoadedStrategyRuntime( entrypoint=entrypoint, runtime_settings=replace( @@ -1494,7 +1648,7 @@ def test_soxl_runtime_rejects_policy_bound_to_wrong_account(monkeypatch): "option_growth_overlay_enabled": False, "option_income_overlay_enabled": False, }, - logger=lambda _message: None, + logger=logs.append, ) snapshot = PortfolioSnapshot( as_of=strategy_runtime_module.pd.Timestamp("2026-08-27", tz="UTC").to_pydatetime(), @@ -1509,6 +1663,10 @@ def test_soxl_runtime_rejects_policy_bound_to_wrong_account(monkeypatch): capabilities = runtime._build_context_capabilities(ib=None, portfolio_snapshot=snapshot) assert runtime._last_capability_status["runtime_risk_status"] == "unavailable:runtime_binding_mismatch" assert not isinstance(capabilities["runtime_risk_limits"], RuntimeRiskLimits) + assert any( + "strategy_runtime_binding_mismatch" in message and "account_hash_mismatch" in message + for message in logs + ) def test_cash_only_runtime_overrides_force_option_overlays_off(): diff --git a/uv.lock b/uv.lock index 262ed05..d2993bf 100644 --- a/uv.lock +++ b/uv.lock @@ -17,7 +17,7 @@ resolution-markers = [ ] [manifest] -overrides = [{ name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=68c51590da8a5097b7de6d75b4ccb6a175318b48" }] +overrides = [{ name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=a90937621a7a05c9f72d0ca8a29be3fcb18a327c" }] [[package]] name = "beautifulsoup4" @@ -791,10 +791,10 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-cov", marker = "extra == 'test'" }, { name = "pytz" }, - { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=68c51590da8a5097b7de6d75b4ccb6a175318b48" }, + { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=a90937621a7a05c9f72d0ca8a29be3fcb18a327c" }, { name = "requests" }, { name = "ruff", marker = "extra == 'test'" }, - { name = "us-equity-strategies", git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=e2258223310913f6db9f40b810756db0ee2cfd68" }, + { name = "us-equity-strategies", git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=4a3943883cd6b5bbfe32a559e56a91b40a81b7ce" }, { name = "yfinance" }, ] provides-extras = ["test"] @@ -1327,7 +1327,7 @@ wheels = [ [[package]] name = "quant-platform-kit" version = "1.0.0" -source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=68c51590da8a5097b7de6d75b4ccb6a175318b48#68c51590da8a5097b7de6d75b4ccb6a175318b48" } +source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=a90937621a7a05c9f72d0ca8a29be3fcb18a327c#a90937621a7a05c9f72d0ca8a29be3fcb18a327c" } [[package]] name = "requests" @@ -1493,7 +1493,7 @@ wheels = [ [[package]] name = "us-equity-strategies" version = "0.7.60" -source = { git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=e2258223310913f6db9f40b810756db0ee2cfd68#e2258223310913f6db9f40b810756db0ee2cfd68" } +source = { git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=4a3943883cd6b5bbfe32a559e56a91b40a81b7ce#4a3943883cd6b5bbfe32a559e56a91b40a81b7ce" } dependencies = [ { name = "pandas" }, { name = "pytz" }, From 03cef49bf45cbcaed66d1c42033c88a4649dc6c3 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:49:59 +0800 Subject: [PATCH 2/3] ci: align QPK_EXPECTED_PIN with a9093762 Co-authored-by: Cursor --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 099fa5a..23d131c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: - name: Check QPK pin consistency run: | set -euo pipefail - QPK_EXPECTED_PIN=68c51590da8a5097b7de6d75b4ccb6a175318b48 uv run --no-sync python scripts/check_qpk_pin_consistency.py + QPK_EXPECTED_PIN=a90937621a7a05c9f72d0ca8a29be3fcb18a327c uv run --no-sync python scripts/check_qpk_pin_consistency.py - name: Ensure uv.lock matches pyproject.toml run: uv lock --check From c37c275777d88c58e7da092a4c5997ca3ea51a93 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:51:42 +0800 Subject: [PATCH 3/3] test: accept portfolio snapshot currency kwargs in runtime mocks Co-authored-by: Cursor --- tests/test_strategy_runtime.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_strategy_runtime.py b/tests/test_strategy_runtime.py index 067a273..64d94c1 100644 --- a/tests/test_strategy_runtime.py +++ b/tests/test_strategy_runtime.py @@ -284,7 +284,7 @@ def evaluate(self, ctx): monkeypatch.setattr( strategy_runtime_module, "fetch_portfolio_snapshot", - lambda _ib: PortfolioSnapshot( + lambda _ib, **_kwargs: PortfolioSnapshot( as_of=strategy_runtime_module.pd.Timestamp("2026-04-07").to_pydatetime(), total_equity=100000.0, metadata={"account_hash": "demo"}, @@ -699,7 +699,7 @@ def fake_guard(path, **kwargs): monkeypatch.setattr(strategy_runtime_module, "load_feature_snapshot_guarded", fake_guard) portfolio_snapshot = SimpleNamespace(total_equity=25000.0) - monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib: portfolio_snapshot) + monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib, **_kwargs: portfolio_snapshot) result = runtime.evaluate( ib="fake-ib", @@ -781,7 +781,7 @@ def evaluate(self, ctx): ), ) portfolio_snapshot = SimpleNamespace(total_equity=50000.0) - monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib: portfolio_snapshot) + monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib, **_kwargs: portfolio_snapshot) def close_loader(_ib, symbol, **_kwargs): return [100.0, 101.0] if symbol == "NVDA" else [] @@ -845,7 +845,7 @@ def loader(*_args, **_kwargs): logger=lambda _message: None, ) portfolio_snapshot = SimpleNamespace(total_equity=1200.0) - monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib: portfolio_snapshot) + monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib, **_kwargs: portfolio_snapshot) result = runtime.evaluate( ib="fake-ib", @@ -974,7 +974,7 @@ def evaluate(self, ctx): ) portfolio_snapshot = SimpleNamespace(total_equity=10000.0) - def fetch_snapshot(ib): + def fetch_snapshot(ib, **_kwargs): assert ib == "fake-ib" return portfolio_snapshot @@ -1115,7 +1115,7 @@ def evaluate(self, ctx): cash_balance=50000.0, positions=(), ) - monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib: portfolio_snapshot) + monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib, **_kwargs: portfolio_snapshot) def fake_loader(_ib, symbol, duration="2 Y", bar_size="1 day"): if symbol == "SOXL": @@ -1207,7 +1207,7 @@ def evaluate(self, ctx): cash_balance=50000.0, positions=(), ) - monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib: portfolio_snapshot) + monkeypatch.setattr(strategy_runtime_module, "fetch_portfolio_snapshot", lambda _ib, **_kwargs: portfolio_snapshot) def fake_candle_loader(_ib, symbol, duration="2 Y", bar_size="1 day"): assert symbol == "QQQ"