checkpoint: spread/commisions working

This commit is contained in:
Khizar Imran
2026-06-06 17:16:32 +01:00
parent 1dc728078f
commit bbfe87edb7
5 changed files with 100 additions and 12 deletions
+5 -3
View File
@@ -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);
}
+16 -7
View File
@@ -4,32 +4,41 @@ use crate::types::{Position, Trade};
pub struct Broker{
pub cash: f64,
pub positions: Vec<Position>,
pub trade_history: Vec<Trade>
pub trade_history: Vec<Trade>,
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
+2 -2
View File
@@ -8,10 +8,10 @@ pub struct Engine {
}
impl Engine {
pub fn new(data: Vec<Bar>, initial_cash: f64) -> Self {
pub fn new(data: Vec<Bar>, 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
}
}
+1
View File
@@ -3,3 +3,4 @@ pub mod strategy;
pub mod broker;
pub mod engine;
pub mod data;
pub mod stats;
+76
View File
@@ -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::<f64>() / 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
)
}
}