use crate::is_valid_price;
#[derive(Debug, Clone, Copy, Default)]
pub struct MaeMfeMetrics {
pub mae: f64,
pub gmfe: f64,
pub bmfe: f64,
pub mdd: f64,
pub pdays: u32,
pub ret: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct MaeMfeConfig {
pub window: usize,
pub window_step: usize,
}
impl Default for MaeMfeConfig {
fn default() -> Self {
Self {
window: 0,
window_step: 1,
}
}
}
pub fn calculate_mae_mfe(
close_prices: &[f64],
trade_prices: &[f64],
entry_index: usize,
exit_index: usize,
is_long: bool,
has_entry_transaction: bool,
has_exit_transaction: bool,
fee_ratio: f64,
tax_ratio: f64,
config: &MaeMfeConfig,
) -> Vec<MaeMfeMetrics> {
if entry_index >= close_prices.len() || exit_index >= close_prices.len() {
return vec![MaeMfeMetrics::default()];
}
let mut exit_max = exit_index;
if config.window > 0 && config.window + entry_index > exit_max {
exit_max = config.window + entry_index;
}
exit_max = exit_max.min(close_prices.len() - 1);
let capacity = exit_max - entry_index + 1;
let mut cummax = Vec::with_capacity(capacity);
let mut cummin = Vec::with_capacity(capacity);
let mut cummin_i = Vec::with_capacity(capacity); let mut mdd = Vec::with_capacity(capacity);
let mut profit_period = Vec::with_capacity(capacity);
let mut returns = Vec::with_capacity(capacity);
let entry_price = trade_prices[entry_index];
let entry_close = close_prices[entry_index];
if !is_valid_price(entry_price) || !is_valid_price(entry_close) {
return vec![MaeMfeMetrics::default()];
}
let mut price_ratio = if is_long {
entry_close / entry_price
} else {
2.0 - entry_close / entry_price
};
if has_entry_transaction {
price_ratio *= 1.0 - fee_ratio;
}
returns.push(price_ratio);
cummax.push(price_ratio.max(1.0));
cummin.push(price_ratio.min(1.0));
cummin_i.push(0);
mdd.push((price_ratio - 1.0).min(0.0));
profit_period.push(if price_ratio > 1.0 { 1 } else { 0 });
let mut pv = entry_close;
for (i, ith) in (entry_index + 1..=exit_max).enumerate() {
let p = close_prices[ith];
if is_valid_price(p) {
let v = p / pv;
pv = p;
if is_long {
price_ratio *= v;
} else {
price_ratio = 2.0 - (2.0 - price_ratio) * v;
}
}
let prev_idx = i; let cmax = cummax[prev_idx];
let cmin = cummin[prev_idx];
if price_ratio > cmax {
cummax.push(price_ratio);
} else {
cummax.push(cmax);
}
if price_ratio < cmin {
cummin.push(price_ratio);
cummin_i.push(i + 1);
} else {
cummin.push(cmin);
cummin_i.push(cummin_i[prev_idx]);
}
let new_mdd = price_ratio / cummax[i + 1] - 1.0;
if new_mdd < mdd[prev_idx] {
mdd.push(new_mdd);
} else {
mdd.push(mdd[prev_idx]);
}
profit_period.push(profit_period[prev_idx] + if price_ratio > 1.0 { 1 } else { 0 });
returns.push(price_ratio);
}
if has_exit_transaction && entry_index != exit_max {
let last_idx = returns.len() - 1;
let exit_trade_price = trade_prices[exit_max];
let exit_close = close_prices[exit_max];
if is_valid_price(exit_trade_price) && is_valid_price(exit_close) {
if is_long {
returns[last_idx] *= exit_trade_price / exit_close;
} else {
returns[last_idx] = 2.0 - (2.0 - returns[last_idx]) * exit_trade_price / exit_close;
}
}
returns[last_idx] *= 1.0 - fee_ratio - tax_ratio;
}
let mut result = Vec::new();
if config.window > 0 {
let window = config.window.min(cummax.len());
for w in (0..window).step_by(config.window_step) {
if w < cummax.len() {
let mae_i = cummin_i[w];
result.push(MaeMfeMetrics {
mae: cummin[w] - 1.0,
gmfe: cummax[w] - 1.0,
bmfe: cummax[mae_i] - 1.0,
mdd: mdd[w],
pdays: profit_period[w],
ret: returns[w] - 1.0,
});
}
}
}
let exit_w = (exit_index - entry_index).min(cummax.len() - 1);
let mae_i = cummin_i[exit_w];
result.push(MaeMfeMetrics {
mae: cummin[exit_w] - 1.0,
gmfe: cummax[exit_w] - 1.0,
bmfe: cummax[mae_i] - 1.0,
mdd: mdd[exit_w],
pdays: profit_period[exit_w],
ret: returns[exit_w] - 1.0,
});
result
}
pub fn calculate_mae_mfe_at_exit(
close_prices: &[f64],
trade_prices: &[f64],
entry_index: usize,
exit_index: usize,
is_long: bool,
has_entry_transaction: bool,
has_exit_transaction: bool,
fee_ratio: f64,
tax_ratio: f64,
) -> MaeMfeMetrics {
let config = MaeMfeConfig::default();
let metrics = calculate_mae_mfe(
close_prices,
trade_prices,
entry_index,
exit_index,
is_long,
has_entry_transaction,
has_exit_transaction,
fee_ratio,
tax_ratio,
&config,
);
metrics.into_iter().last().unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_long_uptrend() {
let close = vec![100.0, 105.0, 110.0, 108.0, 115.0];
let trade = close.clone();
let metrics = calculate_mae_mfe_at_exit(
&close,
&trade,
0, 4, true, false, false, 0.0,
0.0,
);
assert!((metrics.ret - 0.15).abs() < 1e-10);
assert!((metrics.gmfe - 0.15).abs() < 1e-10);
assert!((metrics.mae - 0.0).abs() < 1e-10);
}
#[test]
fn test_long_with_drawdown() {
let close = vec![100.0, 120.0, 110.0, 90.0, 105.0];
let trade = close.clone();
let metrics = calculate_mae_mfe_at_exit(
&close,
&trade,
0,
4,
true,
false,
false,
0.0,
0.0,
);
assert!((metrics.ret - 0.05).abs() < 1e-10);
assert!((metrics.gmfe - 0.20).abs() < 1e-10);
assert!((metrics.mae - (-0.10)).abs() < 1e-10);
assert!((metrics.mdd - (-0.25)).abs() < 1e-10);
}
#[test]
fn test_short_position() {
let close = vec![100.0, 95.0, 90.0, 92.0, 85.0];
let trade = close.clone();
let metrics = calculate_mae_mfe_at_exit(
&close,
&trade,
0,
4,
false, false,
false,
0.0,
0.0,
);
assert!((metrics.ret - 0.15).abs() < 1e-10);
}
#[test]
fn test_with_fees() {
let close = vec![100.0, 110.0];
let trade = close.clone();
let fee_ratio = 0.001425;
let tax_ratio = 0.003;
let metrics = calculate_mae_mfe_at_exit(
&close,
&trade,
0,
1,
true,
true, true, fee_ratio,
tax_ratio,
);
let expected = (1.0 - fee_ratio) * 1.1 * (1.0 - fee_ratio - tax_ratio) - 1.0;
assert!((metrics.ret - expected).abs() < 1e-6);
}
#[test]
fn test_window_metrics() {
let close: Vec<f64> = (0..10).map(|i| 100.0 + i as f64 * 2.0).collect();
let trade = close.clone();
let config = MaeMfeConfig {
window: 10,
window_step: 2,
};
let metrics = calculate_mae_mfe(
&close,
&trade,
0,
9,
true,
false,
false,
0.0,
0.0,
&config,
);
assert_eq!(metrics.len(), 6);
assert!((metrics[0].ret - 0.0).abs() < 1e-10);
assert!((metrics[5].ret - 0.18).abs() < 1e-10);
}
#[test]
fn test_pdays_counting() {
let close = vec![100.0, 101.0, 99.0, 102.0, 98.0, 103.0];
let trade = close.clone();
let metrics = calculate_mae_mfe_at_exit(&close, &trade, 0, 5, true, false, false, 0.0, 0.0);
assert_eq!(metrics.pdays, 3);
}
#[test]
fn test_bmfe_calculation() {
let close = vec![100.0, 110.0, 120.0, 80.0, 90.0];
let trade = close.clone();
let metrics = calculate_mae_mfe_at_exit(&close, &trade, 0, 4, true, false, false, 0.0, 0.0);
assert!((metrics.bmfe - 0.2).abs() < 1e-10);
assert!((metrics.mae - (-0.2)).abs() < 1e-10);
}
}