use rust_decimal::Decimal;
use crate::MetricsError;
pub fn win_rate(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
if trade_pnls.is_empty() {
return Err(MetricsError::InsufficientData {
required: 1,
actual: 0,
});
}
let wins = trade_pnls
.iter()
.filter(|&&pnl| pnl > Decimal::ZERO)
.count();
let total = trade_pnls.len();
Ok(Decimal::from(wins as u64) / Decimal::from(total as u64) * Decimal::from(100))
}
pub fn profit_factor(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
if trade_pnls.is_empty() {
return Err(MetricsError::InsufficientData {
required: 1,
actual: 0,
});
}
let gross_profit: Decimal = trade_pnls.iter().filter(|&&pnl| pnl > Decimal::ZERO).sum();
let gross_loss: Decimal = trade_pnls
.iter()
.filter(|&&pnl| pnl < Decimal::ZERO)
.map(|pnl| pnl.abs())
.sum();
if gross_loss == Decimal::ZERO {
if gross_profit == Decimal::ZERO {
return Ok(Decimal::ONE); }
return Err(MetricsError::DivisionByZero {
context: "no losing trades",
});
}
Ok(gross_profit / gross_loss)
}
pub fn avg_win(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
let wins: Vec<Decimal> = trade_pnls
.iter()
.filter(|&&pnl| pnl > Decimal::ZERO)
.copied()
.collect();
if wins.is_empty() {
return Err(MetricsError::InsufficientData {
required: 1,
actual: 0,
});
}
let sum: Decimal = wins.iter().sum();
Ok(sum / Decimal::from(wins.len() as u64))
}
pub fn avg_loss(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
let losses: Vec<Decimal> = trade_pnls
.iter()
.filter(|&&pnl| pnl < Decimal::ZERO)
.copied()
.collect();
if losses.is_empty() {
return Err(MetricsError::InsufficientData {
required: 1,
actual: 0,
});
}
let sum: Decimal = losses.iter().sum();
Ok(sum / Decimal::from(losses.len() as u64))
}
pub fn expectancy(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
if trade_pnls.is_empty() {
return Err(MetricsError::InsufficientData {
required: 1,
actual: 0,
});
}
let wins: Vec<Decimal> = trade_pnls
.iter()
.filter(|&&pnl| pnl > Decimal::ZERO)
.copied()
.collect();
let losses: Vec<Decimal> = trade_pnls
.iter()
.filter(|&&pnl| pnl < Decimal::ZERO)
.copied()
.collect();
let total = trade_pnls.len() as u64;
let win_pct = Decimal::from(wins.len() as u64) / Decimal::from(total);
let loss_pct = Decimal::ONE - win_pct;
let avg_w = if wins.is_empty() {
Decimal::ZERO
} else {
wins.iter().sum::<Decimal>() / Decimal::from(wins.len() as u64)
};
let avg_l = if losses.is_empty() {
Decimal::ZERO
} else {
losses.iter().sum::<Decimal>() / Decimal::from(losses.len() as u64)
};
Ok((win_pct * avg_w) + (loss_pct * avg_l))
}
#[cfg(test)]
#[path = "trading_tests.rs"]
mod tests;