1use std::path::Path;
5
6use rayon::prelude::*;
7use serde::Serialize;
8use yuzu_core::backtest::BacktestConfig;
9use yuzu_core::report::Report;
10use yuzu_core::run_backtest;
11
12use crate::ctx::{load_ctx, referenced_series};
13
14#[derive(Clone, Copy)]
16pub enum SortKey {
17 Sharpe,
18 TotalReturn,
19 Cagr,
20 Calmar,
21}
22
23#[derive(Serialize)]
25pub struct SweepEntry {
26 pub name: String,
27 pub ok: bool,
28 pub error: Option<String>,
29 pub total_return: f64,
30 pub cagr: f64,
31 pub sharpe: f64,
32 pub sortino: f64,
33 pub max_drawdown: f64,
34 pub calmar: f64,
35}
36
37fn failed(name: &str, err: String) -> SweepEntry {
38 SweepEntry {
39 name: name.to_string(),
40 ok: false,
41 error: Some(err),
42 total_return: f64::NAN,
43 cagr: f64::NAN,
44 sharpe: f64::NAN,
45 sortino: f64::NAN,
46 max_drawdown: f64::NAN,
47 calmar: f64::NAN,
48 }
49}
50
51pub fn run_single(
57 root: &Path,
58 spec_json: &str,
59 from: i32,
60 to: i32,
61 cfg: &BacktestConfig,
62 price_key: &str,
63 symbols: Option<&[String]>,
64) -> Result<Report, String> {
65 let ctx = load_ctx(
66 root,
67 from,
68 to,
69 cfg,
70 price_key,
71 symbols,
72 &referenced_series(&[spec_json]),
73 )?;
74 run_backtest(spec_json, &ctx, price_key, cfg).map_err(|e| e.to_string())
75}
76
77pub fn run_sweep(
82 root: &Path,
83 variants: &[(String, String)],
84 from: i32,
85 to: i32,
86 cfg: &BacktestConfig,
87 price_key: &str,
88 sort_by: SortKey,
89) -> Vec<SweepEntry> {
90 let specs: Vec<&str> = variants.iter().map(|(_, s)| s.as_str()).collect();
91 let ctx = match load_ctx(
92 root,
93 from,
94 to,
95 cfg,
96 price_key,
97 None,
98 &referenced_series(&specs),
99 ) {
100 Ok(v) => v,
101 Err(e) => return variants.iter().map(|(n, _)| failed(n, e.clone())).collect(),
102 };
103
104 let mut board: Vec<SweepEntry> = variants
105 .par_iter()
106 .map(
107 |(name, spec)| match run_backtest(spec, &ctx, price_key, cfg) {
108 Ok(r) => SweepEntry {
109 name: name.clone(),
110 ok: true,
111 error: None,
112 total_return: r.metrics.total_return,
113 cagr: r.metrics.cagr,
114 sharpe: r.metrics.sharpe,
115 sortino: r.metrics.sortino,
116 max_drawdown: r.metrics.max_drawdown,
117 calmar: r.metrics.calmar,
118 },
119 Err(e) => failed(name, e.to_string()),
120 },
121 )
122 .collect();
123
124 let key = |e: &SweepEntry| match sort_by {
125 SortKey::Sharpe => e.sharpe,
126 SortKey::TotalReturn => e.total_return,
127 SortKey::Cagr => e.cagr,
128 SortKey::Calmar => e.calmar,
129 };
130 board.sort_by(|a, b| {
133 b.ok.cmp(&a.ok)
134 .then(key(a).is_nan().cmp(&key(b).is_nan()))
135 .then(
136 key(b)
137 .partial_cmp(&key(a))
138 .unwrap_or(std::cmp::Ordering::Equal),
139 )
140 });
141 board
142}