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 #[inline]
60 fn update(&mut self, candle: Candle) -> Option<TdRangeProjectionOutput> {
61 let pivot_sum = if candle.close < candle.open {
62 candle.high + 2.0 * candle.low + candle.close
63 } else if candle.close > candle.open {
64 2.0 * candle.high + candle.low + candle.close
65 } else {
66 candle.high + candle.low + 2.0 * candle.close
67 };
68 let half = pivot_sum / 2.0;
69 let out = TdRangeProjectionOutput {
70 high: half - candle.low,
71 low: half - candle.high,
72 };
73 self.last_value = Some(out);
74 Some(out)
75 }
76
77 fn reset(&mut self) {
78 self.last_value = None;
79 }
80
81 #[inline]
82 fn warmup_period(&self) -> usize {
83 1
84 }
85
86 #[inline]
87 fn is_ready(&self) -> bool {
88 self.last_value.is_some()
89 }
90
91 #[inline]
92 fn name(&self) -> &'static str {
93 "TDRangeProjection"
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::traits::BatchExt;
101 use approx::assert_relative_eq;
102
103 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
104 Candle::new_unchecked(open, high, low, close, 0.0, ts)
105 }
106
107 #[test]
108 fn bullish_bar_close_above_open_uses_double_high_pivot() {
109 let mut p = TdRangeProjection::new();
113 let v = p.update(c(10.0, 12.0, 9.0, 11.0, 0)).unwrap();
114 assert_relative_eq!(v.high, 13.0, epsilon = 1e-12);
115 assert_relative_eq!(v.low, 10.0, epsilon = 1e-12);
116 }
117
118 #[test]
119 fn bearish_bar_close_below_open_uses_double_low_pivot() {
120 let mut p = TdRangeProjection::new();
124 let v = p.update(c(11.0, 12.0, 9.0, 10.0, 0)).unwrap();
125 assert_relative_eq!(v.high, 11.0, epsilon = 1e-12);
126 assert_relative_eq!(v.low, 8.0, epsilon = 1e-12);
127 }
128
129 #[test]
130 fn doji_close_equals_open_uses_double_close_pivot() {
131 let mut p = TdRangeProjection::new();
135 let v = p.update(c(10.0, 12.0, 9.0, 10.0, 0)).unwrap();
136 assert_relative_eq!(v.high, 11.5, epsilon = 1e-12);
137 assert_relative_eq!(v.low, 8.5, epsilon = 1e-12);
138 }
139
140 #[test]
141 fn batch_equals_streaming() {
142 let candles: Vec<Candle> = (0..30)
143 .map(|i| {
144 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
145 c(m, m + 1.0, m - 1.0, m + 0.3, i64::from(i))
146 })
147 .collect();
148 let mut a = TdRangeProjection::new();
149 let mut b = TdRangeProjection::new();
150 assert_eq!(
151 a.batch(&candles),
152 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
153 );
154 }
155
156 #[test]
157 fn reset_clears_state() {
158 let mut p = TdRangeProjection::new();
159 p.update(c(10.0, 12.0, 9.0, 11.0, 0));
160 assert!(p.is_ready());
161 p.reset();
162 assert!(!p.is_ready());
163 assert_eq!(p.value(), None);
164 }
165
166 #[test]
167 fn accessors_and_metadata() {
168 let p = TdRangeProjection::new();
169 assert_eq!(p.warmup_period(), 1);
170 assert_eq!(p.name(), "TDRangeProjection");
171 assert_eq!(p.value(), None);
172 }
173}