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
8 changes: 7 additions & 1 deletion application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ def _record_platform_execution_telemetry(
"orders_filled": list(summary.get("orders_filled") or ()),
"orders_skipped": list(summary.get("orders_skipped") or ()),
"trade_date": summary.get("trade_date") or metadata.get("trade_date"),
"total_equity": summary.get("equity") or metadata.get("equity"),
"total_equity": (
summary.get("equity")
or summary.get("total_equity")
or metadata.get("equity")
or metadata.get("portfolio_total_equity")
or metadata.get("total_equity")
),
},
domain=str(metadata.get("domain") or ""),
)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies = [
"google-cloud-secret-manager",
"google-cloud-storage",
"yfinance",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@53b2ca73a5a50257b5d1a3c769b75c40924e4ba6",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@f6d5c2c64d2b0dcd2e16a8bb2093e737a281d305",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@17ddb86c72d44b2c7b78ba7a10d8f71b21180166",
"hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@b6a8ac2ad3c8110b5ea74fb059c8206388d63bcd",
]
Expand Down Expand Up @@ -64,5 +64,5 @@ include = [

[tool.uv]
override-dependencies = [
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@53b2ca73a5a50257b5d1a3c769b75c40924e4ba6",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@f6d5c2c64d2b0dcd2e16a8bb2093e737a281d305",
]
2 changes: 1 addition & 1 deletion qsl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ring = 3
allow_legacy = false

[qsl.requires]
quant_platform_kit = "53b2ca73a5a50257b5d1a3c769b75c40924e4ba6"
quant_platform_kit = "f6d5c2c64d2b0dcd2e16a8bb2093e737a281d305"
us_equity_strategies = "17ddb86c72d44b2c7b78ba7a10d8f71b21180166"
hk_equity_strategies = "b6a8ac2ad3c8110b5ea74fb059c8206388d63bcd"

Expand Down
61 changes: 54 additions & 7 deletions strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,46 @@ def _attach_strategy_plugin_metadata(
return portfolio_snapshot
return attach_strategy_plugin_metadata(portfolio_snapshot, tuple(strategy_plugin_signals or ()))

def _with_consecutive_loss_metadata(self, portfolio_snapshot: Any | None) -> Any | None:
"""Stamp trailing consecutive_losses onto portfolio metadata before evaluate."""
if portfolio_snapshot is None:
return None
metadata = dict(getattr(portfolio_snapshot, "metadata", None) or {})
if metadata.get("consecutive_losses") is not None:
return portfolio_snapshot
try:
from quant_platform_kit.strategy_lifecycle.live_equity import resolve_consecutive_losses
from quant_platform_kit.strategy_lifecycle.performance_monitor import infer_strategy_domain

streak = resolve_consecutive_losses(
domain=infer_strategy_domain(self.profile),
strategy_profile=self.profile,
)
except Exception as exc:
self.logger(
"strategy_consecutive_losses_resolve_failed | "
f"profile={self.profile} error_type={type(exc).__name__} error={exc}"
)
return portfolio_snapshot
if streak is None:
return portfolio_snapshot
metadata["consecutive_losses"] = int(streak)
return replace(portfolio_snapshot, metadata=metadata)

def _prepare_portfolio_snapshot(
self,
portfolio_snapshot: Any | None,
strategy_symbols=(),
strategy_plugin_signals=(),
*,
project: bool = True,
) -> Any | None:
snapshot = portfolio_snapshot
if project:
snapshot = self._project_portfolio_snapshot(snapshot, strategy_symbols)
snapshot = self._attach_strategy_plugin_metadata(snapshot, strategy_plugin_signals)
return self._with_consecutive_loss_metadata(snapshot)

@staticmethod
def _normalize_symbols(symbols) -> tuple[str, ...]:
normalized = []
Expand Down Expand Up @@ -680,11 +720,11 @@ def _evaluate_direct_market_data_strategy(
ib,
required=False,
)
portfolio_snapshot = self._project_portfolio_snapshot(
portfolio_snapshot = self._prepare_portfolio_snapshot(
portfolio_snapshot,
self._configured_strategy_symbols(include_ranking_pool=True),
strategy_plugin_signals,
)
portfolio_snapshot = self._attach_strategy_plugin_metadata(portfolio_snapshot, strategy_plugin_signals)
option_chains = self._fetch_option_chains_for_runtime(ib, runtime_config, portfolio_snapshot)
if option_chains:
runtime_config["option_chains"] = option_chains
Expand Down Expand Up @@ -760,11 +800,11 @@ def _evaluate_market_data_strategy(
ib,
required=requires_portfolio,
)
portfolio_snapshot = self._project_portfolio_snapshot(
portfolio_snapshot = self._prepare_portfolio_snapshot(
portfolio_snapshot,
self._configured_strategy_symbols(include_ranking_pool=True),
strategy_plugin_signals,
)
portfolio_snapshot = self._attach_strategy_plugin_metadata(portfolio_snapshot, strategy_plugin_signals)
option_chains = self._fetch_option_chains_for_runtime(ib, runtime_config, portfolio_snapshot)
if option_chains:
runtime_config["option_chains"] = option_chains
Expand Down Expand Up @@ -835,8 +875,11 @@ def _evaluate_value_target_strategy(
apply_runtime_policy_to_runtime_config(runtime_config, self.runtime_adapter)
managed_symbols = self._configured_strategy_symbols()
portfolio_snapshot = self._fetch_portfolio_snapshot_for_context(ib, required=True)
portfolio_snapshot = self._project_portfolio_snapshot(portfolio_snapshot, managed_symbols)
portfolio_snapshot = self._attach_strategy_plugin_metadata(portfolio_snapshot, strategy_plugin_signals)
portfolio_snapshot = self._prepare_portfolio_snapshot(
portfolio_snapshot,
managed_symbols,
strategy_plugin_signals,
)
option_chains = self._fetch_option_chains_for_runtime(ib, runtime_config, portfolio_snapshot)
if option_chains:
runtime_config["option_chains"] = option_chains
Expand Down Expand Up @@ -965,7 +1008,11 @@ def build_available_inputs(feature_snapshot) -> Mapping[str, Any]:
ib,
required=requires_portfolio,
)
portfolio_snapshot = self._attach_strategy_plugin_metadata(portfolio_snapshot, strategy_plugin_signals)
portfolio_snapshot = self._prepare_portfolio_snapshot(
portfolio_snapshot,
strategy_plugin_signals=strategy_plugin_signals,
project=False,
)
if portfolio_snapshot is not None:
portfolio_snapshot_holder["portfolio_snapshot"] = portfolio_snapshot
option_chains = self._fetch_option_chains_for_runtime(ib, runtime_config, portfolio_snapshot)
Expand Down
82 changes: 82 additions & 0 deletions tests/test_strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1221,3 +1221,85 @@ def evaluate(self, ctx):
assert enriched["unrealized_pnl_pct"] == -0.05
assert enriched["consecutive_losses"] == 2
assert enriched["portfolio_total_equity"] == 10_000.0


def test_with_consecutive_loss_metadata_stamps_from_resolver(monkeypatch):
from datetime import datetime, timezone

class FakeEntrypoint:
manifest = StrategyManifest(
profile="global_etf_rotation",
domain="us_equity",
display_name="Global ETF",
description="test",
required_inputs=frozenset(),
)

def evaluate(self, ctx):
return StrategyDecision(positions=())

runtime = strategy_runtime_module.LoadedStrategyRuntime(
entrypoint=FakeEntrypoint(),
runtime_adapter=StrategyRuntimeAdapter(),
runtime_settings=_build_runtime_settings("global_etf_rotation"),
logger=lambda _msg: None,
)
monkeypatch.setattr(
"quant_platform_kit.strategy_lifecycle.live_equity.resolve_consecutive_losses",
lambda **_kwargs: 4,
)
snapshot = PortfolioSnapshot(
as_of=datetime.now(timezone.utc),
total_equity=10_000.0,
positions=(),
metadata={},
)

stamped = runtime._with_consecutive_loss_metadata(snapshot)

assert stamped is not snapshot
assert stamped.metadata["consecutive_losses"] == 4


def test_with_consecutive_loss_metadata_preserves_explicit_value(monkeypatch):
from datetime import datetime, timezone

class FakeEntrypoint:
manifest = StrategyManifest(
profile="global_etf_rotation",
domain="us_equity",
display_name="Global ETF",
description="test",
required_inputs=frozenset(),
)

def evaluate(self, ctx):
return StrategyDecision(positions=())

runtime = strategy_runtime_module.LoadedStrategyRuntime(
entrypoint=FakeEntrypoint(),
runtime_adapter=StrategyRuntimeAdapter(),
runtime_settings=_build_runtime_settings("global_etf_rotation"),
logger=lambda _msg: None,
)
called = {"count": 0}

def _boom(**_kwargs):
called["count"] += 1
raise AssertionError("should not resolve when already set")

monkeypatch.setattr(
"quant_platform_kit.strategy_lifecycle.live_equity.resolve_consecutive_losses",
_boom,
)
snapshot = PortfolioSnapshot(
as_of=datetime.now(timezone.utc),
total_equity=10_000.0,
positions=(),
metadata={"consecutive_losses": 1},
)

stamped = runtime._with_consecutive_loss_metadata(snapshot)

assert stamped is snapshot
assert called["count"] == 0
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading