From bbfe87edb75fa82605537ee0be7cd9da49a04577 Mon Sep 17 00:00:00 2001 From: Khizar Imran Date: Sat, 6 Jun 2026 17:16:32 +0100 Subject: [PATCH] checkpoint: spread/commisions working --- examples/simple_strategy.rs | 8 ++-- src/broker.rs | 23 +++++++---- src/engine.rs | 4 +- src/lib.rs | 1 + src/stats.rs | 76 +++++++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 src/stats.rs diff --git a/examples/simple_strategy.rs b/examples/simple_strategy.rs index 8abcab9..565d7c3 100644 --- a/examples/simple_strategy.rs +++ b/examples/simple_strategy.rs @@ -3,6 +3,8 @@ use backtestingfx::broker::Broker; use backtestingfx::strategy::Strategy; use backtestingfx::engine::Engine; use backtestingfx::data::load_csv; +use backtestingfx::stats::Stats; + struct BuyEveryBar; @@ -15,13 +17,13 @@ impl Strategy for BuyEveryBar { fn main() { let data = load_csv("examples/data/eurusd_lse_1h.csv"); - let mut engine = Engine::new(data, 10000.0); + 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 - println!("Final cash: {}", engine.broker.cash); - println!("Trades executed: {}", engine.broker.trade_history.len()); + let stats = Stats::compute(&engine.broker, 10_000.0); + println!("{}", stats); } diff --git a/src/broker.rs b/src/broker.rs index 5cfa535..8da7d8c 100644 --- a/src/broker.rs +++ b/src/broker.rs @@ -4,32 +4,41 @@ use crate::types::{Position, Trade}; pub struct Broker{ pub cash: f64, pub positions: Vec, - pub trade_history: Vec + pub trade_history: Vec, + pub commission: f64, + pub spread: f64 } impl Broker { - pub fn new(initial_cash: f64) -> Self { // does not need &mut because it initialises something new + pub fn new(initial_cash: f64, commission: f64, spread: f64) -> Self { // does not need &mut because it initialises something new Broker { cash: initial_cash, positions: Vec::new(), - trade_history: Vec::new() + trade_history: Vec::new(), + commission, + spread } } pub fn buy(&mut self, price: f64, lot_size: f64, timestamp: i64) { // needs to modify the broker with new position. (.push works with the Vec::) + let fill_price = price + self.spread; // buy at ask + self.cash -= self.commission * lot_size; // pay commission + self.positions.push(Position { - id: 0, - entry_price: price, + id: self.positions.len() as u64, + entry_price: fill_price, lot_size, is_long: true, entry_timestamp: timestamp, }); } - pub fn sell(&mut self, price: f64, lot_size: f64, timestamp: i64) { // needs to modify the broker with new position. (.push works with the Vec::) + pub fn sell(&mut self, price: f64, lot_size: f64, timestamp: i64) { + 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, - entry_price: price, + entry_price: fill_price, lot_size, is_long: false, entry_timestamp: timestamp diff --git a/src/engine.rs b/src/engine.rs index 000e142..00067b7 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -8,10 +8,10 @@ pub struct Engine { } impl Engine { - pub fn new(data: Vec, initial_cash: f64) -> Self { + pub fn new(data: Vec, initial_cash: f64, commission: f64, spread: f64) -> Self { Engine { data, - broker : Broker::new(initial_cash) // 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 } } diff --git a/src/lib.rs b/src/lib.rs index d5fe90d..2ca5c17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,3 +3,4 @@ pub mod strategy; pub mod broker; pub mod engine; pub mod data; +pub mod stats; \ No newline at end of file diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..c7d6c54 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,76 @@ +use crate::broker::Broker; + + pub struct Stats { + pub initial_cash: f64, + pub final_cash: f64, + pub total_return_pct: f64, + pub num_trades: usize, + pub num_wins: usize, + pub win_rate_pct: f64, + pub avg_pnl: f64, + pub best_trade: f64, + pub worst_trade: f64, + pub profit_factor: f64, + } + + impl Stats { + pub fn compute(broker: &Broker, initial_cash: f64) -> Self { + let num_trades = broker.trade_history.len(); + let final_cash = broker.cash; + let total_return_pct = (final_cash - initial_cash) / initial_cash * 100.0; + + let num_wins = broker.trade_history.iter().filter(|t| t.pnl > 0.0).count(); + let win_rate_pct = if num_trades > 0 { + num_wins as f64 / num_trades as f64 * 100.0 + } else { 0.0 }; + + let avg_pnl = if num_trades > 0 { + broker.trade_history.iter().map(|t| t.pnl).sum::() / num_trades as f64 + } else { 0.0 }; + + let best_trade = broker.trade_history.iter().map(|t| t.pnl) + .fold(f64::NEG_INFINITY, f64::max); + let worst_trade = broker.trade_history.iter().map(|t| t.pnl) + .fold(f64::INFINITY, f64::min); + + let gross_profit: f64 = broker.trade_history.iter() + .filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); + let gross_loss: f64 = broker.trade_history.iter() + .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 }; + + Stats { + initial_cash, + final_cash, + total_return_pct, + num_trades, + num_wins, + win_rate_pct, + avg_pnl, + best_trade: if num_trades > 0 { best_trade } else { 0.0 }, + worst_trade: if num_trades > 0 { worst_trade } else { 0.0 }, + profit_factor, + } + } + } + + impl std::fmt::Display for Stats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "--- Backtest Results ---\n\ + Initial Cash: {:.2}\n\ + Final Cash: {:.2}\n\ + Total Return: {:.2}%\n\ + Trades: {}\n\ + Win Rate: {:.1}%\n\ + Avg PnL: {:.5}\n\ + Best Trade: {:.5}\n\ + Worst Trade: {:.5}\n\ + Profit Factor: {:.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 + ) + } + } \ No newline at end of file