99from quant_platform_kit .common .feature_snapshot import load_feature_snapshot_guarded
1010from quant_platform_kit .ibkr import (
1111 build_ibkr_strategy_context ,
12+ build_benchmark_history_inputs ,
1213 build_market_history_inputs ,
1314 build_semiconductor_rotation_inputs ,
1415 fetch_portfolio_snapshot ,
2930DEFAULT_CASH_RESERVE_RATIO = 0.03
3031_FEATURE_SNAPSHOT_INPUT = "feature_snapshot"
3132_MARKET_HISTORY_INPUT = "market_history"
33+ _BENCHMARK_HISTORY_INPUT = "benchmark_history"
3234_DERIVED_INDICATORS_INPUT = "derived_indicators"
3335_PORTFOLIO_SNAPSHOT_INPUT = "portfolio_snapshot"
3436
@@ -64,6 +66,7 @@ def evaluate(
6466 ib ,
6567 current_holdings ,
6668 historical_close_loader : Callable [..., Any ],
69+ historical_candle_loader : Callable [..., Any ] | None = None ,
6770 run_as_of : pd .Timestamp ,
6871 translator : Callable [[str ], str ],
6972 pacing_sec : float ,
@@ -81,15 +84,20 @@ def evaluate(
8184 ib = ib ,
8285 current_holdings = current_holdings ,
8386 historical_close_loader = historical_close_loader ,
87+ historical_candle_loader = historical_candle_loader ,
8488 run_as_of = run_as_of ,
8589 translator = translator ,
8690 pacing_sec = pacing_sec ,
8791 )
88- if {_DERIVED_INDICATORS_INPUT , _PORTFOLIO_SNAPSHOT_INPUT }.issubset (self .required_inputs ):
92+ if _PORTFOLIO_SNAPSHOT_INPUT in self .required_inputs and (
93+ _DERIVED_INDICATORS_INPUT in self .required_inputs
94+ or _BENCHMARK_HISTORY_INPUT in self .required_inputs
95+ ):
8996 return self ._evaluate_value_target_strategy (
9097 ib = ib ,
9198 current_holdings = current_holdings ,
9299 historical_close_loader = historical_close_loader ,
100+ historical_candle_loader = historical_candle_loader ,
93101 run_as_of = run_as_of ,
94102 translator = translator ,
95103 pacing_sec = pacing_sec ,
@@ -105,6 +113,7 @@ def _evaluate_market_data_strategy(
105113 ib ,
106114 current_holdings ,
107115 historical_close_loader : Callable [..., Any ],
116+ historical_candle_loader : Callable [..., Any ] | None ,
108117 run_as_of : pd .Timestamp ,
109118 translator : Callable [[str ], str ],
110119 pacing_sec : float ,
@@ -144,6 +153,7 @@ def _evaluate_value_target_strategy(
144153 ib ,
145154 current_holdings ,
146155 historical_close_loader : Callable [..., Any ],
156+ historical_candle_loader : Callable [..., Any ] | None ,
147157 run_as_of : pd .Timestamp ,
148158 translator : Callable [[str ], str ],
149159 pacing_sec : float ,
@@ -152,15 +162,16 @@ def _evaluate_value_target_strategy(
152162 runtime_config .setdefault ("translator" , translator )
153163 runtime_config .setdefault ("pacing_sec" , float (pacing_sec ))
154164 portfolio_snapshot = fetch_portfolio_snapshot (ib )
165+ market_inputs = self ._build_value_target_market_inputs (
166+ ib = ib ,
167+ historical_close_loader = historical_close_loader ,
168+ historical_candle_loader = historical_candle_loader ,
169+ )
155170 ctx = build_ibkr_strategy_context (
156171 entrypoint = self .entrypoint ,
157172 runtime_adapter = self .runtime_adapter ,
158173 as_of = run_as_of ,
159- market_inputs = build_semiconductor_rotation_inputs (
160- ib ,
161- historical_close_loader ,
162- trend_ma_window = int (self .merged_runtime_config .get ("trend_ma_window" , 150 )),
163- ),
174+ market_inputs = market_inputs ,
164175 portfolio_snapshot = portfolio_snapshot ,
165176 runtime_config = runtime_config ,
166177 current_holdings = current_holdings ,
@@ -183,8 +194,42 @@ def _evaluate_value_target_strategy(
183194 }
184195 if safe_haven_symbol :
185196 metadata ["safe_haven_symbol" ] = str (safe_haven_symbol )
197+ benchmark_symbol = market_inputs .get ("benchmark_symbol" )
198+ if benchmark_symbol :
199+ metadata ["benchmark_symbol" ] = str (benchmark_symbol )
186200 return StrategyEvaluationResult (decision = decision , metadata = metadata )
187201
202+ def _build_value_target_market_inputs (
203+ self ,
204+ * ,
205+ ib ,
206+ historical_close_loader : Callable [..., Any ],
207+ historical_candle_loader : Callable [..., Any ] | None ,
208+ ) -> dict [str , Any ]:
209+ if _DERIVED_INDICATORS_INPUT in self .required_inputs :
210+ return build_semiconductor_rotation_inputs (
211+ ib ,
212+ historical_close_loader ,
213+ trend_ma_window = int (self .merged_runtime_config .get ("trend_ma_window" , 150 )),
214+ )
215+ if _BENCHMARK_HISTORY_INPUT in self .required_inputs :
216+ if historical_candle_loader is None :
217+ raise ValueError (
218+ f"IBKR strategy profile { self .profile !r} requires benchmark_history but no candle loader was provided"
219+ )
220+ benchmark_symbol = str (self .merged_runtime_config .get ("benchmark_symbol" ) or "QQQ" ).strip ().upper ()
221+ market_inputs = build_benchmark_history_inputs (
222+ ib ,
223+ historical_candle_loader ,
224+ benchmark_symbol = benchmark_symbol ,
225+ )
226+ market_inputs ["benchmark_symbol" ] = benchmark_symbol
227+ return market_inputs
228+ raise ValueError (
229+ f"Unsupported value-target required_inputs for IBKR strategy profile { self .profile !r} : "
230+ f"{ ', ' .join (sorted (self .required_inputs )) or '<none>' } "
231+ )
232+
188233 def _evaluate_feature_snapshot_strategy (
189234 self ,
190235 * ,
0 commit comments