Skip to content

Commit 2284ad8

Browse files
Pigbibiclaudecursoragent
authored
feat(risk): stamp consecutive_losses before strategy evaluate (#216)
Reuse QPK stamp helper so entrypoint circuit breakers see trailing loss streak from live equity history. Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8c4e03b commit 2284ad8

5 files changed

Lines changed: 83 additions & 8 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
"pytest",
1919
"pytz",
2020
"requests",
21-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@53b2ca73a5a50257b5d1a3c769b75c40924e4ba6",
21+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@69a0256934d081b5ef309a885384b9eb9f62cf90",
2222
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@17ddb86c72d44b2c7b78ba7a10d8f71b21180166",
2323
]
2424
license = "MIT"
@@ -82,5 +82,5 @@ show_missing = true
8282

8383
[tool.uv]
8484
override-dependencies = [
85-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@53b2ca73a5a50257b5d1a3c769b75c40924e4ba6",
85+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@69a0256934d081b5ef309a885384b9eb9f62cf90",
8686
]

qsl.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ ring = 3
55
allow_legacy = false
66

77
[qsl.requires]
8-
quant_platform_kit = "53b2ca73a5a50257b5d1a3c769b75c40924e4ba6"
8+
quant_platform_kit = "69a0256934d081b5ef309a885384b9eb9f62cf90"
99
us_equity_strategies = "17ddb86c72d44b2c7b78ba7a10d8f71b21180166"
1010

1111
[qsl.compat]

strategy_runtime.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ def managed_symbols(self) -> tuple[str, ...]:
5858
configured = self.merged_runtime_config.get("managed_symbols", ())
5959
return tuple(str(symbol) for symbol in configured)
6060

61+
def _stamp_portfolio_risk_metadata(self, available_inputs: Mapping[str, Any]) -> dict[str, Any]:
62+
resolved = dict(available_inputs)
63+
snapshot = resolved.get("portfolio_snapshot")
64+
if snapshot is None:
65+
return resolved
66+
from quant_platform_kit.strategy_lifecycle.live_equity import stamp_consecutive_losses_on_snapshot
67+
68+
resolved["portfolio_snapshot"] = stamp_consecutive_losses_on_snapshot(
69+
snapshot,
70+
strategy_profile=self.profile,
71+
logger=self.logger,
72+
)
73+
return resolved
74+
6175
def evaluate(
6276
self,
6377
*,
@@ -74,11 +88,11 @@ def evaluate(
7488
if _FEATURE_SNAPSHOT_INPUT in frozenset(self.entrypoint.manifest.required_inputs):
7589
return self._evaluate_feature_snapshot_strategy(
7690
runtime_config=runtime_config,
77-
available_inputs=available_inputs,
91+
available_inputs=self._stamp_portfolio_risk_metadata(available_inputs),
7892
)
7993

8094
as_of = datetime.now(timezone.utc)
81-
resolved_available_inputs = dict(available_inputs)
95+
resolved_available_inputs = self._stamp_portfolio_risk_metadata(available_inputs)
8296
resolved_available_inputs.update(
8397
resolve_external_market_signal_inputs(
8498
strategy_profile=self.profile,

tests/test_strategy_runtime.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,64 @@ def test_dca_overrides_ignore_non_dca_profiles():
115115
settings = _runtime_settings(dca_mode="smart", dca_base_investment_usd=500.0)
116116

117117
assert _build_runtime_overrides("global_etf_rotation", settings) == {}
118+
119+
120+
def test_stamp_portfolio_risk_metadata_calls_qpk_helper(monkeypatch):
121+
from datetime import datetime, timezone
122+
from unittest.mock import MagicMock
123+
124+
from quant_platform_kit.common.models import PortfolioSnapshot
125+
from quant_platform_kit.strategy_contracts import (
126+
StrategyDecision,
127+
StrategyManifest,
128+
StrategyRuntimeAdapter,
129+
)
130+
import strategy_runtime as strategy_runtime_module
131+
132+
class _Entrypoint:
133+
manifest = StrategyManifest(
134+
profile="global_etf_rotation",
135+
domain="us_equity",
136+
display_name="Global ETF",
137+
description="test",
138+
required_inputs=frozenset({"portfolio_snapshot"}),
139+
)
140+
141+
def evaluate(self, ctx):
142+
self.ctx = ctx
143+
return StrategyDecision()
144+
145+
entrypoint = _Entrypoint()
146+
runtime = strategy_runtime_module.LoadedStrategyRuntime(
147+
entrypoint=entrypoint,
148+
runtime_adapter=StrategyRuntimeAdapter(portfolio_input_name="portfolio_snapshot"),
149+
runtime_settings=_runtime_settings(strategy_profile="global_etf_rotation"),
150+
logger=lambda _msg: None,
151+
)
152+
snapshot = PortfolioSnapshot(
153+
as_of=datetime.now(timezone.utc),
154+
total_equity=1_000.0,
155+
positions=(),
156+
metadata={},
157+
)
158+
stamped = PortfolioSnapshot(
159+
as_of=snapshot.as_of,
160+
total_equity=snapshot.total_equity,
161+
positions=(),
162+
metadata={"consecutive_losses": 2},
163+
)
164+
stamp = MagicMock(return_value=stamped)
165+
monkeypatch.setattr(
166+
"quant_platform_kit.strategy_lifecycle.live_equity.stamp_consecutive_losses_on_snapshot",
167+
stamp,
168+
)
169+
monkeypatch.setattr(
170+
strategy_runtime_module,
171+
"resolve_external_market_signal_inputs",
172+
lambda **_kwargs: {},
173+
)
174+
175+
runtime.evaluate(translator=lambda key, **_kwargs: key, portfolio_snapshot=snapshot)
176+
177+
stamp.assert_called_once()
178+
assert entrypoint.ctx.portfolio.metadata["consecutive_losses"] == 2

uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)