4040 default_model_name ,
4141 make_llm_client ,
4242)
43+ from dashboard .backend .infrastructure .llm .pipeline_runner import (
44+ is_last_bar_of_trading_day ,
45+ recombine_pipeline ,
46+ run_post_trade_analysis ,
47+ split_pipeline ,
48+ trading_day_key ,
49+ )
4350
4451
4552class HourlyBacktester :
@@ -66,6 +73,10 @@ def __init__(self, start_date: str, end_date: str, session_id: str = "legacy-dem
6673 self .strategy_prompt = (strategy_prompt or "" ).strip () or None
6774 # Optional sub-agent pipeline (when set, overrides strategy_prompt).
6875 self .pipeline = pipeline if pipeline else None
76+ self .initial_pipeline = (
77+ json .loads (json .dumps (self .pipeline )) if self .pipeline else None
78+ )
79+ self .prompt_adaptations : List [Dict ] = []
6980 # Model id; defaults to the gateway-appropriate slug (CommonStack vs native).
7081 self .model = model or default_model_name ()
7182 self .live_run_id = (live_run_id or "" ).strip () or None
@@ -178,10 +189,66 @@ def _llm_run_metadata(self) -> Optional[Dict]:
178189 LLM_MAX_OUTPUT_TOKENS is an env knob that changes a run's spend and
179190 response truncation; recording the EFFECTIVE value (post defensive
180191 parse) makes runs auditable after the env changes. Rule-based runs
181- record nothing."""
182- if not self .use_llm :
183- return None
184- return {"llm_max_output_tokens" : llm_harness .DEFAULT_MAX_OUTPUT_TOKENS }
192+ record nothing unless post-trade adaptations were produced."""
193+ meta : Dict = {}
194+ if self .use_llm :
195+ meta ["llm_max_output_tokens" ] = llm_harness .DEFAULT_MAX_OUTPUT_TOKENS
196+ if self .prompt_adaptations :
197+ meta ["prompt_adaptations" ] = self .prompt_adaptations
198+ if self .initial_pipeline is not None :
199+ meta ["initial_pipeline" ] = self .initial_pipeline
200+ if self .pipeline is not None :
201+ meta ["final_pipeline" ] = self .pipeline
202+ return meta or None
203+
204+ def _current_equity (self , manager : PortfolioManager ) -> float :
205+ if manager .equity_history :
206+ return float (manager .equity_history [- 1 ].get ("equity" ) or manager .cash )
207+ return float (manager .cash )
208+
209+ def _run_daily_post_trade (
210+ self ,
211+ * ,
212+ manager : PortfolioManager ,
213+ day_episode : Dict ,
214+ post_trade_steps : List [Dict ],
215+ ) -> None :
216+ """Run once-per-day post-trade analysis and mutate ``self.pipeline``."""
217+ if not post_trade_steps or not self .use_llm or not self .llm_client :
218+ return
219+ if not day_episode .get ("trading_day" ):
220+ return
221+
222+ decision_steps , _ = split_pipeline (self .pipeline )
223+ start_eq = float (day_episode .get ("day_start_equity" ) or 0 )
224+ end_eq = self ._current_equity (manager )
225+ day_return = ((end_eq - start_eq ) / start_eq ) if start_eq else 0.0
226+ trade_start = int (day_episode .get ("trade_start_index" ) or 0 )
227+ day_trades = manager .trades [trade_start :]
228+
229+ episode_context = {
230+ "trading_day" : day_episode .get ("trading_day" ),
231+ "day_start_equity" : start_eq ,
232+ "day_end_equity" : end_eq ,
233+ "day_return" : day_return ,
234+ "trade_count" : len (day_trades ),
235+ "trades" : day_trades ,
236+ "latest_step_outputs" : day_episode .get ("latest_step_outputs" ) or [],
237+ }
238+
239+ patched , record , (in_tok , out_tok ), calls = run_post_trade_analysis (
240+ self .llm_client ,
241+ post_trade_steps = post_trade_steps ,
242+ episode_context = episode_context ,
243+ decision_pipeline = decision_steps ,
244+ model = self .model ,
245+ )
246+ manager .input_tokens += in_tok
247+ manager .output_tokens += out_tok
248+ manager .llm_calls += calls
249+ if record :
250+ self .prompt_adaptations .append (record )
251+ self .pipeline = recombine_pipeline (patched , post_trade_steps )
185252
186253 def run_agent_backtest (self ) -> Tuple [str , List [Dict ]]:
187254 """Run backtest with agent making hourly decisions."""
@@ -192,6 +259,12 @@ def run_agent_backtest(self) -> Tuple[str, List[Dict]]:
192259 llm_model = "rule-based" # Default
193260
194261 manager = PortfolioManager (initial_capital = INITIAL_CAPITAL )
262+ _decision_steps , post_trade_steps = split_pipeline (self .pipeline )
263+ if post_trade_steps :
264+ print (
265+ f" Post-trade analysis: { len (post_trade_steps )} step(s), "
266+ "once per trading day\n "
267+ )
195268
196269 # Get all timestamps
197270 all_timestamps = set ()
@@ -251,9 +324,25 @@ def run_agent_backtest(self) -> Tuple[str, List[Dict]]:
251324 price_cache [symbol ][timestamp ] = last_price
252325
253326 print (" ✅ Cache ready\n " )
327+
328+ day_episode : Dict = {
329+ "trading_day" : None ,
330+ "day_start_equity" : None ,
331+ "trade_start_index" : 0 ,
332+ "latest_step_outputs" : [],
333+ }
254334
255335 # Hourly loop
256336 for i , timestamp in enumerate (all_timestamps ):
337+ day_key = trading_day_key (timestamp )
338+ if day_episode ["trading_day" ] != day_key :
339+ day_episode = {
340+ "trading_day" : day_key ,
341+ "day_start_equity" : self ._current_equity (manager ),
342+ "trade_start_index" : len (manager .trades ),
343+ "latest_step_outputs" : [],
344+ }
345+
257346 # Get market data for this hour (real data when available)
258347 market_data = {}
259348 for symbol in DJIA_30 :
@@ -281,6 +370,8 @@ def run_agent_backtest(self) -> Tuple[str, List[Dict]]:
281370 llm_calls_count += 1 # Track that LLM was used
282371 if llm_calls_count == 1 : # Set on first call
283372 llm_model = self .model
373+ if manager .last_pipeline_step_outputs :
374+ day_episode ["latest_step_outputs" ] = manager .last_pipeline_step_outputs
284375 else :
285376 decision = manager .make_trading_decision (state )
286377
@@ -290,6 +381,13 @@ def run_agent_backtest(self) -> Tuple[str, List[Dict]]:
290381 # Update equity (uses forward-filled prices for smooth valuation)
291382 manager .update_equity (market_data , price_cache , timestamp )
292383 self ._publish_live_progress (i + 1 , total_steps , manager )
384+
385+ if post_trade_steps and is_last_bar_of_trading_day (all_timestamps , i ):
386+ self ._run_daily_post_trade (
387+ manager = manager ,
388+ day_episode = day_episode ,
389+ post_trade_steps = post_trade_steps ,
390+ )
293391
294392 # Progress
295393 if (i + 1 ) % 100 == 0 :
@@ -345,6 +443,8 @@ def run_agent_backtest(self) -> Tuple[str, List[Dict]]:
345443 print (f" • LLM Calls: { llm_calls_count } " )
346444 print (f" • Tokens: { manager .input_tokens :,} in / { manager .output_tokens :,} out (est. cost ${ est_cost :.4f} )" )
347445 print (f" • Trades: { len (manager .trades )} " )
446+ if self .prompt_adaptations :
447+ print (f" • Post-trade adaptations: { len (self .prompt_adaptations )} day(s)" )
348448 print (f" • Final: ${ final_eq :,.0f} " )
349449 print (f" • Return: { total_return * 100 :+.2f} %\n " )
350450
0 commit comments