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 #[inline]
134 fn update(&mut self, candle: Candle) -> Option<f64> {
135 self.naked
137 .retain(|&poc| !(candle.low <= poc && poc <= candle.high));
138 self.last_close = candle.close;
139
140 self.session.push_back(candle);
142 if self.session.len() == self.session_len {
143 let poc = self.session_poc();
144 self.naked.push(poc);
145 self.session.clear();
146 self.ready = true;
147 }
148
149 if !self.ready {
150 return None;
151 }
152 let nearest = self
153 .naked
154 .iter()
155 .copied()
156 .min_by(|a, b| {
157 (a - self.last_close)
158 .abs()
159 .total_cmp(&(b - self.last_close).abs())
160 })
161 .unwrap_or(self.last_close);
162 self.last = Some(nearest);
163 Some(nearest)
164 }
165
166 fn reset(&mut self) {
167 self.session.clear();
168 self.naked.clear();
169 self.last_close = 0.0;
170 self.ready = false;
171 self.last = None;
172 }
173
174 #[inline]
175 fn warmup_period(&self) -> usize {
176 self.session_len
177 }
178
179 #[inline]
180 fn is_ready(&self) -> bool {
181 self.last.is_some()
182 }
183
184 #[inline]
185 fn name(&self) -> &'static str {
186 "NakedPoc"
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::traits::BatchExt;
194
195 fn c(high: f64, low: f64, close: f64, volume: f64) -> Candle {
196 Candle::new_unchecked(f64::midpoint(high, low), high, low, close, volume, 0)
197 }
198
199 #[test]
200 fn rejects_zero_params() {
201 assert!(matches!(NakedPoc::new(0, 24), Err(Error::PeriodZero)));
202 assert!(matches!(NakedPoc::new(20, 0), Err(Error::PeriodZero)));
203 }
204
205 #[test]
206 fn accessors_and_metadata() {
207 let n = NakedPoc::new(20, 24).unwrap();
208 assert_eq!(n.params(), (20, 24));
209 assert_eq!(n.naked_count(), 0);
210 assert_eq!(n.warmup_period(), 20);
211 assert_eq!(n.name(), "NakedPoc");
212 assert!(!n.is_ready());
213 assert_eq!(n.value(), None);
214 }
215
216 #[test]
217 fn first_emission_at_session_end() {
218 let mut n = NakedPoc::new(4, 8).unwrap();
219 let candles: Vec<Candle> = (0..6).map(|_| c(101.0, 99.0, 100.0, 1_000.0)).collect();
220 let out = n.batch(&candles);
221 for v in out.iter().take(3) {
222 assert!(v.is_none());
223 }
224 assert!(out[3].is_some());
225 }
226
227 #[test]
228 fn records_session_poc() {
229 let mut n = NakedPoc::new(4, 16).unwrap();
230 n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
232 assert_eq!(n.naked_count(), 1);
233 let poc = n.value().unwrap();
234 assert!(
235 (poc - 100.0).abs() < 2.0,
236 "POC should be near 100, got {poc}"
237 );
238 }
239
240 #[test]
241 fn revisit_marks_poc_tested() {
242 let mut n = NakedPoc::new(4, 16).unwrap();
243 n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
245 assert_eq!(n.naked_count(), 1);
246 n.update(c(121.0, 119.0, 120.0, 1_000.0));
248 assert_eq!(n.naked_count(), 1);
249 n.update(c(121.0, 95.0, 100.0, 1_000.0));
251 assert_eq!(n.naked_count(), 0);
252 }
253
254 #[test]
255 fn empty_naked_reports_close() {
256 let mut n = NakedPoc::new(4, 16).unwrap();
257 n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
258 let out = n.update(c(121.0, 95.0, 117.0, 1_000.0)).unwrap();
260 assert_eq!(n.naked_count(), 0);
261 assert!(
262 (out - 117.0).abs() < 1e-9,
263 "with no naked POC, output is the close"
264 );
265 }
266
267 #[test]
268 fn reset_clears_state() {
269 let mut n = NakedPoc::new(4, 8).unwrap();
270 n.batch(&[c(101.0, 99.0, 100.0, 1_000.0); 6]);
271 assert!(n.is_ready());
272 n.reset();
273 assert!(!n.is_ready());
274 assert_eq!(n.value(), None);
275 assert_eq!(n.naked_count(), 0);
276 }
277
278 #[test]
279 fn batch_equals_streaming() {
280 let candles: Vec<Candle> = (0..80)
281 .map(|i| {
282 let b = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
283 c(b + 1.0, b - 1.0, b, 1_000.0 + f64::from(i))
284 })
285 .collect();
286 let batch = NakedPoc::new(20, 24).unwrap().batch(&candles);
287 let mut b = NakedPoc::new(20, 24).unwrap();
288 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
289 assert_eq!(batch, streamed);
290 }
291
292 #[test]
293 fn flat_session_reports_price() {
294 let mut n = NakedPoc::new(2, 4).unwrap();
296 n.update(c(50.0, 50.0, 50.0, 10.0));
297 assert_eq!(n.update(c(50.0, 50.0, 50.0, 10.0)), Some(50.0));
298 }
299
300 #[test]
301 fn zero_volume_session_is_handled() {
302 let mut n = NakedPoc::new(2, 4).unwrap();
304 n.update(c(60.0, 40.0, 50.0, 0.0));
305 assert!(n.update(c(60.0, 40.0, 50.0, 0.0)).is_some());
306 }
307
308 #[test]
309 fn nearest_of_two_naked_pocs() {
310 let mut n = NakedPoc::new(2, 4).unwrap();
313 n.update(c(11.0, 9.0, 10.0, 100.0));
314 n.update(c(11.0, 9.0, 10.0, 100.0)); n.update(c(101.0, 99.0, 100.0, 100.0));
316 let v = n.update(c(101.0, 99.0, 100.0, 100.0)).unwrap(); assert!(
318 v > 50.0,
319 "nearest to close 100 should be the upper POC, got {v}"
320 );
321 }
322}