1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
11use crate::traits::Next;
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15#[derive(Debug, Clone, PartialEq, Eq, Error)]
17pub enum SessionLevelsError {
18 #[error("range_bars must be at least 1, got {0}")]
20 InvalidRangeBars(usize),
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum SessionVwapBand {
30 Sigma1,
32 Sigma2,
34 Sigma3,
36}
37
38impl SessionVwapBand {
39 #[inline]
41 #[must_use]
42 pub const fn multiplier(self) -> f64 {
43 match self {
44 Self::Sigma1 => 1.0,
45 Self::Sigma2 => 2.0,
46 Self::Sigma3 => 3.0,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
53pub struct SessionVwapState {
54 pub vwap: f64,
56 pub upper_1: f64,
58 pub lower_1: f64,
60 pub upper_2: f64,
62 pub lower_2: f64,
64 pub upper_3: f64,
66 pub lower_3: f64,
68 pub std_dev: f64,
70 pub cumulative_volume: f64,
72 pub bar_index: usize,
74}
75
76impl SessionVwapState {
77 #[inline]
79 #[must_use]
80 pub fn upper(&self, band: SessionVwapBand) -> f64 {
81 match band {
82 SessionVwapBand::Sigma1 => self.upper_1,
83 SessionVwapBand::Sigma2 => self.upper_2,
84 SessionVwapBand::Sigma3 => self.upper_3,
85 }
86 }
87
88 #[inline]
90 #[must_use]
91 pub fn lower(&self, band: SessionVwapBand) -> f64 {
92 match band {
93 SessionVwapBand::Sigma1 => self.lower_1,
94 SessionVwapBand::Sigma2 => self.lower_2,
95 SessionVwapBand::Sigma3 => self.lower_3,
96 }
97 }
98
99 #[inline]
101 #[must_use]
102 pub fn band(&self, band: SessionVwapBand) -> (f64, f64) {
103 (self.upper(band), self.lower(band))
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
109pub struct SessionVwapInput {
110 pub high: f64,
112 pub low: f64,
114 pub close: f64,
116 pub volume: f64,
118 pub is_session_start: bool,
120}
121
122impl From<(f64, f64, f64, f64, bool)> for SessionVwapInput {
123 #[inline]
124 fn from((high, low, close, volume, is_session_start): (f64, f64, f64, f64, bool)) -> Self {
125 Self {
126 high,
127 low,
128 close,
129 volume,
130 is_session_start,
131 }
132 }
133}
134
135impl From<SessionVwapInput> for (f64, f64, f64, f64, bool) {
136 #[inline]
137 fn from(input: SessionVwapInput) -> Self {
138 (
139 input.high,
140 input.low,
141 input.close,
142 input.volume,
143 input.is_session_start,
144 )
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct SessionVwap {
166 cumulative_volume: f64,
167 vwap: f64,
168 m2: f64,
169 bar_index: usize,
170 initialized: bool,
171}
172
173impl Default for SessionVwap {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179impl SessionVwap {
180 #[must_use]
182 pub fn new() -> Self {
183 Self {
184 cumulative_volume: 0.0,
185 vwap: 0.0,
186 m2: 0.0,
187 bar_index: 0,
188 initialized: false,
189 }
190 }
191
192 pub fn reset(&mut self) {
194 self.cumulative_volume = 0.0;
195 self.vwap = 0.0;
196 self.m2 = 0.0;
197 self.bar_index = 0;
198 self.initialized = false;
199 }
200
201 #[inline]
203 #[must_use]
204 pub fn cumulative_volume(&self) -> f64 {
205 self.cumulative_volume
206 }
207
208 #[inline]
210 #[must_use]
211 pub fn current_vwap(&self) -> f64 {
212 self.vwap
213 }
214
215 #[inline]
217 #[must_use]
218 pub fn current_bar_index(&self) -> usize {
219 self.bar_index
220 }
221
222 #[inline]
223 fn update_internal(
224 &mut self,
225 high: f64,
226 low: f64,
227 close: f64,
228 volume: f64,
229 is_session_start: bool,
230 ) -> SessionVwapState {
231 let tp = (high + low + close) / 3.0;
232 let vol = if volume > 0.0 { volume } else { 0.0 };
233
234 if !self.initialized || is_session_start {
235 self.cumulative_volume = vol;
236 self.vwap = tp;
237 self.m2 = 0.0;
238 self.bar_index = 0;
239 self.initialized = true;
240
241 SessionVwapState {
242 vwap: tp,
243 upper_1: tp,
244 lower_1: tp,
245 upper_2: tp,
246 lower_2: tp,
247 upper_3: tp,
248 lower_3: tp,
249 std_dev: 0.0,
250 cumulative_volume: vol,
251 bar_index: 0,
252 }
253 } else {
254 self.bar_index += 1;
255
256 if vol > 0.0 {
257 let old_vol = self.cumulative_volume;
258 self.cumulative_volume += vol;
259 if self.cumulative_volume > 0.0 {
260 let delta = tp - self.vwap;
261 let r = delta * (vol / self.cumulative_volume);
262 self.vwap += r;
263 self.m2 += old_vol * delta * r;
264 }
265 }
266
267 let variance = if self.cumulative_volume > 0.0 {
268 (self.m2 / self.cumulative_volume).max(0.0)
269 } else {
270 0.0
271 };
272 let std_dev = variance.sqrt();
273
274 SessionVwapState {
275 vwap: self.vwap,
276 upper_1: self.vwap + 1.0 * std_dev,
277 lower_1: self.vwap - 1.0 * std_dev,
278 upper_2: self.vwap + 2.0 * std_dev,
279 lower_2: self.vwap - 2.0 * std_dev,
280 upper_3: self.vwap + 3.0 * std_dev,
281 lower_3: self.vwap - 3.0 * std_dev,
282 std_dev,
283 cumulative_volume: self.cumulative_volume,
284 bar_index: self.bar_index,
285 }
286 }
287 }
288}
289
290impl Next<(f64, f64, f64, f64, bool)> for SessionVwap {
291 type Output = SessionVwapState;
292
293 #[inline]
294 fn next(
295 &mut self,
296 (high, low, close, volume, is_session_start): (f64, f64, f64, f64, bool),
297 ) -> Self::Output {
298 self.update_internal(high, low, close, volume, is_session_start)
299 }
300}
301
302impl Next<SessionVwapInput> for SessionVwap {
303 type Output = SessionVwapState;
304
305 #[inline]
306 fn next(&mut self, input: SessionVwapInput) -> Self::Output {
307 self.update_internal(
308 input.high,
309 input.low,
310 input.close,
311 input.volume,
312 input.is_session_start,
313 )
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
323pub enum OrbPhase {
324 Forming,
326 Active,
328 Closed,
330}
331
332impl OrbPhase {
333 #[inline]
335 #[must_use]
336 pub const fn is_forming(self) -> bool {
337 matches!(self, Self::Forming)
338 }
339
340 #[inline]
342 #[must_use]
343 pub const fn is_active(self) -> bool {
344 matches!(self, Self::Active)
345 }
346
347 #[inline]
349 #[must_use]
350 pub const fn is_closed(self) -> bool {
351 matches!(self, Self::Closed)
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
357pub enum OrbBreakout {
358 None,
360 BullishBreakout,
362 BearishBreakout,
364}
365
366impl OrbBreakout {
367 #[inline]
369 #[must_use]
370 pub const fn is_breakout(self) -> bool {
371 matches!(self, Self::BullishBreakout | Self::BearishBreakout)
372 }
373
374 #[inline]
376 #[must_use]
377 pub const fn is_bullish(self) -> bool {
378 matches!(self, Self::BullishBreakout)
379 }
380
381 #[inline]
383 #[must_use]
384 pub const fn is_bearish(self) -> bool {
385 matches!(self, Self::BearishBreakout)
386 }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
391pub struct OrbState {
392 pub phase: OrbPhase,
394 pub breakout: OrbBreakout,
396 pub range_high: f64,
398 pub range_low: f64,
400 pub range_size: f64,
402 pub target_1_bull: f64,
404 pub target_1_bear: f64,
406 pub target_2_bull: f64,
408 pub target_2_bear: f64,
410 pub bar_index: usize,
412}
413
414impl OrbState {
415 #[inline]
417 #[must_use]
418 pub const fn is_breakout(&self) -> bool {
419 self.breakout.is_breakout()
420 }
421
422 #[inline]
424 #[must_use]
425 pub fn target_1_reached(&self, price: f64) -> bool {
426 price >= self.target_1_bull || price <= self.target_1_bear
427 }
428
429 #[inline]
431 #[must_use]
432 pub fn target_2_reached(&self, price: f64) -> bool {
433 price >= self.target_2_bull || price <= self.target_2_bear
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
439pub struct OrbInput {
440 pub high: f64,
442 pub low: f64,
444 pub close: f64,
446 pub is_session_start: bool,
448}
449
450impl From<(f64, f64, f64, bool)> for OrbInput {
451 #[inline]
452 fn from((high, low, close, is_session_start): (f64, f64, f64, bool)) -> Self {
453 Self {
454 high,
455 low,
456 close,
457 is_session_start,
458 }
459 }
460}
461
462impl From<OrbInput> for (f64, f64, f64, bool) {
463 #[inline]
464 fn from(input: OrbInput) -> Self {
465 (input.high, input.low, input.close, input.is_session_start)
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
474pub struct OpeningRangeBreakout {
475 range_bars: usize,
476 max_session_bars: Option<usize>,
477 range_high: f64,
478 range_low: f64,
479 bars_in_session: usize,
480 phase: OrbPhase,
481 breakout: OrbBreakout,
482 initialized: bool,
483}
484
485impl Default for OpeningRangeBreakout {
486 fn default() -> Self {
487 Self::new(15)
488 }
489}
490
491impl OpeningRangeBreakout {
492 #[must_use]
496 pub fn new(range_bars: usize) -> Self {
497 Self {
498 range_bars: range_bars.max(1),
499 max_session_bars: None,
500 range_high: 0.0,
501 range_low: 0.0,
502 bars_in_session: 0,
503 phase: OrbPhase::Forming,
504 breakout: OrbBreakout::None,
505 initialized: false,
506 }
507 }
508
509 pub fn try_new(range_bars: usize) -> Result<Self, SessionLevelsError> {
511 if range_bars == 0 {
512 return Err(SessionLevelsError::InvalidRangeBars(0));
513 }
514 Ok(Self::new(range_bars))
515 }
516
517 #[must_use]
519 pub fn with_max_session_bars(mut self, max_bars: usize) -> Self {
520 self.max_session_bars = Some(max_bars);
521 self
522 }
523
524 #[inline]
526 #[must_use]
527 pub fn range_bars(&self) -> usize {
528 self.range_bars
529 }
530
531 #[inline]
533 #[must_use]
534 pub fn max_session_bars(&self) -> Option<usize> {
535 self.max_session_bars
536 }
537
538 pub fn reset(&mut self) {
540 self.range_high = 0.0;
541 self.range_low = 0.0;
542 self.bars_in_session = 0;
543 self.phase = OrbPhase::Forming;
544 self.breakout = OrbBreakout::None;
545 self.initialized = false;
546 }
547
548 pub fn close_session(&mut self) {
550 self.phase = OrbPhase::Closed;
551 self.breakout = OrbBreakout::None;
552 }
553
554 #[inline]
555 fn update_internal(
556 &mut self,
557 high: f64,
558 low: f64,
559 close: f64,
560 is_session_start: bool,
561 ) -> OrbState {
562 let h = high.max(low);
563 let l = high.min(low);
564
565 if !self.initialized || is_session_start {
566 self.bars_in_session = 0;
567 self.range_high = h;
568 self.range_low = l;
569 self.phase = OrbPhase::Forming;
570 self.breakout = OrbBreakout::None;
571 self.initialized = true;
572
573 let range_size = (self.range_high - self.range_low).max(0.0);
574 let state = OrbState {
575 phase: OrbPhase::Forming,
576 breakout: OrbBreakout::None,
577 range_high: self.range_high,
578 range_low: self.range_low,
579 range_size,
580 target_1_bull: self.range_high + 0.5 * range_size,
581 target_1_bear: self.range_low - 0.5 * range_size,
582 target_2_bull: self.range_high + 1.0 * range_size,
583 target_2_bear: self.range_low - 1.0 * range_size,
584 bar_index: 0,
585 };
586 self.bars_in_session = 1;
587 state
588 } else {
589 let current_bar = self.bars_in_session;
590 self.bars_in_session += 1;
591
592 if let Some(max_bars) = self.max_session_bars
593 && current_bar >= max_bars
594 {
595 self.phase = OrbPhase::Closed;
596 self.breakout = OrbBreakout::None;
597 let range_size = (self.range_high - self.range_low).max(0.0);
598 return OrbState {
599 phase: OrbPhase::Closed,
600 breakout: OrbBreakout::None,
601 range_high: self.range_high,
602 range_low: self.range_low,
603 range_size,
604 target_1_bull: self.range_high + 0.5 * range_size,
605 target_1_bear: self.range_low - 0.5 * range_size,
606 target_2_bull: self.range_high + 1.0 * range_size,
607 target_2_bear: self.range_low - 1.0 * range_size,
608 bar_index: current_bar,
609 };
610 }
611
612 if current_bar < self.range_bars {
613 self.range_high = self.range_high.max(h);
614 self.range_low = self.range_low.min(l);
615 self.phase = OrbPhase::Forming;
616 self.breakout = OrbBreakout::None;
617 } else {
618 self.phase = OrbPhase::Active;
619 if close > self.range_high {
620 self.breakout = OrbBreakout::BullishBreakout;
621 } else if close < self.range_low {
622 self.breakout = OrbBreakout::BearishBreakout;
623 } else {
624 self.breakout = OrbBreakout::None;
625 }
626 }
627
628 let range_size = (self.range_high - self.range_low).max(0.0);
629 OrbState {
630 phase: self.phase,
631 breakout: self.breakout,
632 range_high: self.range_high,
633 range_low: self.range_low,
634 range_size,
635 target_1_bull: self.range_high + 0.5 * range_size,
636 target_1_bear: self.range_low - 0.5 * range_size,
637 target_2_bull: self.range_high + 1.0 * range_size,
638 target_2_bear: self.range_low - 1.0 * range_size,
639 bar_index: current_bar,
640 }
641 }
642 }
643}
644
645impl Next<(f64, f64, f64, bool)> for OpeningRangeBreakout {
646 type Output = OrbState;
647
648 #[inline]
649 fn next(
650 &mut self,
651 (high, low, close, is_session_start): (f64, f64, f64, bool),
652 ) -> Self::Output {
653 self.update_internal(high, low, close, is_session_start)
654 }
655}
656
657impl Next<OrbInput> for OpeningRangeBreakout {
658 type Output = OrbState;
659
660 #[inline]
661 fn next(&mut self, input: OrbInput) -> Self::Output {
662 self.update_internal(input.high, input.low, input.close, input.is_session_start)
663 }
664}
665
666pub const SESSION_VWAP_METADATA: IndicatorMetadata = IndicatorMetadata {
672 name: "Session VWAP with Standard Deviation Bands",
673 description: "Session-anchored Volume Weighted Average Price (VWAP) with three standard deviation volatility bands, resetting at session boundaries.",
674 usage: "Use as an intraday fair value benchmark. The core VWAP serves as the primary institutional execution level, while Sigma 1, 2, and 3 standard deviation bands identify statistical mean reversion boundaries and dynamic support/resistance.",
675 keywords: &[
676 "vwap",
677 "session",
678 "intraday",
679 "volume",
680 "bands",
681 "standard-deviation",
682 "mean-reversion",
683 ],
684 ehlers_summary: "Volume Weighted Average Price calculates the cumulative typical price weighted by volume across a trading session. Incorporating standard deviation bands provides dynamic volatility envelopes that highlight statistical value containment and institutional liquidation extremes. — StockCharts / Investopedia",
685 params: &[],
686 formula_source: "https://chartschool.stockcharts.com/table-of-contents/technical-indicators-and-overlays/technical-overlays/vwap-standard-deviation-bands",
687 formula_latex: r#"
688\[
689TP_t = \frac{H_t + L_t + C_t}{3} \\
690VWAP_t = \frac{\sum_{i=1}^t TP_i \cdot V_i}{\sum_{i=1}^t V_i} \\
691\sigma_t = \sqrt{\frac{\sum_{i=1}^t V_i \cdot (TP_i - VWAP_t)^2}{\sum_{i=1}^t V_i}} \\
692\text{Upper}_k = VWAP_t + k \cdot \sigma_t, \quad \text{Lower}_k = VWAP_t - k \cdot \sigma_t \quad (k \in \{1, 2, 3\})
693\]
694"#,
695 gold_standard_file: "session_vwap.json",
696 category: "Volume",
697};
698
699pub const ORB_METADATA: IndicatorMetadata = IndicatorMetadata {
701 name: "Opening Range Breakout",
702 description: "Tracks the high and low over the opening range bars and detects directional breakouts with extension targets.",
703 usage: "Identifies early-session market commitment and momentum expansion. When price breaks out of the established opening range, traders enter in the direction of the break targeting 0.5x and 1.0x range extensions.",
704 keywords: &[
705 "orb",
706 "opening-range",
707 "breakout",
708 "session",
709 "intraday",
710 "toby-crabel",
711 "trend",
712 ],
713 ehlers_summary: "Pioneered by Toby Crabel in 'Day Trading with Short Term Price Patterns' (1990), Opening Range Breakout establishes support and resistance during the initial minutes of trading. A decisive breakout beyond the opening boundary often signals directional conviction for the remainder of the trading day. — Toby Crabel / TradingView",
714 params: &[ParamDef {
715 name: "range_bars",
716 default: "15",
717 description: "Number of opening bars defining the range window",
718 }],
719 formula_source: "https://www.tradingview.com/support/solutions/43000696950-opening-range-breakout-orb/",
720 formula_latex: r#"
721\[
722\text{Range High} = \max_{i \in [1, N]}(H_i), \quad \text{Range Low} = \min_{i \in [1, N]}(L_i) \\
723\text{Range Size} = \text{Range High} - \text{Range Low} \\
724\text{Target}_{1,\text{bull}} = \text{Range High} + 0.5 \cdot \text{Range Size}, \quad \text{Target}_{1,\text{bear}} = \text{Range Low} - 0.5 \cdot \text{Range Size} \\
725\text{Target}_{2,\text{bull}} = \text{Range High} + 1.0 \cdot \text{Range Size}, \quad \text{Target}_{2,\text{bear}} = \text{Range Low} - 1.0 \cdot \text{Range Size}
726\]
727"#,
728 gold_standard_file: "orb.json",
729 category: "Price Action",
730};
731
732#[cfg(test)]
737mod tests {
738 use super::*;
739 use approx::assert_relative_eq;
740 use proptest::prelude::*;
741
742 #[test]
743 fn test_session_vwap_single_bar() {
744 let mut vwap = SessionVwap::new();
745 let state = vwap.next((105.0, 95.0, 100.0, 50.0, true));
747
748 assert_relative_eq!(state.vwap, 100.0);
749 assert_relative_eq!(state.std_dev, 0.0);
750 assert_relative_eq!(state.upper_1, 100.0);
751 assert_relative_eq!(state.lower_1, 100.0);
752 assert_relative_eq!(state.upper_2, 100.0);
753 assert_relative_eq!(state.lower_2, 100.0);
754 assert_relative_eq!(state.upper_3, 100.0);
755 assert_relative_eq!(state.lower_3, 100.0);
756 assert_relative_eq!(state.cumulative_volume, 50.0);
757 assert_eq!(state.bar_index, 0);
758 }
759
760 #[test]
761 fn test_session_vwap_multi_bar_accumulation() {
762 let mut vwap = SessionVwap::new();
763
764 let s0 = vwap.next((105.0, 95.0, 100.0, 100.0, true));
766 assert_relative_eq!(s0.vwap, 100.0);
767 assert_relative_eq!(s0.std_dev, 0.0);
768 assert_eq!(s0.bar_index, 0);
769
770 let s1 = vwap.next((115.0, 105.0, 110.0, 100.0, false));
776 assert_relative_eq!(s1.vwap, 105.0);
777 assert_relative_eq!(s1.std_dev, 5.0);
778 assert_relative_eq!(s1.upper_1, 110.0);
779 assert_relative_eq!(s1.lower_1, 100.0);
780 assert_relative_eq!(s1.upper_2, 115.0);
781 assert_relative_eq!(s1.lower_2, 95.0);
782 assert_relative_eq!(s1.upper_3, 120.0);
783 assert_relative_eq!(s1.lower_3, 90.0);
784 assert_relative_eq!(s1.cumulative_volume, 200.0);
785 assert_eq!(s1.bar_index, 1);
786
787 assert_relative_eq!(s1.upper(SessionVwapBand::Sigma1), 110.0);
789 assert_relative_eq!(s1.lower(SessionVwapBand::Sigma1), 100.0);
790 assert_eq!(s1.band(SessionVwapBand::Sigma2), (115.0, 95.0));
791
792 let s2 = vwap.next((125.0, 115.0, 120.0, 200.0, false));
802 assert_relative_eq!(s2.vwap, 112.5);
803 assert_relative_eq!(s2.std_dev, 68.75_f64.sqrt(), epsilon = 1e-10);
804 assert_relative_eq!(s2.upper_1, 112.5 + 68.75_f64.sqrt(), epsilon = 1e-10);
805 assert_relative_eq!(s2.lower_1, 112.5 - 68.75_f64.sqrt(), epsilon = 1e-10);
806 assert_relative_eq!(s2.cumulative_volume, 400.0);
807 assert_eq!(s2.bar_index, 2);
808 }
809
810 #[test]
811 fn test_session_vwap_session_reset() {
812 let mut vwap = SessionVwap::new();
813
814 vwap.next((105.0, 95.0, 100.0, 100.0, true));
815 vwap.next((115.0, 105.0, 110.0, 100.0, false));
816
817 let reset_state = vwap.next((205.0, 195.0, 200.0, 50.0, true));
819 assert_relative_eq!(reset_state.vwap, 200.0);
820 assert_relative_eq!(reset_state.std_dev, 0.0);
821 assert_relative_eq!(reset_state.cumulative_volume, 50.0);
822 assert_eq!(reset_state.bar_index, 0);
823
824 let next_state = vwap.next((215.0, 205.0, 210.0, 50.0, false));
826 assert_relative_eq!(next_state.vwap, 205.0);
827 assert_relative_eq!(next_state.std_dev, 5.0);
828 assert_relative_eq!(next_state.cumulative_volume, 100.0);
829 assert_eq!(next_state.bar_index, 1);
830 }
831
832 #[test]
833 fn test_session_vwap_zero_volume_handling() {
834 let mut vwap = SessionVwap::new();
835
836 let s0 = vwap.next((105.0, 95.0, 100.0, 0.0, true));
838 assert_relative_eq!(s0.vwap, 100.0);
839 assert_relative_eq!(s0.std_dev, 0.0);
840 assert_relative_eq!(s0.cumulative_volume, 0.0);
841
842 let s1 = vwap.next((108.0, 98.0, 103.0, 0.0, false));
844 assert_relative_eq!(s1.vwap, 100.0);
845 assert_relative_eq!(s1.std_dev, 0.0);
846 assert_relative_eq!(s1.cumulative_volume, 0.0);
847 }
848
849 #[test]
850 fn test_orb_range_formation_and_phase_transition() {
851 let mut orb = OpeningRangeBreakout::new(3);
852
853 let s0 = orb.next((105.0, 95.0, 100.0, true));
855 assert_eq!(s0.phase, OrbPhase::Forming);
856 assert_eq!(s0.breakout, OrbBreakout::None);
857 assert_relative_eq!(s0.range_high, 105.0);
858 assert_relative_eq!(s0.range_low, 95.0);
859 assert_relative_eq!(s0.range_size, 10.0);
860 assert_relative_eq!(s0.target_1_bull, 110.0);
861 assert_relative_eq!(s0.target_1_bear, 90.0);
862 assert_relative_eq!(s0.target_2_bull, 115.0);
863 assert_relative_eq!(s0.target_2_bear, 85.0);
864 assert_eq!(s0.bar_index, 0);
865
866 let s1 = orb.next((108.0, 94.0, 102.0, false));
868 assert_eq!(s1.phase, OrbPhase::Forming);
869 assert_eq!(s1.breakout, OrbBreakout::None);
870 assert_relative_eq!(s1.range_high, 108.0);
871 assert_relative_eq!(s1.range_low, 94.0);
872 assert_relative_eq!(s1.range_size, 14.0);
873 assert_eq!(s1.bar_index, 1);
874
875 let s2 = orb.next((107.0, 96.0, 101.0, false));
877 assert_eq!(s2.phase, OrbPhase::Forming);
878 assert_eq!(s2.breakout, OrbBreakout::None);
879 assert_relative_eq!(s2.range_high, 108.0);
880 assert_relative_eq!(s2.range_low, 94.0);
881 assert_eq!(s2.bar_index, 2);
882
883 let s3 = orb.next((106.0, 98.0, 105.0, false));
886 assert_eq!(s3.phase, OrbPhase::Active);
887 assert_eq!(s3.breakout, OrbBreakout::None);
888 assert_relative_eq!(s3.range_high, 108.0); assert_relative_eq!(s3.range_low, 94.0); assert_relative_eq!(s3.range_size, 14.0);
891 assert_relative_eq!(s3.target_1_bull, 115.0);
892 assert_relative_eq!(s3.target_1_bear, 87.0);
893 assert_relative_eq!(s3.target_2_bull, 122.0);
894 assert_relative_eq!(s3.target_2_bear, 80.0);
895 assert_eq!(s3.bar_index, 3);
896 }
897
898 #[test]
899 fn test_orb_bullish_and_bearish_breakouts() {
900 let mut orb = OpeningRangeBreakout::new(2);
901
902 orb.next((105.0, 95.0, 100.0, true));
904 orb.next((110.0, 90.0, 102.0, false));
905
906 let s_bull = orb.next((115.0, 108.0, 112.0, false));
908 assert_eq!(s_bull.phase, OrbPhase::Active);
909 assert_eq!(s_bull.breakout, OrbBreakout::BullishBreakout);
910 assert!(s_bull.is_breakout());
911 assert!(s_bull.breakout.is_bullish());
912 assert!(!s_bull.breakout.is_bearish());
913
914 assert_relative_eq!(s_bull.range_size, 20.0);
916 assert_relative_eq!(s_bull.target_1_bull, 120.0);
917 assert!(!s_bull.target_1_reached(112.0));
918 assert!(s_bull.target_1_reached(120.0));
919 assert!(s_bull.target_1_reached(125.0));
920
921 let s_bear = orb.next((95.0, 85.0, 88.0, false));
923 assert_eq!(s_bear.phase, OrbPhase::Active);
924 assert_eq!(s_bear.breakout, OrbBreakout::BearishBreakout);
925 assert!(s_bear.is_breakout());
926 assert!(s_bear.breakout.is_bearish());
927 assert!(!s_bear.breakout.is_bullish());
928
929 let s_none = orb.next((102.0, 98.0, 100.0, false));
931 assert_eq!(s_none.phase, OrbPhase::Active);
932 assert_eq!(s_none.breakout, OrbBreakout::None);
933 assert!(!s_none.is_breakout());
934 }
935
936 #[test]
937 fn test_orb_session_reset() {
938 let mut orb = OpeningRangeBreakout::new(2);
939
940 orb.next((105.0, 95.0, 100.0, true));
941 orb.next((110.0, 90.0, 102.0, false));
942 let active = orb.next((115.0, 105.0, 112.0, false));
943 assert_eq!(active.phase, OrbPhase::Active);
944
945 let reset_state = orb.next((205.0, 195.0, 200.0, true));
947 assert_eq!(reset_state.phase, OrbPhase::Forming);
948 assert_eq!(reset_state.breakout, OrbBreakout::None);
949 assert_relative_eq!(reset_state.range_high, 205.0);
950 assert_relative_eq!(reset_state.range_low, 195.0);
951 assert_relative_eq!(reset_state.range_size, 10.0);
952 assert_eq!(reset_state.bar_index, 0);
953 }
954
955 #[test]
956 fn test_orb_max_session_bars_closed() {
957 let mut orb = OpeningRangeBreakout::new(2).with_max_session_bars(4);
958
959 orb.next((105.0, 95.0, 100.0, true)); orb.next((108.0, 94.0, 102.0, false)); let s2 = orb.next((112.0, 106.0, 111.0, false)); assert_eq!(s2.phase, OrbPhase::Active);
963 let s3 = orb.next((115.0, 107.0, 114.0, false)); assert_eq!(s3.phase, OrbPhase::Active);
965
966 let s4 = orb.next((120.0, 110.0, 118.0, false));
968 assert_eq!(s4.phase, OrbPhase::Closed);
969 assert_eq!(s4.breakout, OrbBreakout::None);
970 assert!(s4.phase.is_closed());
971 }
972
973 #[test]
974 fn test_orb_try_new_invalid_range_bars() {
975 assert_eq!(
976 OpeningRangeBreakout::try_new(0),
977 Err(SessionLevelsError::InvalidRangeBars(0))
978 );
979 assert!(OpeningRangeBreakout::try_new(5).is_ok());
980 }
981
982 #[test]
983 fn test_metadata_consistency() {
984 assert_eq!(
985 SESSION_VWAP_METADATA.name,
986 "Session VWAP with Standard Deviation Bands"
987 );
988 assert_eq!(
989 SESSION_VWAP_METADATA.gold_standard_file,
990 "session_vwap.json"
991 );
992 assert_eq!(SESSION_VWAP_METADATA.category, "Volume");
993
994 assert_eq!(ORB_METADATA.name, "Opening Range Breakout");
995 assert_eq!(ORB_METADATA.gold_standard_file, "orb.json");
996 assert_eq!(ORB_METADATA.category, "Price Action");
997 }
998
999 fn session_vwap_batch(data: &[(f64, f64, f64, f64, bool)]) -> Vec<SessionVwapState> {
1004 let mut results = Vec::with_capacity(data.len());
1005 let mut session_bars: Vec<(f64, f64)> = Vec::new(); let mut bar_index = 0;
1007
1008 for &(high, low, close, volume, is_session_start) in data {
1009 let tp = (high + low + close) / 3.0;
1010 let vol = if volume > 0.0 { volume } else { 0.0 };
1011
1012 if session_bars.is_empty() || is_session_start {
1013 session_bars.clear();
1014 bar_index = 0;
1015 } else {
1016 bar_index += 1;
1017 }
1018
1019 session_bars.push((tp, vol));
1020
1021 let total_vol: f64 = session_bars.iter().map(|&(_, v)| v).sum();
1022 let vwap = if total_vol > 0.0 {
1023 session_bars.iter().map(|&(p, v)| p * v).sum::<f64>() / total_vol
1024 } else {
1025 tp
1026 };
1027
1028 let variance = if total_vol > 0.0 {
1029 let sum_sq_diff: f64 = session_bars
1030 .iter()
1031 .map(|&(p, v)| v * (p - vwap).powi(2))
1032 .sum();
1033 (sum_sq_diff / total_vol).max(0.0)
1034 } else {
1035 0.0
1036 };
1037 let std_dev = variance.sqrt();
1038
1039 results.push(SessionVwapState {
1040 vwap,
1041 upper_1: vwap + 1.0 * std_dev,
1042 lower_1: vwap - 1.0 * std_dev,
1043 upper_2: vwap + 2.0 * std_dev,
1044 lower_2: vwap - 2.0 * std_dev,
1045 upper_3: vwap + 3.0 * std_dev,
1046 lower_3: vwap - 3.0 * std_dev,
1047 std_dev,
1048 cumulative_volume: total_vol,
1049 bar_index,
1050 });
1051 }
1052
1053 results
1054 }
1055
1056 fn orb_batch(data: &[(f64, f64, f64, bool)], range_bars: usize) -> Vec<OrbState> {
1057 let range_bars = range_bars.max(1);
1058 let mut results = Vec::with_capacity(data.len());
1059 let mut session_highs: Vec<f64> = Vec::new();
1060 let mut session_lows: Vec<f64> = Vec::new();
1061
1062 for &(high, low, close, is_session_start) in data {
1063 let h = high.max(low);
1064 let l = high.min(low);
1065
1066 if session_highs.is_empty() || is_session_start {
1067 session_highs.clear();
1068 session_lows.clear();
1069 }
1070
1071 let bar_idx = session_highs.len();
1072 session_highs.push(h);
1073 session_lows.push(l);
1074
1075 let forming_window = (bar_idx + 1).min(range_bars);
1076 let range_high = session_highs[..forming_window]
1077 .iter()
1078 .copied()
1079 .fold(f64::NEG_INFINITY, f64::max);
1080 let range_low = session_lows[..forming_window]
1081 .iter()
1082 .copied()
1083 .fold(f64::INFINITY, f64::min);
1084 let range_size = (range_high - range_low).max(0.0);
1085
1086 let phase = if bar_idx < range_bars {
1087 OrbPhase::Forming
1088 } else {
1089 OrbPhase::Active
1090 };
1091
1092 let breakout = if phase == OrbPhase::Active {
1093 if close > range_high {
1094 OrbBreakout::BullishBreakout
1095 } else if close < range_low {
1096 OrbBreakout::BearishBreakout
1097 } else {
1098 OrbBreakout::None
1099 }
1100 } else {
1101 OrbBreakout::None
1102 };
1103
1104 results.push(OrbState {
1105 phase,
1106 breakout,
1107 range_high,
1108 range_low,
1109 range_size,
1110 target_1_bull: range_high + 0.5 * range_size,
1111 target_1_bear: range_low - 0.5 * range_size,
1112 target_2_bull: range_high + 1.0 * range_size,
1113 target_2_bear: range_low - 1.0 * range_size,
1114 bar_index: bar_idx,
1115 });
1116 }
1117
1118 results
1119 }
1120
1121 proptest! {
1122 #[test]
1123 fn test_session_vwap_parity(
1124 prices in prop::collection::vec((10.0..500.0f64, 10.0..500.0f64, 10.0..500.0f64, 1.0..1000.0f64, any::<bool>()), 1..100)
1125 ) {
1126 let mut sanitized_input: Vec<(f64, f64, f64, f64, bool)> = Vec::with_capacity(prices.len());
1127 for (i, (h_raw, l_raw, c_raw, vol, is_start)) in prices.into_iter().enumerate() {
1128 let high: f64 = h_raw.max(l_raw).max(c_raw);
1129 let low: f64 = h_raw.min(l_raw).min(c_raw);
1130 let is_session_start = if i == 0 { true } else { is_start };
1131 sanitized_input.push((high, low, c_raw, vol, is_session_start));
1132 }
1133
1134 let mut indicator = SessionVwap::new();
1135 let mut streaming_results = Vec::with_capacity(sanitized_input.len());
1136 for &val in &sanitized_input {
1137 streaming_results.push(indicator.next(val));
1138 }
1139
1140 let batch_results = session_vwap_batch(&sanitized_input);
1141
1142 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
1143 assert_relative_eq!(s.vwap, b.vwap, epsilon = 1e-8);
1144 assert_relative_eq!(s.std_dev, b.std_dev, epsilon = 1e-8);
1145 assert_relative_eq!(s.upper_1, b.upper_1, epsilon = 1e-8);
1146 assert_relative_eq!(s.lower_1, b.lower_1, epsilon = 1e-8);
1147 assert_relative_eq!(s.upper_2, b.upper_2, epsilon = 1e-8);
1148 assert_relative_eq!(s.lower_2, b.lower_2, epsilon = 1e-8);
1149 assert_relative_eq!(s.upper_3, b.upper_3, epsilon = 1e-8);
1150 assert_relative_eq!(s.lower_3, b.lower_3, epsilon = 1e-8);
1151 assert_relative_eq!(s.cumulative_volume, b.cumulative_volume, epsilon = 1e-8);
1152 assert_eq!(s.bar_index, b.bar_index);
1153 }
1154 }
1155
1156 #[test]
1157 fn test_orb_parity(
1158 bars in prop::collection::vec((10.0..500.0f64, 10.0..500.0f64, 10.0..500.0f64, any::<bool>()), 1..100),
1159 range_bars in 1..20usize,
1160 ) {
1161 let mut sanitized_input: Vec<(f64, f64, f64, bool)> = Vec::with_capacity(bars.len());
1162 for (i, (h_raw, l_raw, c_raw, is_start)) in bars.into_iter().enumerate() {
1163 let high: f64 = h_raw.max(l_raw).max(c_raw);
1164 let low: f64 = h_raw.min(l_raw).min(c_raw);
1165 let is_session_start = if i == 0 { true } else { is_start };
1166 sanitized_input.push((high, low, c_raw, is_session_start));
1167 }
1168
1169 let mut indicator = OpeningRangeBreakout::new(range_bars);
1170 let mut streaming_results = Vec::with_capacity(sanitized_input.len());
1171 for &val in &sanitized_input {
1172 streaming_results.push(indicator.next(val));
1173 }
1174
1175 let batch_results = orb_batch(&sanitized_input, range_bars);
1176
1177 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
1178 assert_eq!(s.phase, b.phase);
1179 assert_eq!(s.breakout, b.breakout);
1180 assert_relative_eq!(s.range_high, b.range_high, epsilon = 1e-10);
1181 assert_relative_eq!(s.range_low, b.range_low, epsilon = 1e-10);
1182 assert_relative_eq!(s.range_size, b.range_size, epsilon = 1e-10);
1183 assert_relative_eq!(s.target_1_bull, b.target_1_bull, epsilon = 1e-10);
1184 assert_relative_eq!(s.target_1_bear, b.target_1_bear, epsilon = 1e-10);
1185 assert_relative_eq!(s.target_2_bull, b.target_2_bull, epsilon = 1e-10);
1186 assert_relative_eq!(s.target_2_bear, b.target_2_bear, epsilon = 1e-10);
1187 assert_eq!(s.bar_index, b.bar_index);
1188 }
1189 }
1190 }
1191}