use super::types::{BearIthResult, BullIthResult};
#[inline]
pub fn bull_ith(nav: &[f64], tmaeg: f64) -> BullIthResult {
if nav.is_empty() {
return BullIthResult {
excess_gains: vec![],
excess_losses: vec![],
num_of_epochs: 0,
epochs: vec![],
intervals_cv: f64::NAN,
max_drawdown: 0.0,
};
}
let n = nav.len();
let mut excess_gains = vec![0.0; n];
let mut excess_losses = vec![0.0; n];
let mut epochs = vec![false; n];
let mut excess_gain = 0.0;
let mut excess_loss = 0.0;
let mut endorsing_crest = nav[0]; let mut endorsing_nadir = nav[0]; let mut candidate_crest = nav[0]; let mut candidate_nadir = nav[0];
let mut running_max = nav[0];
let mut max_drawdown = 0.0;
for i in 1..n {
let equity = nav[i - 1];
let next_equity = nav[i];
if next_equity > running_max {
running_max = next_equity;
}
let current_drawdown = if running_max > 0.0 {
1.0 - next_equity / running_max
} else {
0.0
};
if current_drawdown > max_drawdown {
max_drawdown = current_drawdown;
}
if next_equity > candidate_crest {
if endorsing_crest != 0.0 && next_equity != 0.0 {
excess_gain = next_equity / endorsing_crest - 1.0;
} else {
excess_gain = 0.0;
}
candidate_crest = next_equity;
}
if next_equity < candidate_nadir {
if endorsing_crest != 0.0 {
excess_loss = 1.0 - next_equity / endorsing_crest;
} else {
excess_loss = 0.0;
}
candidate_nadir = next_equity;
}
let reset_condition = excess_gain > excess_loss.abs()
&& excess_gain > tmaeg
&& candidate_crest >= endorsing_crest;
if reset_condition {
endorsing_crest = candidate_crest;
endorsing_nadir = equity;
candidate_nadir = equity;
} else {
endorsing_nadir = endorsing_nadir.min(equity);
}
excess_gains[i] = excess_gain;
excess_losses[i] = excess_loss;
if reset_condition {
excess_gain = 0.0;
excess_loss = 0.0;
}
let bull_epoch_condition = excess_gains[i] > excess_losses[i] && excess_gains[i] > tmaeg;
epochs[i] = bull_epoch_condition;
}
let num_of_epochs = epochs.iter().filter(|&&e| e).count();
let intervals_cv = calculate_intervals_cv_numba_style(&epochs, num_of_epochs);
BullIthResult {
excess_gains,
excess_losses,
num_of_epochs,
epochs,
intervals_cv,
max_drawdown,
}
}
#[inline]
pub fn bear_ith(nav: &[f64], tmaeg: f64) -> BearIthResult {
if nav.is_empty() {
return BearIthResult {
excess_gains: vec![],
excess_losses: vec![],
num_of_epochs: 0,
epochs: vec![],
intervals_cv: f64::NAN,
max_runup: 0.0,
};
}
let n = nav.len();
let mut excess_gains = vec![0.0; n];
let mut excess_losses = vec![0.0; n];
let mut epochs = vec![false; n];
let mut excess_gain = 0.0;
let mut excess_loss = 0.0;
let mut endorsing_trough = nav[0]; let mut endorsing_peak = nav[0]; let mut candidate_trough = nav[0]; let mut candidate_peak = nav[0];
let mut running_min = nav[0];
let mut max_runup = 0.0;
for i in 1..n {
let equity = nav[i - 1];
let next_equity = nav[i];
if next_equity < running_min {
running_min = next_equity;
}
let current_runup = if next_equity > 0.0 {
1.0 - running_min / next_equity
} else {
0.0
};
if current_runup > max_runup {
max_runup = current_runup;
}
if next_equity < candidate_trough {
if endorsing_trough != 0.0 && next_equity != 0.0 {
excess_gain = endorsing_trough / next_equity - 1.0;
} else {
excess_gain = 0.0;
}
candidate_trough = next_equity;
}
if next_equity > candidate_peak {
if next_equity != 0.0 {
excess_loss = 1.0 - endorsing_trough / next_equity;
} else {
excess_loss = 0.0;
}
candidate_peak = next_equity;
}
let reset_condition = excess_gain > excess_loss.abs()
&& excess_gain > tmaeg
&& candidate_trough <= endorsing_trough;
if reset_condition {
endorsing_trough = candidate_trough;
endorsing_peak = equity;
candidate_peak = equity;
} else {
endorsing_peak = endorsing_peak.max(equity);
}
excess_gains[i] = excess_gain;
excess_losses[i] = excess_loss;
if reset_condition {
excess_gain = 0.0;
excess_loss = 0.0;
}
let bear_epoch_condition = excess_gains[i] > excess_losses[i] && excess_gains[i] > tmaeg;
epochs[i] = bear_epoch_condition;
}
let num_of_epochs = epochs.iter().filter(|&&e| e).count();
let intervals_cv = calculate_intervals_cv_numba_style(&epochs, num_of_epochs);
BearIthResult {
excess_gains,
excess_losses,
num_of_epochs,
epochs,
intervals_cv,
max_runup,
}
}
fn calculate_intervals_cv_numba_style(epochs: &[bool], num_of_epochs: usize) -> f64 {
if num_of_epochs == 0 {
return f64::NAN;
}
let mut prev_idx: usize = 0;
let mut sum = 0.0_f64;
let mut sum_sq = 0.0_f64;
let mut count: usize = 0;
for (i, &is_epoch) in epochs.iter().enumerate() {
if is_epoch {
let interval = (i - prev_idx) as f64;
sum += interval;
sum_sq += interval * interval;
count += 1;
prev_idx = i;
if count >= num_of_epochs {
break;
}
}
}
if count == 0 {
return f64::NAN;
}
let n = count as f64;
let mean = sum / n;
if mean <= 0.0 {
return f64::NAN;
}
let variance = (sum_sq / n - mean * mean).max(0.0);
let std_dev = variance.sqrt();
std_dev / mean
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bull_ith_empty() {
let result = bull_ith(&[], 0.05);
assert_eq!(result.num_of_epochs, 0);
assert!(result.intervals_cv.is_nan());
}
#[test]
fn test_bull_ith_no_epochs() {
let nav = vec![1.0, 0.99, 0.98, 0.97, 0.96];
let result = bull_ith(&nav, 0.05);
assert_eq!(result.num_of_epochs, 0);
}
#[test]
fn test_bull_ith_with_epochs() {
let nav = vec![1.0, 1.02, 1.04, 1.06, 1.08, 1.10];
let result = bull_ith(&nav, 0.05);
assert!(result.num_of_epochs > 0);
}
#[test]
fn test_bull_ith_max_drawdown() {
let nav = vec![1.0, 1.10, 1.05, 1.15, 1.00];
let result = bull_ith(&nav, 0.05);
assert!(result.max_drawdown > 0.10);
}
#[test]
fn test_bull_ith_state_machine() {
let nav = vec![1.0, 1.06, 1.03, 1.09];
let result = bull_ith(&nav, 0.05);
assert!(result.epochs[1]);
assert!(!result.epochs[3]);
}
#[test]
fn test_bear_ith_empty() {
let result = bear_ith(&[], 0.05);
assert_eq!(result.num_of_epochs, 0);
assert!(result.intervals_cv.is_nan());
}
#[test]
fn test_bear_ith_no_epochs() {
let nav = vec![1.0, 1.01, 1.02, 1.03, 1.04];
let result = bear_ith(&nav, 0.05);
assert_eq!(result.num_of_epochs, 0);
}
#[test]
fn test_bear_ith_with_epochs() {
let nav = vec![1.0, 0.98, 0.96, 0.94, 0.92, 0.90];
let result = bear_ith(&nav, 0.05);
assert!(result.num_of_epochs > 0);
}
#[test]
fn test_bear_ith_max_runup() {
let nav = vec![1.0, 0.90, 0.95, 0.85, 1.00];
let result = bear_ith(&nav, 0.05);
assert!(result.max_runup > 0.15);
}
#[test]
fn test_bear_ith_state_machine() {
let nav = vec![1.0, 0.94, 0.97, 0.91];
let result = bear_ith(&nav, 0.05);
assert!(result.epochs[1]);
}
#[test]
fn test_intervals_cv_numba_style() {
let epochs = vec![false, true, false, true];
let num_epochs = 2;
let cv = calculate_intervals_cv_numba_style(&epochs, num_epochs);
assert!((cv - 0.3333).abs() < 0.01);
}
#[test]
fn test_intervals_cv_no_epochs() {
let epochs = vec![false, false, false];
let cv = calculate_intervals_cv_numba_style(&epochs, 0);
assert!(cv.is_nan());
}
#[test]
fn test_intervals_cv_equal_spacing() {
let mut epochs = vec![false; 31];
epochs[10] = true;
epochs[20] = true;
epochs[30] = true;
let cv = calculate_intervals_cv_numba_style(&epochs, 3);
assert!((cv - 0.0).abs() < 0.001);
}
}