feat: strategy data access, sharpe ratio, and correctness fixes

- Trade.pnl now stores net pnl (after exit commission) so per-trade stats are accurate
- Position pyclass uses from_py_object to fix deprecation warning
- Removed dead AttributeError swallow in engine.rs
- Strategy gains self.data, self.index, self.cash, self.equity properties
- Broker.cash exposed to Python via pyo3(get)
- Sharpe ratio added to Stats (unannualized)
- Added examples/sma_cross.py and examples/compare_bt.py
- Logic verified against backtesting.py: 34 trades, 29.4% win rate match
This commit is contained in:
KhizarImran
2026-07-05 14:42:10 +01:00
parent 2af19afafa
commit c22153f02f
7 changed files with 182 additions and 15 deletions
+10 -6
View File
@@ -3,6 +3,7 @@ use pyo3::prelude::*;
#[pyclass]
pub struct Broker {
#[pyo3(get)]
pub cash: f64,
pub initial_cash: f64,
next_id: u64,
@@ -63,13 +64,14 @@ impl Broker {
* self.contract_size
* self.quote_to_account
};
self.cash += pnl - self.commission * position.lot_size;
let net_pnl = pnl - self.commission * position.lot_size;
self.cash += net_pnl;
self.trade_history.push(Trade {
entry_price: position.entry_price,
exit_price: close_price,
lot_size: position.lot_size,
is_long: position.is_long,
pnl,
pnl: net_pnl,
entry_timestamp: position.entry_timestamp,
exit_timestamp: bar.timestamp,
});
@@ -175,12 +177,13 @@ impl Broker {
* self.quote_to_account
};
self.cash += pnl - self.commission * position.lot_size;
let net_pnl = pnl - self.commission * position.lot_size;
self.cash += net_pnl;
self.trade_history.push(Trade {
entry_price: position.entry_price,
lot_size: position.lot_size,
is_long: position.is_long,
pnl,
pnl: net_pnl,
entry_timestamp: position.entry_timestamp,
exit_timestamp: timestamp,
exit_price: close_price,
@@ -207,12 +210,13 @@ impl Broker {
* self.contract_size
* self.quote_to_account
};
self.cash += pnl - self.commission * position.lot_size;
let net_pnl = pnl - self.commission * position.lot_size;
self.cash += net_pnl;
self.trade_history.push(Trade {
entry_price: position.entry_price,
lot_size: position.lot_size,
is_long: position.is_long,
pnl,
pnl: net_pnl,
entry_timestamp: position.entry_timestamp,
exit_timestamp: timestamp,
exit_price: close_price,
+1 -6
View File
@@ -54,12 +54,7 @@ impl Engine {
pub fn run_py(&mut self, py: Python<'_>, strategy: Py<PyAny>) -> PyResult<Stats> {
self.equity_curve.clear();
let init_result = strategy.bind(py).call_method1("init", (self.data.clone(),));
if let Err(e) = init_result {
if !e.is_instance_of::<pyo3::exceptions::PyAttributeError>(py) {
return Err(e);
}
}
strategy.bind(py).call_method1("init", (self.data.clone(),))?;
let broker_py = Py::new(
py,
+24 -2
View File
@@ -25,6 +25,24 @@ pub struct Stats {
pub profit_factor: f64,
#[pyo3(get)]
pub max_drawdown_pct: f64,
#[pyo3(get)]
pub sharpe_ratio: f64,
}
fn sharpe_ratio(equity_curve: &[f64]) -> f64 {
if equity_curve.len() < 2 {
return 0.0;
}
let returns: Vec<f64> = equity_curve
.windows(2)
.map(|w| (w[1] - w[0]) / w[0])
.collect();
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / returns.len() as f64;
if variance == 0.0 {
return 0.0;
}
mean / variance.sqrt()
}
fn max_drawdown(equity_curve: &[f64]) -> f64 {
@@ -101,6 +119,7 @@ impl Stats {
f64::INFINITY
};
let max_drawdown_pct = max_drawdown(equity_curve);
let sharpe_ratio = sharpe_ratio(equity_curve);
Stats {
initial_cash,
@@ -114,6 +133,7 @@ impl Stats {
worst_trade: if num_trades > 0 { worst_trade } else { 0.0 },
profit_factor,
max_drawdown_pct,
sharpe_ratio,
}
}
}
@@ -132,7 +152,8 @@ impl std::fmt::Display for Stats {
Best Trade: {:.5}\n\
Worst Trade: {:.5}\n\
Profit Factor: {:.2}\n\
Max Drawdown: {:.2}%",
Max Drawdown: {:.2}%\n\
Sharpe Ratio: {:.4} (unannualized)",
self.initial_cash,
self.final_cash,
self.total_return_pct,
@@ -142,7 +163,8 @@ impl std::fmt::Display for Stats {
self.best_trade,
self.worst_trade,
self.profit_factor,
self.max_drawdown_pct
self.max_drawdown_pct,
self.sharpe_ratio
)
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ impl Bar {
}
}
#[pyclass]
#[pyclass(from_py_object)]
#[derive(Debug, Clone)]
pub struct Position {
// this is for the trading position