From 7219569534437a9d5596356e9657d69d8f38f7dd Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:14:40 +0800 Subject: [PATCH 1/2] feat(strategy_runtime): compute unrealized PnL in portfolio metadata Enrich runtime metadata with QPK extract_portfolio_risk_diagnostics so decision_mapper can forward stop-loss / circuit-breaker inputs. Depends on QuantPlatformKit#197 (QPK pin bumped). Co-Authored-By: Claude Co-authored-by: Cursor --- pyproject.toml | 2 +- strategy_runtime.py | 3 +++ tests/test_strategy_runtime.py | 38 ++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3a7e027..00651da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "google-cloud-secret-manager", "google-cloud-storage", "yfinance", - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@f1d2c323b2a96383acec83a07bbf1816938c4650", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@69b55d80c61a8dc587e4f5b3641cc00bdb2bd472", "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@17ddb86c72d44b2c7b78ba7a10d8f71b21180166", "hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@b6a8ac2ad3c8110b5ea74fb059c8206388d63bcd", ] diff --git a/strategy_runtime.py b/strategy_runtime.py index 9d85ac2..190f484 100644 --- a/strategy_runtime.py +++ b/strategy_runtime.py @@ -444,6 +444,9 @@ def _enrich_portfolio_metadata( for position in getattr(portfolio_snapshot, "positions", ()) or () if str(getattr(position, "symbol", "") or "").strip() } + from quant_platform_kit.risk.portfolio_diagnostics import extract_portfolio_risk_diagnostics + + enriched.update(extract_portfolio_risk_diagnostics(portfolio_snapshot)) return enriched def _build_market_history_inputs( diff --git a/tests/test_strategy_runtime.py b/tests/test_strategy_runtime.py index 8adc0cc..58ada25 100644 --- a/tests/test_strategy_runtime.py +++ b/tests/test_strategy_runtime.py @@ -1183,3 +1183,41 @@ def fake_close_loader(_ib, symbol, duration="2 Y", bar_size="1 day"): assert close_loader_symbols == [("TQQQ", "10 D", "1 day"), ("BOXX", "10 D", "1 day")] assert result.metadata["price_fallback_source"] == "historical_close" assert result.metadata["price_fallbacks"] == {"TQQQ": 70.0, "BOXX": 105.0} + + +def test_enrich_portfolio_metadata_includes_unrealized_pnl(): + from datetime import datetime, timezone + + from quant_platform_kit.common.models import Position + + class FakeEntrypoint: + manifest = StrategyManifest( + profile="tech_communication_pullback_enhancement", + domain="us_equity", + display_name="Tech Pullback", + 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(), + ) + snapshot = PortfolioSnapshot( + as_of=datetime.now(timezone.utc), + total_equity=10_000.0, + positions=( + Position(symbol="SPY", quantity=10.0, market_value=4_500.0, average_cost=500.0), + ), + metadata={"consecutive_losses": 2}, + ) + + enriched = runtime._enrich_portfolio_metadata({}, snapshot) + + assert enriched["unrealized_pnl_pct"] == -0.05 + assert enriched["consecutive_losses"] == 2 + assert enriched["portfolio_total_equity"] == 10_000.0 From 459f14b09ea881972993b03c981d7c8c52210d77 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:18:08 +0800 Subject: [PATCH 2/2] fix(ibkr): QPK pin fallback for portfolio risk diagnostics Keep canonical QPK pin; use local fallback module until QPK#197 merges. Co-Authored-By: Claude Co-authored-by: Cursor --- application/portfolio_risk_diagnostics.py | 44 +++++++++++++++++++++++ pyproject.toml | 2 +- strategy_runtime.py | 2 +- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 application/portfolio_risk_diagnostics.py diff --git a/application/portfolio_risk_diagnostics.py b/application/portfolio_risk_diagnostics.py new file mode 100644 index 0000000..6c7de95 --- /dev/null +++ b/application/portfolio_risk_diagnostics.py @@ -0,0 +1,44 @@ +"""Portfolio risk diagnostics with QPK import fallback until pin bumps.""" + +from __future__ import annotations + +from typing import Any + +try: + from quant_platform_kit.risk.portfolio_diagnostics import extract_portfolio_risk_diagnostics +except ImportError: # pragma: no cover - exercised only before QPK pin bump + + def _position_unrealized_pnl(position: Any) -> float | None: + quantity = float(getattr(position, "quantity", 0.0) or 0.0) + if quantity == 0.0: + return 0.0 + market_value = float(getattr(position, "market_value", 0.0) or 0.0) + average_cost = getattr(position, "average_cost", None) + if average_cost is None: + return None + cost_basis = abs(quantity) * float(average_cost) + return market_value - cost_basis + + def extract_portfolio_risk_diagnostics(snapshot: Any) -> dict[str, float | int]: + diagnostics: dict[str, float | int] = {} + total_equity = float(getattr(snapshot, "total_equity", 0.0) or 0.0) + metadata = dict(getattr(snapshot, "metadata", None) or {}) + if metadata.get("unrealized_pnl_pct") is not None: + diagnostics["unrealized_pnl_pct"] = float(metadata["unrealized_pnl_pct"]) + elif total_equity > 0.0: + positions = getattr(snapshot, "positions", ()) or () + unrealized = 0.0 + has_cost_basis = False + for position in positions: + position_pnl = _position_unrealized_pnl(position) + if position_pnl is None: + continue + has_cost_basis = True + unrealized += position_pnl + if has_cost_basis or not positions: + diagnostics["unrealized_pnl_pct"] = float(unrealized / total_equity) + if metadata.get("consecutive_losses") is not None: + diagnostics["consecutive_losses"] = int(metadata["consecutive_losses"]) + return diagnostics + +__all__ = ["extract_portfolio_risk_diagnostics"] diff --git a/pyproject.toml b/pyproject.toml index 00651da..3a7e027 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "google-cloud-secret-manager", "google-cloud-storage", "yfinance", - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@69b55d80c61a8dc587e4f5b3641cc00bdb2bd472", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@f1d2c323b2a96383acec83a07bbf1816938c4650", "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@17ddb86c72d44b2c7b78ba7a10d8f71b21180166", "hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@b6a8ac2ad3c8110b5ea74fb059c8206388d63bcd", ] diff --git a/strategy_runtime.py b/strategy_runtime.py index 190f484..01f6ae4 100644 --- a/strategy_runtime.py +++ b/strategy_runtime.py @@ -444,7 +444,7 @@ def _enrich_portfolio_metadata( for position in getattr(portfolio_snapshot, "positions", ()) or () if str(getattr(position, "symbol", "") or "").strip() } - from quant_platform_kit.risk.portfolio_diagnostics import extract_portfolio_risk_diagnostics + from application.portfolio_risk_diagnostics import extract_portfolio_risk_diagnostics enriched.update(extract_portfolio_risk_diagnostics(portfolio_snapshot)) return enriched