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 fn update(&mut self, candle: Candle) -> Option<CentralPivotRangeOutput> {
68 let pivot = (candle.high + candle.low + candle.close) / 3.0;
69 let bc_raw = f64::midpoint(candle.high, candle.low);
70 let tc_raw = 2.0 * pivot - bc_raw;
71 let tc = tc_raw.max(bc_raw);
72 let bc = tc_raw.min(bc_raw);
73 self.ready = true;
74 Some(CentralPivotRangeOutput { pivot, tc, bc })
75 }
76
77 fn reset(&mut self) {
78 self.ready = false;
79 }
80
81 fn warmup_period(&self) -> usize {
82 1
83 }
84
85 fn is_ready(&self) -> bool {
86 self.ready
87 }
88
89 fn name(&self) -> &'static str {
90 "CentralPivotRange"
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::traits::BatchExt;
98
99 fn c(high: f64, low: f64, close: f64) -> Candle {
100 Candle::new_unchecked(close, high, low, close, 1_000.0, 0)
101 }
102
103 #[test]
104 fn accessors_and_metadata() {
105 let cpr = CentralPivotRange::new();
106 assert_eq!(cpr.warmup_period(), 1);
107 assert_eq!(cpr.name(), "CentralPivotRange");
108 assert!(!cpr.is_ready());
109 }
110
111 #[test]
112 fn formula_reference_values() {
113 let out = CentralPivotRange::new()
115 .update(c(110.0, 90.0, 105.0))
116 .unwrap();
117 let pivot = 305.0 / 3.0;
118 let bc_raw = 100.0;
119 let tc_raw = 2.0 * pivot - bc_raw;
120 assert!((out.pivot - pivot).abs() < 1e-12);
121 assert!((out.tc - tc_raw.max(bc_raw)).abs() < 1e-12);
122 assert!((out.bc - tc_raw.min(bc_raw)).abs() < 1e-12);
123 }
124
125 #[test]
126 fn tc_never_below_bc() {
127 let out = CentralPivotRange::new()
128 .update(c(200.0, 100.0, 150.0))
129 .unwrap();
130 assert!(out.tc >= out.bc);
131 }
132
133 #[test]
134 fn constant_bar_collapses_range() {
135 let out = CentralPivotRange::new()
137 .update(c(50.0, 50.0, 50.0))
138 .unwrap();
139 assert_eq!(out.pivot, 50.0);
140 assert_eq!(out.tc, 50.0);
141 assert_eq!(out.bc, 50.0);
142 }
143
144 #[test]
145 fn ready_after_first_update() {
146 let mut cpr = CentralPivotRange::new();
147 assert!(!cpr.is_ready());
148 cpr.update(c(11.0, 9.0, 10.0));
149 assert!(cpr.is_ready());
150 }
151
152 #[test]
153 fn reset_clears_state() {
154 let mut cpr = CentralPivotRange::new();
155 cpr.update(c(11.0, 9.0, 10.0));
156 assert!(cpr.is_ready());
157 cpr.reset();
158 assert!(!cpr.is_ready());
159 }
160
161 #[test]
162 fn batch_equals_streaming() {
163 let candles: Vec<Candle> = (0..40)
164 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
165 .collect();
166 let batch = CentralPivotRange::new().batch(&candles);
167 let mut b = CentralPivotRange::new();
168 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
169 assert_eq!(batch, streamed);
170 }
171}