use crate::indicators::Indicator;
use crate::types::{Candle, DonchianOutput};
use crate::utils::ringbuf::RingBuf;
#[derive(Debug, Clone)]
pub struct DonchianChannels {
period: usize,
window: RingBuf<(f64, f64)>,
}
impl DonchianChannels {
pub fn new(period: usize) -> Self {
assert!(period > 0, "period must be > 0");
Self {
period,
window: RingBuf::new(period, (0.0, 0.0)),
}
}
}
impl Indicator for DonchianChannels {
type Output = DonchianOutput;
fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
self.window.push((candle.high, candle.low));
if self.window.len() < self.period {
return None;
}
let mut upper = f64::MIN;
let mut lower = f64::MAX;
for &(h, l) in self.window.iter() {
if h > upper {
upper = h;
}
if l < lower {
lower = l;
}
}
Some(DonchianOutput {
upper,
middle: (upper + lower) / 2.0,
lower,
})
}
fn reset(&mut self) {
self.window = RingBuf::new(self.period, (0.0, 0.0));
}
fn warmup_period(&self) -> usize {
self.period
}
fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn candle(high: f64, low: f64) -> Candle {
Candle {
timestamp: 0,
open: (high + low) / 2.0,
high,
low,
close: (high + low) / 2.0,
volume: 1_000.0,
}
}
#[test]
fn donchian_emits_after_warmup() {
let mut dc = DonchianChannels::new(5);
let candles: Vec<Candle> = (0..8)
.map(|i| candle(100.0 + i as f64, 99.0 + i as f64))
.collect();
let outputs: Vec<_> = candles.iter().map(|c| dc.next(c)).collect();
let wp = dc.warmup_period();
assert!(outputs.iter().take(wp - 1).all(|o| o.is_none()));
assert!(outputs[wp - 1].is_some());
}
#[test]
fn donchian_tracks_highest_high_lowest_low() {
let mut dc = DonchianChannels::new(3);
let candles: Vec<Candle> = [(10.0, 5.0), (12.0, 6.0), (8.0, 3.0), (9.0, 4.0), (7.0, 2.0)]
.iter()
.map(|&(h, l)| candle(h, l))
.collect();
let outputs: Vec<_> = candles.iter().map(|c| dc.next(c)).collect();
let out2 = outputs[2].unwrap();
assert!((out2.upper - 12.0).abs() < 1e-9);
assert!((out2.lower - 3.0).abs() < 1e-9);
assert!((out2.middle - 7.5).abs() < 1e-9);
let out3 = outputs[3].unwrap();
assert!((out3.upper - 12.0).abs() < 1e-9);
assert!((out3.lower - 3.0).abs() < 1e-9);
let out4 = outputs[4].unwrap();
assert!((out4.upper - 9.0).abs() < 1e-9);
assert!((out4.lower - 2.0).abs() < 1e-9);
}
#[test]
fn donchian_bands_bracket_middle() {
let mut dc = DonchianChannels::new(4);
let candles: Vec<Candle> = (0..8)
.map(|i| candle(100.0 + i as f64, 99.0 - i as f64))
.collect();
for c in &candles {
if let Some(out) = dc.next(c) {
assert!(out.upper >= out.middle);
assert!(out.middle >= out.lower);
assert!((out.middle - (out.upper + out.lower) / 2.0).abs() < 1e-9);
}
}
}
#[test]
fn donchian_flat_prices_zero_width_band() {
let mut dc = DonchianChannels::new(3);
let candles: Vec<Candle> = (0..5).map(|_| candle(50.0, 50.0)).collect();
for c in &candles {
if let Some(out) = dc.next(c) {
assert!((out.upper - 50.0).abs() < 1e-9);
assert!((out.lower - 50.0).abs() < 1e-9);
assert!((out.middle - 50.0).abs() < 1e-9);
}
}
}
#[test]
fn donchian_streaming_matches_batch() {
let candles: Vec<Candle> = (0..15)
.map(|i| candle(90.0 + i as f64 * 0.7, 89.0 + i as f64 * 0.7))
.collect();
let batch = DonchianChannels::new(5).calculate(&candles);
let mut streamed_dc = DonchianChannels::new(5);
let streamed: Vec<_> = candles.iter().map(|c| streamed_dc.next(c)).collect();
assert_eq!(streamed, batch);
}
#[test]
fn donchian_reset_clears_state() {
let mut dc = DonchianChannels::new(4);
let candles: Vec<Candle> = (0..6)
.map(|i| candle(100.0 + i as f64, 99.0 + i as f64))
.collect();
for c in &candles {
dc.next(c);
}
dc.reset();
let mut fresh = DonchianChannels::new(4);
for c in &candles {
assert_eq!(dc.next(c), fresh.next(c));
}
}
#[test]
#[should_panic(expected = "period must be > 0")]
fn donchian_rejects_zero_period() {
DonchianChannels::new(0);
}
}