diff --git a/examples/simple_strategy.rs b/examples/simple_strategy.rs index 565d7c3..32bbc95 100644 --- a/examples/simple_strategy.rs +++ b/examples/simple_strategy.rs @@ -3,7 +3,6 @@ use backtestingfx::broker::Broker; use backtestingfx::strategy::Strategy; use backtestingfx::engine::Engine; use backtestingfx::data::load_csv; -use backtestingfx::stats::Stats; struct BuyEveryBar; @@ -20,9 +19,7 @@ fn main() { let mut engine = Engine::new(data, 10_000.0, 0.0, 0.00010); let mut strategy = BuyEveryBar; - engine.run(&mut strategy); // main line that runs the strategy - - let stats = Stats::compute(&engine.broker, 10_000.0); + let stats = engine.run(&mut strategy); println!("{}", stats); } diff --git a/src/broker.rs b/src/broker.rs index 8da7d8c..bc8d9ff 100644 --- a/src/broker.rs +++ b/src/broker.rs @@ -3,6 +3,7 @@ use crate::types::{Position, Trade}; pub struct Broker{ pub cash: f64, + pub initial_cash: f64, pub positions: Vec, pub trade_history: Vec, pub commission: f64, @@ -13,6 +14,7 @@ impl Broker { pub fn new(initial_cash: f64, commission: f64, spread: f64) -> Self { // does not need &mut because it initialises something new Broker { cash: initial_cash, + initial_cash, positions: Vec::new(), trade_history: Vec::new(), commission, @@ -37,7 +39,7 @@ impl Broker { let fill_price = price - self.spread; // buy at ask self.cash -= self.commission * lot_size; // pay commission // needs to modify the broker with new position. (.push works with the Vec::) self.positions.push(Position { - id : 0, + id : self.positions.len() as u64, entry_price: fill_price, lot_size, is_long: false, @@ -88,5 +90,16 @@ impl Broker { } } + pub fn equity(&self, current_price: f64) -> f64 { // computes unrealised positions from the opened positions + let unrealized: f64 = self.positions.iter().map(|p| { + if p.is_long { + (current_price - p.entry_price) * p.lot_size + } else { + (p.entry_price - current_price) * p.lot_size + } + }).sum(); + + self.cash + unrealized + } } diff --git a/src/engine.rs b/src/engine.rs index 00067b7..2ba6e16 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,23 +1,29 @@ use crate::types::Bar; use crate::broker::Broker; use crate::strategy::Strategy; +use crate::stats::Stats; pub struct Engine { pub data: Vec, - pub broker: Broker + pub broker: Broker, + pub equity_curve: Vec } impl Engine { pub fn new(data: Vec, initial_cash: f64, commission: f64, spread: f64) -> Self { Engine { data, - broker : Broker::new(initial_cash, commission, spread) // it takes initial cash and not Broker as Engine is responsible for broker not the user + broker : Broker::new(initial_cash, commission, spread), // it takes initial cash and not Broker as Engine is responsible for broker not the user + equity_curve: Vec::new() } } - pub fn run (&mut self, strategy: &mut dyn Strategy) { //&mut dyn allows class inheritence + pub fn run (&mut self, strategy: &mut dyn Strategy) -> Stats { + strategy.init(&self.data); for bar in &self.data { strategy.next(bar, &mut self.broker); + self.equity_curve.push(self.broker.equity(bar.close)); } + Stats::compute(&self.broker, &self.equity_curve) } } \ No newline at end of file diff --git a/src/stats.rs b/src/stats.rs index c7d6c54..e346890 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -11,11 +11,32 @@ use crate::broker::Broker; pub best_trade: f64, pub worst_trade: f64, pub profit_factor: f64, + pub max_drawdown_pct: f64 + + } + + fn max_drawdown(equity_curve: &[f64]) -> f64 { + let mut peak = f64::NEG_INFINITY; + let mut max_dd = 0.0f64; + + for &equity in equity_curve { + if equity > peak { + peak = equity; + } + if peak > 0.0 { + let dd = (peak - equity) / peak * 100.0; + if dd > max_dd { + max_dd = dd; + } + } + } + max_dd } impl Stats { - pub fn compute(broker: &Broker, initial_cash: f64) -> Self { + pub fn compute(broker: &Broker, equity_curve: &[f64]) -> Self { let num_trades = broker.trade_history.len(); + let initial_cash = broker.initial_cash; let final_cash = broker.cash; let total_return_pct = (final_cash - initial_cash) / initial_cash * 100.0; @@ -39,6 +60,7 @@ use crate::broker::Broker; .filter(|t| t.pnl < 0.0).map(|t| t.pnl.abs()).sum(); let profit_factor = if gross_loss > 0.0 { gross_profit / gross_loss } else { f64::INFINITY }; + let max_drawdown_pct = max_drawdown(equity_curve); Stats { initial_cash, @@ -51,6 +73,7 @@ use crate::broker::Broker; best_trade: if num_trades > 0 { best_trade } else { 0.0 }, worst_trade: if num_trades > 0 { worst_trade } else { 0.0 }, profit_factor, + max_drawdown_pct } } } @@ -67,10 +90,12 @@ use crate::broker::Broker; Avg PnL: {:.5}\n\ Best Trade: {:.5}\n\ Worst Trade: {:.5}\n\ - Profit Factor: {:.2}", + Profit Factor: {:.2}\n\ + Max Drawdown: {:.2}%", self.initial_cash, self.final_cash, self.total_return_pct, self.num_trades, self.win_rate_pct, self.avg_pnl, - self.best_trade, self.worst_trade, self.profit_factor + self.best_trade, self.worst_trade, self.profit_factor, + self.max_drawdown_pct ) } } \ No newline at end of file diff --git a/src/strategy.rs b/src/strategy.rs index cff9c34..4fdb677 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -2,6 +2,7 @@ use crate::types::Bar; use crate::broker::Broker; pub trait Strategy { + fn init(&mut self, _data: &[Bar]) {} fn next(&mut self, bar: &Bar, broker: &mut Broker); }