use std::fmt;
use crate::engine::*;
use chrono::{DateTime, Utc};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Event {
AddOrder(DateTime<Utc>, Order),
DelOrder(DateTime<Utc>, Order),
AddPosition(DateTime<Utc>, Position),
DelPosition(DateTime<Utc>, Position),
WalletUpdate {
datetime: DateTime<Utc>,
pnl: f64,
fees: f64,
free: f64,
locked: f64,
balance: f64,
},
}
impl From<(DateTime<Utc>, &Wallet)> for Event {
fn from((datetime, value): (DateTime<Utc>, &Wallet)) -> Self {
Self::WalletUpdate {
datetime,
locked: value.locked(),
fees: value.fees_paid(),
balance: value.balance(),
pnl: value.unrealized_pnl(),
free: value.free_balance().expect("should give the free balance"),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Metrics {
pnl: f64,
fees: f64,
balance: f64,
events: Vec<Event>,
initial_balance: f64,
}
impl From<&Backtest> for Metrics {
fn from(value: &Backtest) -> Self {
Self {
fees: value.fees_paid(),
balance: value.balance(),
pnl: value.unrealized_pnl(),
initial_balance: value.initial_balance(),
events: value.events().cloned().collect(),
}
}
}
impl Metrics {
pub fn new(events: Vec<Event>, initial_balance: f64, balance: f64, pnl: f64, fees: f64) -> Self {
Self {
pnl,
fees,
events,
balance,
initial_balance,
}
}
pub fn initial_balance(&self) -> f64 {
self.initial_balance
}
pub fn events(&self) -> std::slice::Iter<'_, Event> {
self.events.iter()
}
pub fn balance(&self) -> f64 {
self.balance
}
pub fn fees(&self) -> f64 {
self.fees
}
pub fn pnl(&self) -> f64 {
self.pnl
}
pub fn max_drawdown(&self) -> f64 {
let mut balance_history = Vec::new();
for event in &self.events {
if let Event::WalletUpdate { balance, .. } = event {
balance_history.push(*balance);
}
}
let mut max_peak = self.initial_balance;
let mut max_drawdown = 0.0;
for &balance in &balance_history {
if balance > max_peak {
max_peak = balance;
}
let drawdown = (max_peak - balance) / max_peak;
if drawdown > max_drawdown {
max_drawdown = drawdown;
}
}
max_drawdown * 100.0
}
pub fn profit_factor(&self) -> f64 {
let mut total_gains = 0.0;
let mut total_losses = 0.0;
for event in &self.events {
if let Event::DelPosition(_, position) = event {
let pnl = position.pnl().expect("pnl should be set the last exit price");
if pnl > 0.0 {
total_gains += pnl;
} else {
total_losses += pnl.abs();
}
}
}
if total_losses == 0.0 {
return f64::INFINITY;
}
total_gains / total_losses
}
pub fn sharpe_ratio(&self, risk_free_rate: f64) -> f64 {
let mut returns = Vec::new();
let mut previous_balance = self.initial_balance;
for event in &self.events {
if let Event::WalletUpdate { balance, .. } = event {
let return_pct = (*balance - previous_balance) / previous_balance;
returns.push(return_pct);
previous_balance = *balance;
}
}
let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
let std_dev = (returns.iter().map(|r| (r - mean_return).powi(2)).sum::<f64>() / returns.len() as f64).sqrt();
(mean_return - risk_free_rate) / std_dev
}
pub fn win_rate(&self) -> f64 {
let mut winning_trades = 0;
let mut total_trades = 0;
for event in &self.events {
if let Event::DelPosition(_, position) = event {
total_trades += 1;
if position.pnl().expect("pnl should be set the last exit price") > 0.0 {
winning_trades += 1;
}
}
}
if total_trades == 0 {
return 0.0;
}
(winning_trades as f64 / total_trades as f64) * 100.0
}
}
impl fmt::Display for Metrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "=== Backtest Metrics ===")?;
writeln!(f, "Initial Balance: {:.2}", self.initial_balance)?;
writeln!(f, "Final Balance: {:.2}", self.balance)?;
writeln!(f, "Profit & Loss (P&L): {:.2}", self.pnl)?;
writeln!(f, "Fees paid: {:.2}", self.fees)?;
#[allow(clippy::writeln_empty_string)]
writeln!(f, "")?;
writeln!(f, "Max Drawdown: {:.2}%", self.max_drawdown())?;
writeln!(f, "Profit Factor: {:.2}", self.profit_factor())?;
writeln!(f, "Sharpe Ratio (risk-free rate = 0.0): {:.2}", self.sharpe_ratio(0.0))?;
writeln!(f, "Win Rate: {:.2}%", self.win_rate())
}
}
#[cfg(test)]
fn create_position(pnl: f64) -> Position {
let order: Order = (OrderType::Market(100.0), 1.0, OrderSide::Buy).into();
let mut position = Position::from(order);
position.set_exit_price(100.0 + pnl).unwrap(); position
}
#[cfg(test)]
#[test]
fn max_drawdown() {
let events = vec![
Event::WalletUpdate {
datetime: DateTime::default(),
pnl: 0.0,
fees: 0.0,
free: 10000.0,
locked: 0.0,
balance: 10000.0,
},
Event::WalletUpdate {
datetime: DateTime::default(),
pnl: 0.0,
fees: 0.0,
free: 12000.0,
locked: 0.0,
balance: 12000.0,
},
Event::WalletUpdate {
datetime: DateTime::default(),
pnl: 0.0,
fees: 0.0,
free: 9000.0,
locked: 0.0,
balance: 9000.0,
},
Event::WalletUpdate {
datetime: DateTime::default(),
pnl: 0.0,
fees: 0.0,
free: 11000.0,
locked: 0.0,
balance: 11000.0,
},
];
let metrics = Metrics::new(events, 10000.0, 0.0, 0.0, 0.0);
assert_eq!(metrics.max_drawdown(), 25.0); }
#[cfg(test)]
#[test]
fn max_drawdown_no_events() {
let metrics = Metrics::new(vec![], 10000.0, 0.0, 0.0, 0.0);
assert_eq!(metrics.max_drawdown(), 0.0); }
#[cfg(test)]
#[test]
fn profit_factor() {
let winning_position = create_position(20.0);
let losing_position = create_position(-10.0);
let events = vec![
Event::DelPosition(DateTime::default(), winning_position),
Event::DelPosition(DateTime::default(), losing_position),
];
let metrics = Metrics::new(events, 10000.0, 0.0, 0.0, 0.0);
assert_eq!(metrics.profit_factor(), 2.0); }
#[cfg(test)]
#[test]
fn profit_factor_no_losses() {
let winning_position = create_position(20.0);
let events = vec![Event::DelPosition(DateTime::default(), winning_position)];
let metrics = Metrics::new(events, 10000.0, 0.0, 0.0, 0.0);
assert_eq!(metrics.profit_factor(), f64::INFINITY); }
#[cfg(test)]
#[test]
fn profit_factor_no_trades() {
let metrics = Metrics::new(vec![], 10000.0, 0.0, 0.0, 0.0);
assert_eq!(metrics.profit_factor(), f64::INFINITY); }
#[cfg(test)]
#[test]
fn sharpe_ratio() {
let events = vec![
Event::WalletUpdate {
datetime: DateTime::default(),
pnl: 0.0,
fees: 0.0,
free: 10000.0,
locked: 0.0,
balance: 10000.0,
},
Event::WalletUpdate {
datetime: DateTime::default(),
pnl: 0.0,
fees: 0.0,
free: 10500.0,
locked: 0.0,
balance: 10500.0,
},
Event::WalletUpdate {
datetime: DateTime::default(),
pnl: 0.0,
fees: 0.0,
free: 10300.0,
locked: 0.0,
balance: 10300.0,
},
Event::WalletUpdate {
datetime: DateTime::default(),
pnl: 0.0,
fees: 0.0,
free: 10700.0,
locked: 0.0,
balance: 10700.0,
},
];
let metrics = Metrics::new(events, 10000.0, 0.0, 0.0, 0.0);
let sharpe = metrics.sharpe_ratio(0.0);
assert!(sharpe > 0.0 && sharpe < 1.0);
}
#[cfg(test)]
#[test]
fn sharpe_ratio_no_events() {
let metrics = Metrics::new(vec![], 10000.0, 0.0, 0.0, 0.0);
assert!(metrics.sharpe_ratio(0.0).is_nan());
}
#[cfg(test)]
#[test]
fn win_rate() {
let winning_position = create_position(20.0);
let losing_position = create_position(-10.0);
let events = vec![
Event::DelPosition(DateTime::default(), winning_position),
Event::DelPosition(DateTime::default(), losing_position),
];
let metrics = Metrics::new(events, 10000.0, 0.0, 0.0, 0.0);
assert_eq!(metrics.win_rate(), 50.0); }
#[cfg(test)]
#[test]
fn win_rate_no_trades() {
let metrics = Metrics::new(vec![], 10000.0, 0.0, 0.0, 0.0);
assert_eq!(metrics.win_rate(), 0.0); }
#[cfg(test)]
#[test]
fn win_rate_all_winning() {
let winning_position = create_position(20.0);
let events = vec![Event::DelPosition(DateTime::default(), winning_position)];
let metrics = Metrics::new(events, 10000.0, 0.0, 0.0, 0.0);
assert_eq!(metrics.win_rate(), 100.0); }