1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
45pub struct NakedPoc {
46 session_len: usize,
47 bins: usize,
48 session: VecDeque<Candle>,
49 naked: Vec<f64>,
50 last_close: f64,
51 ready: bool,
52 last: Option<f64>,
53}
54
55impl NakedPoc {
56 pub fn new(session_len: usize, bins: usize) -> Result<Self> {
63 if session_len == 0 || bins == 0 {
64 return Err(Error::PeriodZero);
65 }
66 Ok(Self {
67 session_len,
68 bins,
69 session: VecDeque::with_capacity(session_len),
70 naked: Vec::new(),
71 last_close: 0.0,
72 ready: false,
73 last: None,
74 })
75 }
76
77 pub const fn params(&self) -> (usize, usize) {
79 (self.session_len, self.bins)
80 }
81
82 pub fn naked_count(&self) -> usize {
84 self.naked.len()
85 }
86
87 pub const fn value(&self) -> Option<f64> {
89 self.last
90 }
91
92 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
93 fn session_poc(&self) -> f64 {
94 let mut low = f64::INFINITY;
95 let mut high = f64::NEG_INFINITY;
96 for c in &self.session {
97 low = low.min(c.low);
98 high = high.max(c.high);
99 }
100 let span = high - low;
101 if span <= 0.0 {
102 return low;
103 }
104 let width = span / self.bins as f64;
105 let mut hist = vec![0.0; self.bins];
106 for c in &self.session {
107 if c.volume == 0.0 {
108 continue;
109 }
110 let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1);
111 let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1);
112 let share = c.volume / (hi_idx - lo_idx + 1) as f64;
113 for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) {
114 *bin += share;
115 }
116 }
117 let mut poc = 0;
118 let mut poc_vol = f64::NEG_INFINITY;
119 for (idx, &vol) in hist.iter().enumerate() {
120 if vol > poc_vol {
121 poc_vol = vol;
122 poc = idx;
123 }
124 }
125 low + (poc as f64 + 0.5) * width
126 }
127}
128
129impl Indicator for NakedPoc {
130 type Input = Candle;
131 type Output = f64;
132
133 fn update(&mut self, candle: Candle) -> Option<f64> {
134 self.naked
136 .retain(|&poc| !(candle.low <= poc && poc <= candle.high));
137 self.last_close = candle.close;
138
139 self.session.push_back(candle);
141 if self.session.len() == self.session_len {
142 let poc = self.session_poc();
143 self.naked.push(poc);
144 self.session.clear();
145 self.ready = true;
146 }
147
148 if !self.ready {
149 return None;
150 }
151 let nearest = self
152 .naked
153 .iter()
154 .copied()
155 .min_by(|a, b| {
156 (a - self.last_close)
157 .abs()
158 .total_cmp(&(b - self.last_close).abs())
159 })
160 .unwrap_or(self.last_close);
161 self.last = Some(nearest);
162 Some(nearest)
163 }
164
165 fn reset(&mut self) {
166 self.session.clear();
167 self.naked.clear();
168 self.last_close = 0.0;
169 self.ready = false;
170 self.last = None;
171 }
172
173 fn warmup_period(&self) -> usize {
174 self.session_len
175 }
176
177 fn is_ready(&self) -> bool {
178 self.last.is_some()
179 }
180
181 fn name(&self) -> &'static str {
182 "NakedPoc"
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::traits::BatchExt;
190
191 fn c(high: f64, low: f64, close: f64, volume: f64) -> Candle {
192 Candle::new_unchecked(f64::midpoint(high, low), high, low, close, volume, 0)
193 }
194
195 #[test]
196 fn rejects_zero_params() {
197 assert!(matches!(NakedPoc::new(0, 24), Err(Error::PeriodZero)));
198 assert!(matches!(NakedPoc::new(20, 0), Err(Error::PeriodZero)));
199 }
200
201 #[test]
202 fn accessors_and_metadata() {
203 let n = NakedPoc::new(20, 24).unwrap();
204 assert_eq!(n.params(), (20, 24));
205 assert_eq!(n.naked_count(), 0);
206 assert_eq!(n.warmup_period(), 20);
207 assert_eq!(n.name(), "NakedPoc");
208 assert!(!n.is_ready());
209 assert_eq!(n.value(), None);
210 }
211
212 #[test]
213 fn first_emission_at_session_end() {
214 let mut n = NakedPoc::new(4, 8).unwrap();
215 let candles: Vec<Candle> = (0..6).map(|_| c(101.0, 99.0, 100.0, 1_000.0)).collect();
216 let out = n.batch(&candles);
217 for v in out.iter().take(3) {
218 assert!(v.is_none());
219 }
220 assert!(out[3].is_some());
221 }
222
223 #[test]
224 fn records_session_poc() {
225 let mut n = NakedPoc::new(4, 16).unwrap();
226 n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
228 assert_eq!(n.naked_count(), 1);
229 let poc = n.value().unwrap();
230 assert!(
231 (poc - 100.0).abs() < 2.0,
232 "POC should be near 100, got {poc}"
233 );
234 }
235
236 #[test]
237 fn revisit_marks_poc_tested() {
238 let mut n = NakedPoc::new(4, 16).unwrap();
239 n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
241 assert_eq!(n.naked_count(), 1);
242 n.update(c(121.0, 119.0, 120.0, 1_000.0));
244 assert_eq!(n.naked_count(), 1);
245 n.update(c(121.0, 95.0, 100.0, 1_000.0));
247 assert_eq!(n.naked_count(), 0);
248 }
249
250 #[test]
251 fn empty_naked_reports_close() {
252 let mut n = NakedPoc::new(4, 16).unwrap();
253 n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
254 let out = n.update(c(121.0, 95.0, 117.0, 1_000.0)).unwrap();
256 assert_eq!(n.naked_count(), 0);
257 assert!(
258 (out - 117.0).abs() < 1e-9,
259 "with no naked POC, output is the close"
260 );
261 }
262
263 #[test]
264 fn reset_clears_state() {
265 let mut n = NakedPoc::new(4, 8).unwrap();
266 n.batch(&[c(101.0, 99.0, 100.0, 1_000.0); 6]);
267 assert!(n.is_ready());
268 n.reset();
269 assert!(!n.is_ready());
270 assert_eq!(n.value(), None);
271 assert_eq!(n.naked_count(), 0);
272 }
273
274 #[test]
275 fn batch_equals_streaming() {
276 let candles: Vec<Candle> = (0..80)
277 .map(|i| {
278 let b = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
279 c(b + 1.0, b - 1.0, b, 1_000.0 + f64::from(i))
280 })
281 .collect();
282 let batch = NakedPoc::new(20, 24).unwrap().batch(&candles);
283 let mut b = NakedPoc::new(20, 24).unwrap();
284 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
285 assert_eq!(batch, streamed);
286 }
287
288 #[test]
289 fn flat_session_reports_price() {
290 let mut n = NakedPoc::new(2, 4).unwrap();
292 n.update(c(50.0, 50.0, 50.0, 10.0));
293 assert_eq!(n.update(c(50.0, 50.0, 50.0, 10.0)), Some(50.0));
294 }
295
296 #[test]
297 fn zero_volume_session_is_handled() {
298 let mut n = NakedPoc::new(2, 4).unwrap();
300 n.update(c(60.0, 40.0, 50.0, 0.0));
301 assert!(n.update(c(60.0, 40.0, 50.0, 0.0)).is_some());
302 }
303
304 #[test]
305 fn nearest_of_two_naked_pocs() {
306 let mut n = NakedPoc::new(2, 4).unwrap();
309 n.update(c(11.0, 9.0, 10.0, 100.0));
310 n.update(c(11.0, 9.0, 10.0, 100.0)); n.update(c(101.0, 99.0, 100.0, 100.0));
312 let v = n.update(c(101.0, 99.0, 100.0, 100.0)).unwrap(); assert!(
314 v > 50.0,
315 "nearest to close 100 should be the upper POC, got {v}"
316 );
317 }
318}