#![allow(clippy::doc_markdown)]
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TdRangeProjectionOutput {
pub high: f64,
pub low: f64,
}
#[derive(Debug, Clone, Default)]
pub struct TdRangeProjection {
last_value: Option<TdRangeProjectionOutput>,
}
impl TdRangeProjection {
pub fn new() -> Self {
Self::default()
}
pub const fn value(&self) -> Option<TdRangeProjectionOutput> {
self.last_value
}
}
impl Indicator for TdRangeProjection {
type Input = Candle;
type Output = TdRangeProjectionOutput;
fn update(&mut self, candle: Candle) -> Option<TdRangeProjectionOutput> {
let pivot_sum = if candle.close < candle.open {
candle.high + 2.0 * candle.low + candle.close
} else if candle.close > candle.open {
2.0 * candle.high + candle.low + candle.close
} else {
candle.high + candle.low + 2.0 * candle.close
};
let half = pivot_sum / 2.0;
let out = TdRangeProjectionOutput {
high: half - candle.low,
low: half - candle.high,
};
self.last_value = Some(out);
Some(out)
}
fn reset(&mut self) {
self.last_value = None;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDRangeProjection"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new_unchecked(open, high, low, close, 0.0, ts)
}
#[test]
fn bullish_bar_close_above_open_uses_double_high_pivot() {
let mut p = TdRangeProjection::new();
let v = p.update(c(10.0, 12.0, 9.0, 11.0, 0)).unwrap();
assert_relative_eq!(v.high, 13.0, epsilon = 1e-12);
assert_relative_eq!(v.low, 10.0, epsilon = 1e-12);
}
#[test]
fn bearish_bar_close_below_open_uses_double_low_pivot() {
let mut p = TdRangeProjection::new();
let v = p.update(c(11.0, 12.0, 9.0, 10.0, 0)).unwrap();
assert_relative_eq!(v.high, 11.0, epsilon = 1e-12);
assert_relative_eq!(v.low, 8.0, epsilon = 1e-12);
}
#[test]
fn doji_close_equals_open_uses_double_close_pivot() {
let mut p = TdRangeProjection::new();
let v = p.update(c(10.0, 12.0, 9.0, 10.0, 0)).unwrap();
assert_relative_eq!(v.high, 11.5, epsilon = 1e-12);
assert_relative_eq!(v.low, 8.5, epsilon = 1e-12);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..30)
.map(|i| {
let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
c(m, m + 1.0, m - 1.0, m + 0.3, i64::from(i))
})
.collect();
let mut a = TdRangeProjection::new();
let mut b = TdRangeProjection::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut p = TdRangeProjection::new();
p.update(c(10.0, 12.0, 9.0, 11.0, 0));
assert!(p.is_ready());
p.reset();
assert!(!p.is_ready());
assert_eq!(p.value(), None);
}
#[test]
fn accessors_and_metadata() {
let p = TdRangeProjection::new();
assert_eq!(p.warmup_period(), 1);
assert_eq!(p.name(), "TDRangeProjection");
assert_eq!(p.value(), None);
}
}