11from __future__ import annotations
22
33from collections .abc import Callable , Mapping
4- from dataclasses import dataclass , field
4+ from dataclasses import dataclass , field , replace
55from typing import Any
66
77import pandas as pd
2323 StrategyDecision ,
2424 StrategyEntrypoint ,
2525 StrategyRuntimeAdapter ,
26+ build_strategy_context_from_available_inputs ,
27+ build_strategy_evaluation_inputs ,
2628)
2729from runtime_config_support import PlatformRuntimeSettings
2830from strategy_loader import (
@@ -65,6 +67,68 @@ def profile(self) -> str:
6567 def required_inputs (self ) -> frozenset [str ]:
6668 return frozenset (self .entrypoint .manifest .required_inputs )
6769
70+ def _runtime_adapter_with_portfolio (
71+ self ,
72+ runtime_adapter : StrategyRuntimeAdapter ,
73+ portfolio_snapshot : Any | None ,
74+ ) -> StrategyRuntimeAdapter :
75+ if portfolio_snapshot is None or runtime_adapter .portfolio_input_name :
76+ return runtime_adapter
77+ available_inputs = set (runtime_adapter .available_inputs or self .required_inputs )
78+ available_inputs .update (self .required_inputs )
79+ available_inputs .add (_PORTFOLIO_SNAPSHOT_INPUT )
80+ return replace (
81+ runtime_adapter ,
82+ available_inputs = frozenset (available_inputs ),
83+ portfolio_input_name = _PORTFOLIO_SNAPSHOT_INPUT ,
84+ )
85+
86+ def _fetch_portfolio_snapshot_for_context (self , ib , * , required : bool ) -> Any | None :
87+ if ib is None and not required :
88+ return None
89+ if required :
90+ return fetch_portfolio_snapshot (ib )
91+ try :
92+ return fetch_portfolio_snapshot (ib )
93+ except Exception as exc :
94+ self .logger (
95+ "strategy_dashboard_portfolio_snapshot_failed | "
96+ f"profile={ self .profile } error_type={ type (exc ).__name__ } error={ exc } "
97+ )
98+ return None
99+
100+ def _build_strategy_context (
101+ self ,
102+ * ,
103+ runtime_adapter : StrategyRuntimeAdapter ,
104+ as_of : pd .Timestamp ,
105+ market_inputs : Mapping [str , Any ],
106+ portfolio_snapshot : Any | None ,
107+ runtime_config : Mapping [str , Any ],
108+ current_holdings ,
109+ ib ,
110+ ):
111+ context_adapter = self ._runtime_adapter_with_portfolio (runtime_adapter , portfolio_snapshot )
112+ available_inputs = set (context_adapter .available_inputs or self .required_inputs )
113+ available_inputs .update (self .required_inputs )
114+ evaluation_inputs = build_strategy_evaluation_inputs (
115+ available_inputs = available_inputs ,
116+ market_inputs = market_inputs ,
117+ portfolio_snapshot = portfolio_snapshot ,
118+ )
119+ capabilities = {}
120+ if ib is not None :
121+ capabilities ["broker_client" ] = ib
122+ return build_strategy_context_from_available_inputs (
123+ entrypoint = self .entrypoint ,
124+ runtime_adapter = context_adapter ,
125+ as_of = as_of ,
126+ available_inputs = evaluation_inputs ,
127+ runtime_config = runtime_config ,
128+ state = {"current_holdings" : tuple (current_holdings )},
129+ capabilities = capabilities ,
130+ )
131+
68132 def evaluate (
69133 self ,
70134 * ,
@@ -129,11 +193,12 @@ def _evaluate_market_data_strategy(
129193 runtime_config = dict (self .runtime_config )
130194 runtime_config .setdefault ("translator" , translator )
131195 runtime_config .setdefault ("pacing_sec" , float (pacing_sec ))
132- ctx = build_ibkr_strategy_context (
133- entrypoint = self .entrypoint ,
196+ portfolio_snapshot = self . _fetch_portfolio_snapshot_for_context ( ib , required = False )
197+ ctx = self ._build_strategy_context (
134198 runtime_adapter = self .runtime_adapter ,
135199 as_of = run_as_of ,
136200 market_inputs = build_market_history_inputs (historical_close_loader ),
201+ portfolio_snapshot = portfolio_snapshot ,
137202 runtime_config = runtime_config ,
138203 current_holdings = current_holdings ,
139204 ib = ib ,
@@ -151,6 +216,8 @@ def _evaluate_market_data_strategy(
151216 "status_icon" : self .status_icon ,
152217 "dry_run_only" : self .runtime_settings .dry_run_only ,
153218 }
219+ if portfolio_snapshot is not None :
220+ metadata ["portfolio_total_equity" ] = float (getattr (portfolio_snapshot , "total_equity" , 0.0 ) or 0.0 )
154221 if safe_haven_symbol :
155222 metadata ["safe_haven_symbol" ] = safe_haven_symbol
156223 return StrategyEvaluationResult (decision = decision , metadata = metadata )
@@ -248,14 +315,24 @@ def _evaluate_feature_snapshot_strategy(
248315 translator : Callable [[str ], str ],
249316 pacing_sec : float ,
250317 ) -> StrategyEvaluationResult :
251- del translator , pacing_sec
318+ del pacing_sec
252319 runtime_config_path = self .merged_runtime_config .get ("runtime_config_path" ) or self .runtime_settings .strategy_config_path
253320 benchmark_symbol = str (self .merged_runtime_config .get ("benchmark_symbol" ) or "SPY" ).strip ().upper ()
254321 portfolio_snapshot_holder : dict [str , Any ] = {}
322+ runtime_config = dict (self .runtime_config )
323+ runtime_config .setdefault ("translator" , translator )
255324
256325 def build_available_inputs (feature_snapshot ) -> Mapping [str , Any ]:
257- if _PORTFOLIO_SNAPSHOT_INPUT in self .required_inputs :
258- portfolio_snapshot_holder ["portfolio_snapshot" ] = fetch_portfolio_snapshot (ib )
326+ requires_portfolio = (
327+ _PORTFOLIO_SNAPSHOT_INPUT in self .required_inputs
328+ or self .runtime_adapter .portfolio_input_name == _PORTFOLIO_SNAPSHOT_INPUT
329+ )
330+ portfolio_snapshot = self ._fetch_portfolio_snapshot_for_context (
331+ ib ,
332+ required = requires_portfolio ,
333+ )
334+ if portfolio_snapshot is not None :
335+ portfolio_snapshot_holder ["portfolio_snapshot" ] = portfolio_snapshot
259336 market_inputs : dict [str , Any ] = {_FEATURE_SNAPSHOT_INPUT : feature_snapshot }
260337 if _MARKET_HISTORY_INPUT in self .required_inputs :
261338 market_inputs .update (build_market_history_inputs (historical_close_loader ))
@@ -274,15 +351,25 @@ def build_available_inputs(feature_snapshot) -> Mapping[str, Any]:
274351 return market_inputs
275352
276353 def build_context (request : FeatureSnapshotContextRequest ):
277- return build_ibkr_strategy_context (
354+ portfolio_snapshot = portfolio_snapshot_holder .get ("portfolio_snapshot" )
355+ runtime_adapter = self ._runtime_adapter_with_portfolio (
356+ request .runtime_adapter ,
357+ portfolio_snapshot ,
358+ )
359+ available_inputs = dict (request .available_inputs )
360+ if portfolio_snapshot is not None :
361+ available_inputs [_PORTFOLIO_SNAPSHOT_INPUT ] = portfolio_snapshot
362+ capabilities = {}
363+ if ib is not None :
364+ capabilities ["broker_client" ] = ib
365+ return build_strategy_context_from_available_inputs (
278366 entrypoint = request .entrypoint ,
279- runtime_adapter = request . runtime_adapter ,
367+ runtime_adapter = runtime_adapter ,
280368 as_of = request .as_of ,
281- market_inputs = request .available_inputs ,
282- portfolio_snapshot = portfolio_snapshot_holder .get ("portfolio_snapshot" ),
369+ available_inputs = available_inputs ,
283370 runtime_config = request .runtime_config ,
284- current_holdings = current_holdings ,
285- ib = ib ,
371+ state = { " current_holdings" : tuple ( current_holdings )} ,
372+ capabilities = capabilities ,
286373 )
287374
288375 def log_guard_metadata (guard_metadata : Mapping [str , Any ]) -> None :
@@ -323,7 +410,7 @@ def build_extra_metadata(
323410 strategy_config_source = self .runtime_settings .strategy_config_source ,
324411 dry_run_only = self .runtime_settings .dry_run_only ,
325412 ),
326- runtime_config = dict ( self . runtime_config ) ,
413+ runtime_config = runtime_config ,
327414 merged_runtime_config = self .merged_runtime_config ,
328415 as_of = run_as_of ,
329416 base_managed_symbols = (),
0 commit comments