1use napi::bindgen_prelude::*;
4use napi_derive::napi;
5
6#[napi(object)]
8pub struct BacktestConfig {
9 pub initial_capital: f64,
10 pub start_date: String,
11 pub end_date: String,
12 pub commission: f64, pub slippage: f64, pub use_mark_to_market: bool,
15}
16
17impl Default for BacktestConfig {
18 fn default() -> Self {
19 Self {
20 initial_capital: 100000.0,
21 start_date: "2023-01-01".to_string(),
22 end_date: "2023-12-31".to_string(),
23 commission: 0.001,
24 slippage: 0.0005,
25 use_mark_to_market: true,
26 }
27 }
28}
29
30#[napi(object)]
32pub struct Trade {
33 pub symbol: String,
34 pub entry_date: String,
35 pub exit_date: String,
36 pub entry_price: f64,
37 pub exit_price: f64,
38 pub quantity: i64,
39 pub pnl: f64,
40 pub pnl_percentage: f64,
41 pub commission_paid: f64,
42}
43
44#[napi(object)]
46pub struct BacktestMetrics {
47 pub total_return: f64,
48 pub annual_return: f64,
49 pub sharpe_ratio: f64,
50 pub sortino_ratio: f64,
51 pub max_drawdown: f64,
52 pub win_rate: f64,
53 pub profit_factor: f64,
54 pub total_trades: u32,
55 pub winning_trades: u32,
56 pub losing_trades: u32,
57 pub avg_win: f64,
58 pub avg_loss: f64,
59 pub largest_win: f64,
60 pub largest_loss: f64,
61 pub final_equity: f64,
62}
63
64#[napi(object)]
66pub struct BacktestResult {
67 pub metrics: BacktestMetrics,
68 pub trades: Vec<Trade>,
69 pub equity_curve: Vec<f64>,
70 pub dates: Vec<String>,
71}
72
73#[napi]
75pub struct BacktestEngine {
76 config: BacktestConfig,
77}
78
79#[napi]
80impl BacktestEngine {
81 #[napi(constructor)]
83 pub fn new(config: BacktestConfig) -> Self {
84 tracing::info!(
85 "Creating backtest engine: ${} capital, {} to {}",
86 config.initial_capital,
87 config.start_date,
88 config.end_date
89 );
90
91 Self { config }
92 }
93
94 #[napi]
96 pub async fn run(
97 &self,
98 signals: Vec<crate::strategy::Signal>,
99 _market_data: String, ) -> Result<BacktestResult> {
101 tracing::info!("Running backtest with {} signals", signals.len());
102
103 let mut trades = Vec::new();
107 let mut equity_curve = vec![self.config.initial_capital];
108 let mut dates = vec![self.config.start_date.clone()];
109
110 for (i, signal) in signals.iter().enumerate().take(10) {
112 let entry_price = 100.0 + i as f64;
113 let exit_price = entry_price * 1.02; let quantity = 100;
115
116 let pnl = (exit_price - entry_price) * quantity as f64;
117 let commission = self.config.commission * quantity as f64 * 2.0; trades.push(Trade {
120 symbol: signal.symbol.clone(),
121 entry_date: format!("2023-{:02}-01", i + 1),
122 exit_date: format!("2023-{:02}-15", i + 1),
123 entry_price,
124 exit_price,
125 quantity,
126 pnl: pnl - commission,
127 pnl_percentage: ((exit_price - entry_price) / entry_price) * 100.0,
128 commission_paid: commission,
129 });
130
131 let new_equity = equity_curve.last().unwrap() + pnl - commission;
132 equity_curve.push(new_equity);
133 dates.push(format!("2023-{:02}-15", i + 1));
134 }
135
136 let final_equity = *equity_curve.last().unwrap();
137 let total_return = (final_equity - self.config.initial_capital) / self.config.initial_capital;
138
139 let winning_trades = trades.iter().filter(|t| t.pnl > 0.0).count() as u32;
140 let losing_trades = trades.len() as u32 - winning_trades;
141
142 let metrics = BacktestMetrics {
143 total_return,
144 annual_return: total_return, sharpe_ratio: 1.5,
146 sortino_ratio: 2.0,
147 max_drawdown: 0.1,
148 win_rate: winning_trades as f64 / trades.len() as f64,
149 profit_factor: 2.0,
150 total_trades: trades.len() as u32,
151 winning_trades,
152 losing_trades,
153 avg_win: 200.0,
154 avg_loss: -100.0,
155 largest_win: 500.0,
156 largest_loss: -200.0,
157 final_equity,
158 };
159
160 Ok(BacktestResult {
161 metrics,
162 trades,
163 equity_curve,
164 dates,
165 })
166 }
167
168 #[napi]
170 pub fn calculate_metrics(&self, equity_curve: Vec<f64>) -> Result<BacktestMetrics> {
171 if equity_curve.len() < 2 {
172 return Err(Error::from_reason("Equity curve too short"));
173 }
174
175 let returns: Vec<f64> = equity_curve
177 .windows(2)
178 .map(|w| (w[1] - w[0]) / w[0])
179 .collect();
180
181 let total_return = (equity_curve.last().unwrap() - equity_curve[0]) / equity_curve[0];
182
183 let mut max_drawdown = 0.0;
185 let mut peak = equity_curve[0];
186
187 for &value in &equity_curve {
188 if value > peak {
189 peak = value;
190 }
191 let drawdown = (peak - value) / peak;
192 if drawdown > max_drawdown {
193 max_drawdown = drawdown;
194 }
195 }
196
197 let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
199 let variance: f64 = returns
200 .iter()
201 .map(|r| (r - mean_return).powi(2))
202 .sum::<f64>()
203 / returns.len() as f64;
204 let std_dev = variance.sqrt();
205 let sharpe_ratio = if std_dev > 0.0 {
206 (mean_return / std_dev) * (252.0_f64).sqrt() } else {
208 0.0
209 };
210
211 Ok(BacktestMetrics {
212 total_return,
213 annual_return: total_return, sharpe_ratio,
215 sortino_ratio: sharpe_ratio * 1.2, max_drawdown,
217 win_rate: 0.0, profit_factor: 0.0,
219 total_trades: 0,
220 winning_trades: 0,
221 losing_trades: 0,
222 avg_win: 0.0,
223 avg_loss: 0.0,
224 largest_win: 0.0,
225 largest_loss: 0.0,
226 final_equity: *equity_curve.last().unwrap(),
227 })
228 }
229
230 #[napi]
232 pub fn export_trades_csv(&self, trades: Vec<Trade>) -> Result<String> {
233 let mut csv = String::from("Symbol,Entry Date,Exit Date,Entry Price,Exit Price,Quantity,PnL,PnL%,Commission\n");
234
235 for trade in trades {
236 csv.push_str(&format!(
237 "{},{},{},{:.2},{:.2},{},{:.2},{:.2}%,{:.2}\n",
238 trade.symbol,
239 trade.entry_date,
240 trade.exit_date,
241 trade.entry_price,
242 trade.exit_price,
243 trade.quantity,
244 trade.pnl,
245 trade.pnl_percentage,
246 trade.commission_paid
247 ));
248 }
249
250 Ok(csv)
251 }
252}
253
254#[napi]
256pub fn compare_backtests(results: Vec<BacktestResult>) -> Result<String> {
257 if results.is_empty() {
258 return Err(Error::from_reason("No results to compare"));
259 }
260
261 let comparison = results
262 .iter()
263 .enumerate()
264 .map(|(i, result)| {
265 serde_json::json!({
266 "strategy": format!("Strategy {}", i + 1),
267 "total_return": result.metrics.total_return,
268 "sharpe_ratio": result.metrics.sharpe_ratio,
269 "max_drawdown": result.metrics.max_drawdown,
270 "total_trades": result.metrics.total_trades,
271 "win_rate": result.metrics.win_rate,
272 })
273 })
274 .collect::<Vec<_>>();
275
276 let output = serde_json::json!({ "comparison": comparison });
277 Ok(output.to_string())
278}