wickra_core/indicators/
td_range_projection.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
25use crate::traits::Indicator;
26
27#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct TdRangeProjectionOutput {
31 pub high: f64,
33 pub low: f64,
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct TdRangeProjection {
40 last_value: Option<TdRangeProjectionOutput>,
41}
42
43impl TdRangeProjection {
44 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub const fn value(&self) -> Option<TdRangeProjectionOutput> {
51 self.last_value
52 }
53}
54
55impl Indicator for TdRangeProjection {
56 type Input = Candle;
57 type Output = TdRangeProjectionOutput;
58
59 fn update(&mut self, candle: Candle) -> Option<TdRangeProjectionOutput> {
60 let pivot_sum = if candle.close < candle.open {
61 candle.high + 2.0 * candle.low + candle.close
62 } else if candle.close > candle.open {
63 2.0 * candle.high + candle.low + candle.close
64 } else {
65 candle.high + candle.low + 2.0 * candle.close
66 };
67 let half = pivot_sum / 2.0;
68 let out = TdRangeProjectionOutput {
69 high: half - candle.low,
70 low: half - candle.high,
71 };
72 self.last_value = Some(out);
73 Some(out)
74 }
75
76 fn reset(&mut self) {
77 self.last_value = None;
78 }
79
80 fn warmup_period(&self) -> usize {
81 1
82 }
83
84 fn is_ready(&self) -> bool {
85 self.last_value.is_some()
86 }
87
88 fn name(&self) -> &'static str {
89 "TDRangeProjection"
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::traits::BatchExt;
97 use approx::assert_relative_eq;
98
99 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
100 Candle::new_unchecked(open, high, low, close, 0.0, ts)
101 }
102
103 #[test]
104 fn bullish_bar_close_above_open_uses_double_high_pivot() {
105 let mut p = TdRangeProjection::new();
109 let v = p.update(c(10.0, 12.0, 9.0, 11.0, 0)).unwrap();
110 assert_relative_eq!(v.high, 13.0, epsilon = 1e-12);
111 assert_relative_eq!(v.low, 10.0, epsilon = 1e-12);
112 }
113
114 #[test]
115 fn bearish_bar_close_below_open_uses_double_low_pivot() {
116 let mut p = TdRangeProjection::new();
120 let v = p.update(c(11.0, 12.0, 9.0, 10.0, 0)).unwrap();
121 assert_relative_eq!(v.high, 11.0, epsilon = 1e-12);
122 assert_relative_eq!(v.low, 8.0, epsilon = 1e-12);
123 }
124
125 #[test]
126 fn doji_close_equals_open_uses_double_close_pivot() {
127 let mut p = TdRangeProjection::new();
131 let v = p.update(c(10.0, 12.0, 9.0, 10.0, 0)).unwrap();
132 assert_relative_eq!(v.high, 11.5, epsilon = 1e-12);
133 assert_relative_eq!(v.low, 8.5, epsilon = 1e-12);
134 }
135
136 #[test]
137 fn batch_equals_streaming() {
138 let candles: Vec<Candle> = (0..30)
139 .map(|i| {
140 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
141 c(m, m + 1.0, m - 1.0, m + 0.3, i64::from(i))
142 })
143 .collect();
144 let mut a = TdRangeProjection::new();
145 let mut b = TdRangeProjection::new();
146 assert_eq!(
147 a.batch(&candles),
148 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
149 );
150 }
151
152 #[test]
153 fn reset_clears_state() {
154 let mut p = TdRangeProjection::new();
155 p.update(c(10.0, 12.0, 9.0, 11.0, 0));
156 assert!(p.is_ready());
157 p.reset();
158 assert!(!p.is_ready());
159 assert_eq!(p.value(), None);
160 }
161
162 #[test]
163 fn accessors_and_metadata() {
164 let p = TdRangeProjection::new();
165 assert_eq!(p.warmup_period(), 1);
166 assert_eq!(p.name(), "TDRangeProjection");
167 assert_eq!(p.value(), None);
168 }
169}