use crate::Decimal;
use crate::execution::Side;
use crate::types::error::{MMError, MMResult};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EquityPoint {
pub timestamp: u64,
pub equity: Decimal,
}
impl EquityPoint {
#[must_use]
pub fn new(timestamp: u64, equity: Decimal) -> Self {
Self { timestamp, equity }
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TradeRecord {
pub entry_time: u64,
pub exit_time: u64,
pub side: Side,
pub entry_price: Decimal,
pub exit_price: Decimal,
pub quantity: Decimal,
pub pnl: Decimal,
pub fees: Decimal,
}
impl TradeRecord {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
entry_time: u64,
exit_time: u64,
side: Side,
entry_price: Decimal,
exit_price: Decimal,
quantity: Decimal,
pnl: Decimal,
fees: Decimal,
) -> Self {
Self {
entry_time,
exit_time,
side,
entry_price,
exit_price,
quantity,
pnl,
fees,
}
}
#[must_use]
pub fn net_pnl(&self) -> Decimal {
self.pnl - self.fees
}
#[must_use]
pub fn is_winner(&self) -> bool {
self.net_pnl() > Decimal::ZERO
}
#[must_use]
pub fn is_loser(&self) -> bool {
self.net_pnl() < Decimal::ZERO
}
#[must_use]
pub fn duration_ms(&self) -> u64 {
self.exit_time.saturating_sub(self.entry_time)
}
#[must_use]
pub fn return_pct(&self) -> Decimal {
if self.entry_price == Decimal::ZERO {
return Decimal::ZERO;
}
let notional = self.entry_price * self.quantity;
if notional == Decimal::ZERO {
return Decimal::ZERO;
}
(self.net_pnl() / notional) * Decimal::ONE_HUNDRED
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MetricsConfig {
pub risk_free_rate: Decimal,
pub trading_days_per_year: u32,
pub benchmark_returns: Option<Vec<Decimal>>,
}
impl MetricsConfig {
#[must_use]
pub fn new(risk_free_rate: Decimal, trading_days_per_year: u32) -> Self {
Self {
risk_free_rate,
trading_days_per_year,
benchmark_returns: None,
}
}
#[must_use]
pub fn with_risk_free_rate(mut self, rate: Decimal) -> Self {
self.risk_free_rate = rate;
self
}
#[must_use]
pub fn with_trading_days(mut self, days: u32) -> Self {
self.trading_days_per_year = days;
self
}
#[must_use]
pub fn with_benchmark(mut self, returns: Vec<Decimal>) -> Self {
self.benchmark_returns = Some(returns);
self
}
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
risk_free_rate: Decimal::ZERO,
trading_days_per_year: 252,
benchmark_returns: None,
}
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PerformanceMetrics {
pub total_return: Decimal,
pub total_return_pct: Decimal,
pub annualized_return: Decimal,
pub cagr: Decimal,
pub volatility: Decimal,
pub downside_volatility: Decimal,
pub max_drawdown: Decimal,
pub max_drawdown_duration_ms: u64,
pub var_95: Decimal,
pub var_99: Decimal,
pub sharpe_ratio: Decimal,
pub sortino_ratio: Decimal,
pub calmar_ratio: Decimal,
pub information_ratio: Option<Decimal>,
pub total_trades: u64,
pub winning_trades: u64,
pub losing_trades: u64,
pub win_rate: Decimal,
pub profit_factor: Decimal,
pub average_trade_pnl: Decimal,
pub average_winner: Decimal,
pub average_loser: Decimal,
pub largest_winner: Decimal,
pub largest_loser: Decimal,
pub avg_trade_duration_ms: u64,
pub average_spread_captured: Option<Decimal>,
pub inventory_turnover: Option<Decimal>,
pub time_in_market_pct: Option<Decimal>,
}
impl PerformanceMetrics {
#[must_use]
pub fn is_profitable(&self) -> bool {
self.total_return > Decimal::ZERO
}
#[must_use]
pub fn risk_reward_ratio(&self) -> Decimal {
if self.average_loser == Decimal::ZERO {
return Decimal::ZERO;
}
self.average_winner / self.average_loser.abs()
}
#[must_use]
pub fn expectancy(&self) -> Decimal {
let win_component = self.win_rate * self.average_winner;
let loss_component = (Decimal::ONE - self.win_rate) * self.average_loser.abs();
win_component - loss_component
}
}
#[derive(Debug, Clone)]
pub struct MetricsCalculator {
config: MetricsConfig,
}
impl MetricsCalculator {
#[must_use]
pub fn new(config: MetricsConfig) -> Self {
Self { config }
}
#[must_use]
pub fn with_defaults() -> Self {
Self::new(MetricsConfig::default())
}
#[must_use]
pub fn config(&self) -> &MetricsConfig {
&self.config
}
pub fn calculate(
&self,
equity_curve: &[EquityPoint],
trades: &[TradeRecord],
initial_capital: Decimal,
) -> MMResult<PerformanceMetrics> {
if equity_curve.is_empty() {
return Err(MMError::InvalidConfiguration(
"Equity curve cannot be empty".to_string(),
));
}
let returns = self.calculate_returns(equity_curve);
let (max_dd, max_dd_duration) = self.max_drawdown(equity_curve);
let final_equity = equity_curve
.last()
.map(|p| p.equity)
.unwrap_or(initial_capital);
let total_return = final_equity - initial_capital;
let total_return_pct = if initial_capital != Decimal::ZERO {
(total_return / initial_capital) * Decimal::ONE_HUNDRED
} else {
Decimal::ZERO
};
let time_span_ms = if equity_curve.len() >= 2 {
equity_curve.last().unwrap().timestamp - equity_curve.first().unwrap().timestamp
} else {
0
};
let years = self.ms_to_years(time_span_ms);
let annualized_return =
self.annualize_return(total_return_pct / Decimal::ONE_HUNDRED, years);
let cagr = self.calculate_cagr(initial_capital, final_equity, years);
let volatility = self.calculate_volatility(&returns);
let downside_volatility = self.calculate_downside_volatility(&returns);
let var_95 = self.var(&returns, Decimal::from_str_exact("0.95").unwrap());
let var_99 = self.var(&returns, Decimal::from_str_exact("0.99").unwrap());
let sharpe = self.sharpe_ratio(&returns);
let sortino = self.sortino_ratio(&returns);
let calmar = if max_dd != Decimal::ZERO {
annualized_return / max_dd
} else {
Decimal::ZERO
};
let information_ratio = self.calculate_information_ratio(&returns);
let trading_metrics = self.calculate_trading_metrics(trades);
Ok(PerformanceMetrics {
total_return,
total_return_pct,
annualized_return,
cagr,
volatility,
downside_volatility,
max_drawdown: max_dd,
max_drawdown_duration_ms: max_dd_duration,
var_95,
var_99,
sharpe_ratio: sharpe,
sortino_ratio: sortino,
calmar_ratio: calmar,
information_ratio,
..trading_metrics
})
}
#[must_use]
pub fn calculate_returns(&self, equity_curve: &[EquityPoint]) -> Vec<Decimal> {
if equity_curve.len() < 2 {
return Vec::new();
}
equity_curve
.windows(2)
.filter_map(|w| {
if w[0].equity != Decimal::ZERO {
Some((w[1].equity - w[0].equity) / w[0].equity)
} else {
None
}
})
.collect()
}
#[must_use]
pub fn sharpe_ratio(&self, returns: &[Decimal]) -> Decimal {
if returns.is_empty() {
return Decimal::ZERO;
}
let mean = self.mean(returns);
let std_dev = self.std_dev(returns);
if std_dev == Decimal::ZERO {
return Decimal::ZERO;
}
let daily_rf =
self.config.risk_free_rate / Decimal::from(self.config.trading_days_per_year);
let excess_return = mean - daily_rf;
let annualization_factor = decimal_sqrt(Decimal::from(self.config.trading_days_per_year));
(excess_return / std_dev) * annualization_factor
}
#[must_use]
pub fn sortino_ratio(&self, returns: &[Decimal]) -> Decimal {
if returns.is_empty() {
return Decimal::ZERO;
}
let mean = self.mean(returns);
let downside_dev = self.downside_deviation(returns);
if downside_dev == Decimal::ZERO {
return Decimal::ZERO;
}
let daily_rf =
self.config.risk_free_rate / Decimal::from(self.config.trading_days_per_year);
let excess_return = mean - daily_rf;
let annualization_factor = decimal_sqrt(Decimal::from(self.config.trading_days_per_year));
(excess_return / downside_dev) * annualization_factor
}
#[must_use]
pub fn max_drawdown(&self, equity_curve: &[EquityPoint]) -> (Decimal, u64) {
if equity_curve.is_empty() {
return (Decimal::ZERO, 0);
}
let mut peak = equity_curve[0].equity;
let _peak_time = equity_curve[0].timestamp;
let mut max_dd = Decimal::ZERO;
let mut max_dd_duration: u64 = 0;
let mut current_dd_start = equity_curve[0].timestamp;
for point in equity_curve {
if point.equity > peak {
peak = point.equity;
current_dd_start = point.timestamp;
}
if peak > Decimal::ZERO {
let dd = (peak - point.equity) / peak;
if dd > max_dd {
max_dd = dd;
max_dd_duration = point.timestamp - current_dd_start;
}
}
}
(max_dd, max_dd_duration)
}
#[must_use]
pub fn var(&self, returns: &[Decimal], confidence: Decimal) -> Decimal {
if returns.is_empty() {
return Decimal::ZERO;
}
let mut sorted: Vec<Decimal> = returns.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let percentile = Decimal::ONE - confidence;
let index = (percentile * Decimal::from(sorted.len() - 1))
.to_string()
.parse::<f64>()
.unwrap_or(0.0) as usize;
let index = index.min(sorted.len() - 1);
-sorted[index]
}
#[must_use]
pub fn profit_factor(&self, trades: &[TradeRecord]) -> Decimal {
let gross_profit: Decimal = trades
.iter()
.filter(|t| t.net_pnl() > Decimal::ZERO)
.map(|t| t.net_pnl())
.sum();
let gross_loss: Decimal = trades
.iter()
.filter(|t| t.net_pnl() < Decimal::ZERO)
.map(|t| t.net_pnl().abs())
.sum();
if gross_loss == Decimal::ZERO {
if gross_profit > Decimal::ZERO {
return Decimal::from(999); }
return Decimal::ZERO;
}
gross_profit / gross_loss
}
fn calculate_volatility(&self, returns: &[Decimal]) -> Decimal {
let std_dev = self.std_dev(returns);
let annualization = decimal_sqrt(Decimal::from(self.config.trading_days_per_year));
std_dev * annualization
}
fn calculate_downside_volatility(&self, returns: &[Decimal]) -> Decimal {
let downside_dev = self.downside_deviation(returns);
let annualization = decimal_sqrt(Decimal::from(self.config.trading_days_per_year));
downside_dev * annualization
}
fn calculate_cagr(&self, initial: Decimal, final_val: Decimal, years: Decimal) -> Decimal {
if initial <= Decimal::ZERO || years <= Decimal::ZERO {
return Decimal::ZERO;
}
let ratio = final_val / initial;
if ratio <= Decimal::ZERO {
return Decimal::ZERO;
}
let ln_ratio = decimal_ln(ratio);
let exponent = ln_ratio / years;
decimal_exp(exponent) - Decimal::ONE
}
fn annualize_return(&self, total_return: Decimal, years: Decimal) -> Decimal {
if years <= Decimal::ZERO {
return Decimal::ZERO;
}
total_return / years
}
fn ms_to_years(&self, ms: u64) -> Decimal {
let ms_per_day = Decimal::from(86_400_000u64);
let days = Decimal::from(ms) / ms_per_day;
days / Decimal::from(self.config.trading_days_per_year)
}
fn calculate_information_ratio(&self, returns: &[Decimal]) -> Option<Decimal> {
let benchmark = self.config.benchmark_returns.as_ref()?;
if returns.len() != benchmark.len() || returns.is_empty() {
return None;
}
let excess: Vec<Decimal> = returns
.iter()
.zip(benchmark.iter())
.map(|(r, b)| *r - *b)
.collect();
let mean_excess = self.mean(&excess);
let tracking_error = self.std_dev(&excess);
if tracking_error == Decimal::ZERO {
return None;
}
let annualization = decimal_sqrt(Decimal::from(self.config.trading_days_per_year));
Some((mean_excess / tracking_error) * annualization)
}
fn calculate_trading_metrics(&self, trades: &[TradeRecord]) -> PerformanceMetrics {
let total_trades = trades.len() as u64;
if total_trades == 0 {
return PerformanceMetrics::default();
}
let winners: Vec<&TradeRecord> = trades.iter().filter(|t| t.is_winner()).collect();
let losers: Vec<&TradeRecord> = trades.iter().filter(|t| t.is_loser()).collect();
let winning_trades = winners.len() as u64;
let losing_trades = losers.len() as u64;
let win_rate = Decimal::from(winning_trades) / Decimal::from(total_trades);
let total_pnl: Decimal = trades.iter().map(|t| t.net_pnl()).sum();
let average_trade_pnl = total_pnl / Decimal::from(total_trades);
let average_winner = if !winners.is_empty() {
winners.iter().map(|t| t.net_pnl()).sum::<Decimal>() / Decimal::from(winners.len())
} else {
Decimal::ZERO
};
let average_loser = if !losers.is_empty() {
losers.iter().map(|t| t.net_pnl()).sum::<Decimal>() / Decimal::from(losers.len())
} else {
Decimal::ZERO
};
let largest_winner = winners
.iter()
.map(|t| t.net_pnl())
.max()
.unwrap_or(Decimal::ZERO);
let largest_loser = losers
.iter()
.map(|t| t.net_pnl())
.min()
.unwrap_or(Decimal::ZERO);
let total_duration: u64 = trades.iter().map(|t| t.duration_ms()).sum();
let avg_trade_duration_ms = total_duration / total_trades;
let profit_factor = self.profit_factor(trades);
PerformanceMetrics {
total_trades,
winning_trades,
losing_trades,
win_rate,
profit_factor,
average_trade_pnl,
average_winner,
average_loser,
largest_winner,
largest_loser,
avg_trade_duration_ms,
..Default::default()
}
}
fn mean(&self, values: &[Decimal]) -> Decimal {
if values.is_empty() {
return Decimal::ZERO;
}
let sum: Decimal = values.iter().sum();
sum / Decimal::from(values.len())
}
fn std_dev(&self, values: &[Decimal]) -> Decimal {
if values.len() < 2 {
return Decimal::ZERO;
}
let mean = self.mean(values);
let variance: Decimal = values
.iter()
.map(|v| {
let diff = *v - mean;
diff * diff
})
.sum::<Decimal>()
/ Decimal::from(values.len() - 1);
decimal_sqrt(variance)
}
fn downside_deviation(&self, returns: &[Decimal]) -> Decimal {
let negative_returns: Vec<Decimal> = returns
.iter()
.filter(|r| **r < Decimal::ZERO)
.copied()
.collect();
if negative_returns.len() < 2 {
return Decimal::ZERO;
}
let mean = self.mean(&negative_returns);
let variance: Decimal = negative_returns
.iter()
.map(|v| {
let diff = *v - mean;
diff * diff
})
.sum::<Decimal>()
/ Decimal::from(negative_returns.len() - 1);
decimal_sqrt(variance)
}
}
impl Default for MetricsCalculator {
fn default() -> Self {
Self::with_defaults()
}
}
fn decimal_sqrt(n: Decimal) -> Decimal {
if n <= Decimal::ZERO {
return Decimal::ZERO;
}
let mut x = n;
let two = Decimal::TWO;
for _ in 0..20 {
let next = (x + n / x) / two;
if (next - x).abs() < Decimal::from_str_exact("0.0000001").unwrap() {
return next;
}
x = next;
}
x
}
fn decimal_ln(n: Decimal) -> Decimal {
if n <= Decimal::ZERO {
return Decimal::ZERO;
}
let x = (n - Decimal::ONE) / (n + Decimal::ONE);
let x2 = x * x;
let mut result = Decimal::ZERO;
let mut term = x;
for k in 0..20 {
let divisor = Decimal::from(2 * k + 1);
result += term / divisor;
term *= x2;
}
result * Decimal::TWO
}
fn decimal_exp(x: Decimal) -> Decimal {
let mut result = Decimal::ONE;
let mut term = Decimal::ONE;
for n in 1..30 {
term *= x / Decimal::from(n);
result += term;
if term.abs() < Decimal::from_str_exact("0.0000001").unwrap() {
break;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dec;
fn create_equity_curve(values: &[(u64, Decimal)]) -> Vec<EquityPoint> {
values
.iter()
.map(|(ts, eq)| EquityPoint::new(*ts, *eq))
.collect()
}
#[test]
fn test_equity_point_new() {
let point = EquityPoint::new(1000, dec!(10000.0));
assert_eq!(point.timestamp, 1000);
assert_eq!(point.equity, dec!(10000.0));
}
#[test]
fn test_trade_record_new() {
let trade = TradeRecord::new(
1000,
2000,
Side::Buy,
dec!(100.0),
dec!(105.0),
dec!(10.0),
dec!(50.0),
dec!(1.0),
);
assert_eq!(trade.entry_time, 1000);
assert_eq!(trade.exit_time, 2000);
assert_eq!(trade.pnl, dec!(50.0));
assert_eq!(trade.fees, dec!(1.0));
}
#[test]
fn test_trade_record_net_pnl() {
let trade = TradeRecord::new(
0,
0,
Side::Buy,
dec!(100.0),
dec!(105.0),
dec!(10.0),
dec!(50.0),
dec!(2.0),
);
assert_eq!(trade.net_pnl(), dec!(48.0));
}
#[test]
fn test_trade_record_is_winner() {
let winner = TradeRecord::new(
0,
0,
Side::Buy,
dec!(100.0),
dec!(105.0),
dec!(10.0),
dec!(50.0),
dec!(1.0),
);
let loser = TradeRecord::new(
0,
0,
Side::Buy,
dec!(100.0),
dec!(95.0),
dec!(10.0),
dec!(-50.0),
dec!(1.0),
);
assert!(winner.is_winner());
assert!(!winner.is_loser());
assert!(!loser.is_winner());
assert!(loser.is_loser());
}
#[test]
fn test_trade_record_duration() {
let trade = TradeRecord::new(
1000,
5000,
Side::Buy,
dec!(100.0),
dec!(105.0),
dec!(10.0),
dec!(50.0),
dec!(1.0),
);
assert_eq!(trade.duration_ms(), 4000);
}
#[test]
fn test_metrics_config_default() {
let config = MetricsConfig::default();
assert_eq!(config.risk_free_rate, Decimal::ZERO);
assert_eq!(config.trading_days_per_year, 252);
assert!(config.benchmark_returns.is_none());
}
#[test]
fn test_metrics_config_builder() {
let config = MetricsConfig::default()
.with_risk_free_rate(dec!(0.05))
.with_trading_days(365)
.with_benchmark(vec![dec!(0.01), dec!(0.02)]);
assert_eq!(config.risk_free_rate, dec!(0.05));
assert_eq!(config.trading_days_per_year, 365);
assert!(config.benchmark_returns.is_some());
}
#[test]
fn test_calculate_returns() {
let calculator = MetricsCalculator::with_defaults();
let equity = create_equity_curve(&[
(0, dec!(10000.0)),
(1000, dec!(10100.0)),
(2000, dec!(10201.0)),
]);
let returns = calculator.calculate_returns(&equity);
assert_eq!(returns.len(), 2);
assert_eq!(returns[0], dec!(0.01)); assert_eq!(returns[1], dec!(0.01)); }
#[test]
fn test_calculate_returns_empty() {
let calculator = MetricsCalculator::with_defaults();
let returns = calculator.calculate_returns(&[]);
assert!(returns.is_empty());
}
#[test]
fn test_calculate_returns_single_point() {
let calculator = MetricsCalculator::with_defaults();
let equity = create_equity_curve(&[(0, dec!(10000.0))]);
let returns = calculator.calculate_returns(&equity);
assert!(returns.is_empty());
}
#[test]
fn test_max_drawdown() {
let calculator = MetricsCalculator::with_defaults();
let equity = create_equity_curve(&[
(0, dec!(10000.0)),
(1000, dec!(11000.0)), (2000, dec!(9900.0)), (3000, dec!(10500.0)), ]);
let (max_dd, _duration) = calculator.max_drawdown(&equity);
assert_eq!(max_dd, dec!(0.1));
}
#[test]
fn test_max_drawdown_empty() {
let calculator = MetricsCalculator::with_defaults();
let (max_dd, duration) = calculator.max_drawdown(&[]);
assert_eq!(max_dd, Decimal::ZERO);
assert_eq!(duration, 0);
}
#[test]
fn test_sharpe_ratio() {
let calculator = MetricsCalculator::with_defaults();
let returns = vec![dec!(0.01), dec!(0.02), dec!(0.015), dec!(0.012)];
let sharpe = calculator.sharpe_ratio(&returns);
assert!(sharpe > Decimal::ZERO);
}
#[test]
fn test_sharpe_ratio_empty() {
let calculator = MetricsCalculator::with_defaults();
let sharpe = calculator.sharpe_ratio(&[]);
assert_eq!(sharpe, Decimal::ZERO);
}
#[test]
fn test_sortino_ratio() {
let calculator = MetricsCalculator::with_defaults();
let returns = vec![dec!(0.02), dec!(-0.01), dec!(0.03), dec!(-0.005)];
let sortino = calculator.sortino_ratio(&returns);
assert!(sortino > Decimal::ZERO);
}
#[test]
fn test_var() {
let calculator = MetricsCalculator::with_defaults();
let returns = vec![
dec!(-0.05),
dec!(-0.03),
dec!(-0.01),
dec!(0.01),
dec!(0.02),
dec!(0.03),
dec!(0.04),
dec!(0.05),
];
let var_95 = calculator.var(&returns, dec!(0.95));
assert!(var_95 > Decimal::ZERO);
}
#[test]
fn test_profit_factor() {
let calculator = MetricsCalculator::with_defaults();
let trades = vec![
TradeRecord::new(
0,
0,
Side::Buy,
dec!(100.0),
dec!(110.0),
dec!(1.0),
dec!(100.0),
dec!(0.0),
),
TradeRecord::new(
0,
0,
Side::Buy,
dec!(100.0),
dec!(90.0),
dec!(1.0),
dec!(-50.0),
dec!(0.0),
),
];
let pf = calculator.profit_factor(&trades);
assert_eq!(pf, dec!(2.0));
}
#[test]
fn test_profit_factor_no_losses() {
let calculator = MetricsCalculator::with_defaults();
let trades = vec![TradeRecord::new(
0,
0,
Side::Buy,
dec!(100.0),
dec!(110.0),
dec!(1.0),
dec!(100.0),
dec!(0.0),
)];
let pf = calculator.profit_factor(&trades);
assert_eq!(pf, dec!(999));
}
#[test]
fn test_calculate_full_metrics() {
let calculator = MetricsCalculator::with_defaults();
let equity = create_equity_curve(&[
(0, dec!(10000.0)),
(86400000, dec!(10100.0)),
(172800000, dec!(10250.0)),
(259200000, dec!(10150.0)),
(345600000, dec!(10300.0)),
]);
let trades = vec![
TradeRecord::new(
0,
86400000,
Side::Buy,
dec!(100.0),
dec!(101.0),
dec!(10.0),
dec!(100.0),
dec!(1.0),
),
TradeRecord::new(
86400000,
172800000,
Side::Buy,
dec!(101.0),
dec!(102.5),
dec!(10.0),
dec!(150.0),
dec!(1.0),
),
TradeRecord::new(
172800000,
259200000,
Side::Sell,
dec!(102.5),
dec!(101.5),
dec!(10.0),
dec!(-100.0),
dec!(1.0),
),
TradeRecord::new(
259200000,
345600000,
Side::Buy,
dec!(101.5),
dec!(103.0),
dec!(10.0),
dec!(150.0),
dec!(1.0),
),
];
let metrics = calculator
.calculate(&equity, &trades, dec!(10000.0))
.unwrap();
assert_eq!(metrics.total_return, dec!(300.0));
assert_eq!(metrics.total_return_pct, dec!(3.0));
assert_eq!(metrics.total_trades, 4);
assert_eq!(metrics.winning_trades, 3);
assert_eq!(metrics.losing_trades, 1);
assert!(metrics.win_rate > dec!(0.7)); assert!(metrics.profit_factor > Decimal::ONE);
}
#[test]
fn test_calculate_empty_equity() {
let calculator = MetricsCalculator::with_defaults();
let result = calculator.calculate(&[], &[], dec!(10000.0));
assert!(result.is_err());
}
#[test]
fn test_calculate_no_trades() {
let calculator = MetricsCalculator::with_defaults();
let equity = create_equity_curve(&[
(0, dec!(10000.0)),
(86400000, dec!(10100.0)), ]);
let metrics = calculator.calculate(&equity, &[], dec!(10000.0)).unwrap();
assert_eq!(metrics.total_trades, 0);
assert_eq!(metrics.win_rate, Decimal::ZERO);
}
#[test]
fn test_performance_metrics_is_profitable() {
let metrics = PerformanceMetrics {
total_return: dec!(100.0),
..Default::default()
};
assert!(metrics.is_profitable());
let metrics_loss = PerformanceMetrics {
total_return: dec!(-100.0),
..Default::default()
};
assert!(!metrics_loss.is_profitable());
}
#[test]
fn test_performance_metrics_risk_reward() {
let metrics = PerformanceMetrics {
average_winner: dec!(100.0),
average_loser: dec!(-50.0),
..Default::default()
};
assert_eq!(metrics.risk_reward_ratio(), dec!(2.0));
}
#[test]
fn test_performance_metrics_expectancy() {
let metrics = PerformanceMetrics {
win_rate: dec!(0.5),
average_winner: dec!(100.0),
average_loser: dec!(-50.0),
..Default::default()
};
assert_eq!(metrics.expectancy(), dec!(25.0));
}
#[test]
fn test_decimal_sqrt() {
let result = decimal_sqrt(dec!(4.0));
assert!((result - dec!(2.0)).abs() < dec!(0.0001));
let result = decimal_sqrt(dec!(9.0));
assert!((result - dec!(3.0)).abs() < dec!(0.0001));
}
#[test]
fn test_decimal_sqrt_zero() {
assert_eq!(decimal_sqrt(Decimal::ZERO), Decimal::ZERO);
}
#[test]
fn test_decimal_ln() {
let e = dec!(2.718281828);
let result = decimal_ln(e);
assert!((result - Decimal::ONE).abs() < dec!(0.01));
}
#[test]
fn test_decimal_exp() {
assert_eq!(decimal_exp(Decimal::ZERO), Decimal::ONE);
let result = decimal_exp(Decimal::ONE);
assert!((result - dec!(2.718281828)).abs() < dec!(0.01));
}
#[cfg(feature = "serde")]
#[test]
fn test_equity_point_serialization() {
let point = EquityPoint::new(1000, dec!(10000.0));
let json = serde_json::to_string(&point).unwrap();
let deserialized: EquityPoint = serde_json::from_str(&json).unwrap();
assert_eq!(point, deserialized);
}
#[cfg(feature = "serde")]
#[test]
fn test_trade_record_serialization() {
let trade = TradeRecord::new(
1000,
2000,
Side::Buy,
dec!(100.0),
dec!(105.0),
dec!(10.0),
dec!(50.0),
dec!(1.0),
);
let json = serde_json::to_string(&trade).unwrap();
let deserialized: TradeRecord = serde_json::from_str(&json).unwrap();
assert_eq!(trade, deserialized);
}
}