use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DonchianStopOutput {
pub stop_long: f64,
pub stop_short: f64,
}
#[derive(Debug, Clone)]
pub struct DonchianStop {
period: usize,
highs: VecDeque<f64>,
lows: VecDeque<f64>,
}
impl DonchianStop {
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
highs: VecDeque::with_capacity(period),
lows: VecDeque::with_capacity(period),
})
}
pub fn classic() -> Self {
Self::new(10).expect("classic Donchian Stop period is valid")
}
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for DonchianStop {
type Input = Candle;
type Output = DonchianStopOutput;
fn update(&mut self, candle: Candle) -> Option<DonchianStopOutput> {
if self.highs.len() == self.period {
self.highs.pop_front();
self.lows.pop_front();
}
self.highs.push_back(candle.high);
self.lows.push_back(candle.low);
if self.highs.len() < self.period {
return None;
}
let stop_short = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let stop_long = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
Some(DonchianStopOutput {
stop_long,
stop_short,
})
}
fn reset(&mut self) {
self.highs.clear();
self.lows.clear();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.highs.len() == self.period
}
fn name(&self) -> &'static str {
"DonchianStop"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
}
#[test]
fn rejects_zero_period() {
assert!(DonchianStop::new(0).is_err());
}
#[test]
fn accessors_and_metadata() {
let s = DonchianStop::classic();
assert_eq!(s.period(), 10);
assert_eq!(s.warmup_period(), 10);
assert_eq!(s.name(), "DonchianStop");
}
#[test]
fn first_emission_matches_warmup() {
let candles: Vec<Candle> = (0..10)
.map(|i| {
let base = 100.0 + i as f64;
c(base + 1.0, base - 1.0, base, i)
})
.collect();
let mut s = DonchianStop::new(5).unwrap();
let out = s.batch(&candles);
for (i, v) in out.iter().enumerate().take(4) {
assert!(v.is_none(), "index {i} must be None during warmup");
}
assert!(out[4].is_some());
}
#[test]
fn reference_values_uptrend_window() {
let candles: Vec<Candle> = (0..5)
.map(|i| {
let base = i as f64 + 0.5;
c(base + 0.5, base - 0.5, base, i)
})
.collect();
let mut s = DonchianStop::new(5).unwrap();
let out = s.batch(&candles);
let v = out[4].expect("ready at index 4");
assert_relative_eq!(v.stop_short, 5.0, epsilon = 1e-12);
assert_relative_eq!(v.stop_long, 0.0, epsilon = 1e-12);
}
#[test]
fn constant_series_holds_both_stops() {
let candles: Vec<Candle> = (0..30).map(|i| c(11.0, 9.0, 10.0, i)).collect();
let mut s = DonchianStop::new(5).unwrap();
for v in s.batch(&candles).into_iter().flatten() {
assert_relative_eq!(v.stop_short, 11.0, epsilon = 1e-12);
assert_relative_eq!(v.stop_long, 9.0, epsilon = 1e-12);
}
}
#[test]
fn reset_clears_state() {
let candles: Vec<Candle> = (0..30)
.map(|i| {
let base = 100.0 + i as f64;
c(base + 1.0, base - 1.0, base, i)
})
.collect();
let mut s = DonchianStop::classic();
s.batch(&candles);
assert!(s.is_ready());
s.reset();
assert!(!s.is_ready());
assert_eq!(s.update(candles[0]), None);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80)
.map(|i| {
let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
c(mid + 1.5, mid - 1.5, mid + 0.5, i)
})
.collect();
let mut a = DonchianStop::classic();
let mut b = DonchianStop::classic();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}