schwab_cli/agent/backtest/
report.rs1use std::collections::HashMap;
4use std::path::Path;
5
6use anyhow::Result;
7use serde_json::{json, Value};
8
9use crate::agent::journal;
10use crate::agent::paths::backtest_state_path;
11use crate::agent::sim::compute_stats;
12use crate::agent::state::load_state;
13use crate::rules::RulesConfig;
14
15pub fn build_backtest_report(rules_path: &Path, rules: &RulesConfig) -> Result<Value> {
16 let state = load_state(&backtest_state_path(rules_path)).unwrap_or_default();
17 let stats = compute_stats(&state, rules);
18 let events = journal::read_all_backtest(rules_path)?;
19
20 let mut event_counts: HashMap<String, u32> = HashMap::new();
21 let mut exit_reasons: HashMap<String, u32> = HashMap::new();
22 let mut monthly_pnl: HashMap<String, f64> = HashMap::new();
23 let mut trading_days = 0u32;
24 let mut entries = 0u32;
25 let mut rolls = 0u32;
26
27 for e in &events {
28 let Some(t) = e.get("type").and_then(|v| v.as_str()) else {
29 continue;
30 };
31 *event_counts.entry(t.to_string()).or_insert(0) += 1;
32 let payload = e.get("payload").cloned().unwrap_or(json!({}));
33 match t {
34 "backtest_day_summary" => {
35 trading_days += 1;
36 }
37 "sim_entry_filled" => entries += 1,
38 "defensive_roll" => rolls += 1,
39 "sim_exit_filled" => {
40 let reason = payload
41 .get("exit_reason")
42 .and_then(|v| v.as_str())
43 .unwrap_or("unknown")
44 .to_string();
45 *exit_reasons.entry(reason).or_insert(0) += 1;
46 let pnl = payload.get("pnl_usd").and_then(|v| v.as_f64()).unwrap_or(0.0);
47 if let Some(ts) = e.get("ts").and_then(|v| v.as_str()) {
48 if let Some(month) = ts.get(0..7) {
49 *monthly_pnl.entry(month.to_string()).or_insert(0.0) += pnl;
50 }
51 }
52 }
53 _ => {}
54 }
55 }
56
57 Ok(json!({
58 "agent_id": rules.agent_id,
59 "pricing_model": "black_scholes_vix_iv",
60 "caveat": "Synthetic BS marks using VIX as IV proxy — not OPRA historical fills. Use for gate/threshold research, not absolute expectancy.",
61 "ledger_stats": stats,
62 "event_counts": event_counts,
63 "exit_reason_counts": exit_reasons,
64 "monthly_closed_pnl_usd": monthly_pnl,
65 "trading_days_observed": trading_days,
66 "entries": entries,
67 "defensive_rolls": rolls,
68 "open_positions": state.open_positions.len(),
69 "closed_trades": state.sim.as_ref().map(|s| s.closed_trades.len()).unwrap_or(0),
70 }))
71}