neural_trader/
backtest.rs

1//! Backtesting engine bindings for Node.js
2
3use napi::bindgen_prelude::*;
4use napi_derive::napi;
5
6/// Backtest configuration
7#[napi(object)]
8pub struct BacktestConfig {
9    pub initial_capital: f64,
10    pub start_date: String,
11    pub end_date: String,
12    pub commission: f64,          // Per-share commission
13    pub slippage: f64,            // Slippage as percentage
14    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/// Trade record from backtest
31#[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/// Backtest performance metrics
45#[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/// Backtest result
65#[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/// Backtesting engine
74#[napi]
75pub struct BacktestEngine {
76    config: BacktestConfig,
77}
78
79#[napi]
80impl BacktestEngine {
81    /// Create a new backtest engine
82    #[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    /// Run backtest with strategy signals
95    #[napi]
96    pub async fn run(
97        &self,
98        signals: Vec<crate::strategy::Signal>,
99        _market_data: String,  // JSON string of market data
100    ) -> Result<BacktestResult> {
101        tracing::info!("Running backtest with {} signals", signals.len());
102
103        // TODO: Implement actual backtesting using nt-backtesting crate
104        // For now, return mock results
105
106        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        // Simulate some trades
111        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; // 2% profit
114            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; // Buy + sell
118
119            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, // Simplified
145            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    /// Calculate performance metrics from equity curve
169    #[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        // Calculate returns
176        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        // Calculate max drawdown
184        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        // Calculate Sharpe ratio
198        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() // Annualized
207        } else {
208            0.0
209        };
210
211        Ok(BacktestMetrics {
212            total_return,
213            annual_return: total_return, // Simplified
214            sharpe_ratio,
215            sortino_ratio: sharpe_ratio * 1.2, // Approximate
216            max_drawdown,
217            win_rate: 0.0,  // Need trade data
218            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    /// Export backtest results to CSV
231    #[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/// Compare multiple backtest results
255#[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}