#[derive(Debug, Clone, Default)]
pub struct IntraStreamAccumulator {
inv_first_price: Option<f64>,
running_max: f64,
running_min: f64,
max_drawdown: f64,
max_runup: f64,
trade_count: usize,
}
impl IntraStreamAccumulator {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn update(&mut self, price: f64) {
if !price.is_finite() || price <= 0.0 {
return;
}
match self.inv_first_price {
None => {
let inv = 1.0 / price;
self.inv_first_price = Some(inv);
let first_nav = price * inv;
self.running_max = first_nav;
self.running_min = first_nav;
self.trade_count = 1;
}
Some(inv) => {
let nav = price * inv;
if nav > self.running_max {
self.running_max = nav;
}
if nav < self.running_min {
self.running_min = nav;
}
if self.running_max > 0.0 {
let dd = 1.0 - nav / self.running_max;
if dd > self.max_drawdown {
self.max_drawdown = dd;
}
}
if nav > 0.0 {
let ru = 1.0 - self.running_min / nav;
if ru > self.max_runup {
self.max_runup = ru;
}
}
self.trade_count += 1;
}
}
}
#[inline]
pub fn finalize(&self) -> Option<(f64, f64)> {
if self.trade_count >= 2 {
Some((self.max_drawdown, self.max_runup))
} else {
None
}
}
#[inline]
pub fn trade_count(&self) -> usize {
self.trade_count
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::intrabar::ith::{bear_ith, bull_ith};
fn reference_dd_ru(prices: &[f64]) -> (f64, f64) {
let inv = 1.0 / prices[0];
let nav: Vec<f64> = prices.iter().map(|p| p * inv).collect();
let bull = bull_ith(&nav, 0.0);
let bear = bear_ith(&nav, 0.0);
(bull.max_drawdown, bear.max_runup)
}
#[test]
fn matches_bull_ith_drawdown_simple() {
let prices = vec![100.0, 110.0, 95.0, 105.0, 90.0];
let (ref_dd, ref_ru) = reference_dd_ru(&prices);
let mut acc = IntraStreamAccumulator::new();
for p in &prices {
acc.update(*p);
}
let (dd, ru) = acc.finalize().expect("≥2 trades");
assert_eq!(dd.to_bits(), ref_dd.to_bits(), "drawdown bit-exact");
assert_eq!(ru.to_bits(), ref_ru.to_bits(), "runup bit-exact");
}
#[test]
fn matches_bull_ith_drawdown_uptrend() {
let prices = vec![100.0, 101.0, 102.0, 103.0, 104.0, 105.0];
let (ref_dd, ref_ru) = reference_dd_ru(&prices);
let mut acc = IntraStreamAccumulator::new();
for p in &prices {
acc.update(*p);
}
let (dd, ru) = acc.finalize().expect("≥2 trades");
assert_eq!(dd.to_bits(), ref_dd.to_bits());
assert_eq!(ru.to_bits(), ref_ru.to_bits());
}
#[test]
fn matches_bull_ith_large_window() {
let mut prices = Vec::with_capacity(150_000);
let mut p = 100.0;
for i in 0..150_000_u32 {
p += if i.is_multiple_of(7) { -0.13 } else { 0.05 };
prices.push(p);
}
let (ref_dd, ref_ru) = reference_dd_ru(&prices);
let mut acc = IntraStreamAccumulator::new();
for p in &prices {
acc.update(*p);
}
let (dd, ru) = acc.finalize().expect("≥2 trades");
assert_eq!(
dd.to_bits(),
ref_dd.to_bits(),
"drawdown bit-exact at 150K trades"
);
assert_eq!(
ru.to_bits(),
ref_ru.to_bits(),
"runup bit-exact at 150K trades"
);
}
#[test]
fn empty_returns_none() {
let acc = IntraStreamAccumulator::new();
assert!(acc.finalize().is_none());
}
#[test]
fn single_trade_returns_none() {
let mut acc = IntraStreamAccumulator::new();
acc.update(100.0);
assert!(acc.finalize().is_none());
}
#[test]
fn ignores_invalid_prices() {
let mut acc = IntraStreamAccumulator::new();
acc.update(f64::NAN);
acc.update(0.0);
acc.update(-1.0);
acc.update(f64::INFINITY);
assert!(acc.finalize().is_none());
acc.update(100.0);
acc.update(99.0);
let (dd, _) = acc.finalize().expect("≥2 valid trades");
assert!(dd > 0.0);
}
}