wickra_core/indicators/
central_pivot_range.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct CentralPivotRangeOutput {
10 pub pivot: f64,
12 pub tc: f64,
14 pub bc: f64,
16}
17
18#[derive(Debug, Clone, Default)]
51pub struct CentralPivotRange {
52 ready: bool,
53}
54
55impl CentralPivotRange {
56 #[must_use]
58 pub const fn new() -> Self {
59 Self { ready: false }
60 }
61}
62
63impl Indicator for CentralPivotRange {
64 type Input = Candle;
65 type Output = CentralPivotRangeOutput;
66
67 #[inline]
68 fn update(&mut self, candle: Candle) -> Option<CentralPivotRangeOutput> {
69 let pivot = (candle.high + candle.low + candle.close) / 3.0;
70 let bc_raw = f64::midpoint(candle.high, candle.low);
71 let tc_raw = 2.0 * pivot - bc_raw;
72 let tc = tc_raw.max(bc_raw);
73 let bc = tc_raw.min(bc_raw);
74 self.ready = true;
75 Some(CentralPivotRangeOutput { pivot, tc, bc })
76 }
77
78 fn reset(&mut self) {
79 self.ready = false;
80 }
81
82 #[inline]
83 fn warmup_period(&self) -> usize {
84 1
85 }
86
87 #[inline]
88 fn is_ready(&self) -> bool {
89 self.ready
90 }
91
92 #[inline]
93 fn name(&self) -> &'static str {
94 "CentralPivotRange"
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::traits::BatchExt;
102
103 fn c(high: f64, low: f64, close: f64) -> Candle {
104 Candle::new_unchecked(close, high, low, close, 1_000.0, 0)
105 }
106
107 #[test]
108 fn accessors_and_metadata() {
109 let cpr = CentralPivotRange::new();
110 assert_eq!(cpr.warmup_period(), 1);
111 assert_eq!(cpr.name(), "CentralPivotRange");
112 assert!(!cpr.is_ready());
113 }
114
115 #[test]
116 fn formula_reference_values() {
117 let out = CentralPivotRange::new()
119 .update(c(110.0, 90.0, 105.0))
120 .unwrap();
121 let pivot = 305.0 / 3.0;
122 let bc_raw = 100.0;
123 let tc_raw = 2.0 * pivot - bc_raw;
124 assert!((out.pivot - pivot).abs() < 1e-12);
125 assert!((out.tc - tc_raw.max(bc_raw)).abs() < 1e-12);
126 assert!((out.bc - tc_raw.min(bc_raw)).abs() < 1e-12);
127 }
128
129 #[test]
130 fn tc_never_below_bc() {
131 let out = CentralPivotRange::new()
132 .update(c(200.0, 100.0, 150.0))
133 .unwrap();
134 assert!(out.tc >= out.bc);
135 }
136
137 #[test]
138 fn constant_bar_collapses_range() {
139 let out = CentralPivotRange::new()
141 .update(c(50.0, 50.0, 50.0))
142 .unwrap();
143 assert_eq!(out.pivot, 50.0);
144 assert_eq!(out.tc, 50.0);
145 assert_eq!(out.bc, 50.0);
146 }
147
148 #[test]
149 fn ready_after_first_update() {
150 let mut cpr = CentralPivotRange::new();
151 assert!(!cpr.is_ready());
152 cpr.update(c(11.0, 9.0, 10.0));
153 assert!(cpr.is_ready());
154 }
155
156 #[test]
157 fn reset_clears_state() {
158 let mut cpr = CentralPivotRange::new();
159 cpr.update(c(11.0, 9.0, 10.0));
160 assert!(cpr.is_ready());
161 cpr.reset();
162 assert!(!cpr.is_ready());
163 }
164
165 #[test]
166 fn batch_equals_streaming() {
167 let candles: Vec<Candle> = (0..40)
168 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
169 .collect();
170 let batch = CentralPivotRange::new().batch(&candles);
171 let mut b = CentralPivotRange::new();
172 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
173 assert_eq!(batch, streamed);
174 }
175}