1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
61use crate::traits::Next;
62use serde::{Deserialize, Serialize};
63use std::collections::VecDeque;
64
65pub const CHOCH_METADATA: IndicatorMetadata = IndicatorMetadata {
70 name: "choch",
71 description: "Detects Market Structure Breaks (BOS) and Changes of Character (CHoCH) using swing highs and lows.",
72 usage: "Identifies trend continuations (BOS) and trend reversals (CHoCH) in market structure for Smart Money Concepts (SMC) trading strategies.",
73 keywords: &["smc", "market structure", "choch", "bos", "swings", "trend"],
74 ehlers_summary: "",
75 params: &[ParamDef {
76 name: "swing_strength",
77 default: "5",
78 description: "Number of bars required on each side to confirm a swing high or swing low.",
79 }],
80 formula_source: "https://www.tradingview.com/support/solutions/43000703076-market-structure/",
81 formula_latex: r"\[ \text{BOS} = \text{Close} > \text{SwingHigh}_{\text{bull}} \lor \text{Close} < \text{SwingLow}_{\text{bear}}, \quad \text{CHoCH} = \text{Close} < \text{SwingLow}_{\text{bull}} \lor \text{Close} > \text{SwingHigh}_{\text{bear}} \]",
82 gold_standard_file: "choch.json",
83 category: "Price Action / SMC",
84};
85
86pub const ORDER_BLOCK_METADATA: IndicatorMetadata = IndicatorMetadata {
87 name: "order_block",
88 description: "Detects Bullish and Bearish Order Blocks (institutional entry zones) and tracks their mitigation.",
89 usage: "Identifies institutional order blocks preceding energetic displacements and monitors subsequent mitigation/invalidation.",
90 keywords: &[
91 "smc",
92 "order block",
93 "ob",
94 "smart money",
95 "mitigation",
96 "institutional",
97 ],
98 ehlers_summary: "",
99 params: &[ParamDef {
100 name: "max_active_blocks",
101 default: "20",
102 description: "Maximum number of unmitigated or partially mitigated active order blocks retained in memory.",
103 }],
104 formula_source: "https://www.tradingview.com/support/solutions/43000703077-order-blocks/",
105 formula_latex: r"\[ \text{Bullish OB} = \text{Candle}_{t-1} \text{ (down)} \land \text{Close}_t > \text{High}_{t-1}, \quad \text{Bearish OB} = \text{Candle}_{t-1} \text{ (up)} \land \text{Close}_t < \text{Low}_{t-1} \]",
106 gold_standard_file: "order_block.json",
107 category: "Price Action / SMC",
108};
109
110pub const FAIR_VALUE_GAP_METADATA: IndicatorMetadata = IndicatorMetadata {
111 name: "fair_value_gap",
112 description: "Detects 3-bar Fair Value Gaps (price imbalances) and tracks their mitigation and fill levels.",
113 usage: "Identifies liquidity imbalances where price moves impulsively, leaving untraded zones that act as magnets for price retests.",
114 keywords: &[
115 "smc",
116 "fvg",
117 "fair value gap",
118 "imbalance",
119 "liquidity",
120 "smart money",
121 ],
122 ehlers_summary: "",
123 params: &[
124 ParamDef {
125 name: "min_gap_size",
126 default: "0.0",
127 description: "Minimum price distance required between outer bars to register a valid Fair Value Gap.",
128 },
129 ParamDef {
130 name: "max_active_gaps",
131 default: "20",
132 description: "Maximum number of unmitigated or partially mitigated fair value gaps retained in memory.",
133 },
134 ],
135 formula_source: "https://www.tradingview.com/support/solutions/43000703078-fair-value-gaps/",
136 formula_latex: r"\[ \text{Bullish FVG} = \text{Low}_t > \text{High}_{t-2}, \quad \text{Bearish FVG} = \text{High}_t < \text{Low}_{t-2} \]",
137 gold_standard_file: "fair_value_gap.json",
138 category: "Price Action / SMC",
139};
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
147pub enum StructureTrend {
148 Bullish,
150 Bearish,
152 Undetermined,
154}
155
156impl StructureTrend {
157 #[inline]
158 pub fn is_bullish(self) -> bool {
159 self == Self::Bullish
160 }
161
162 #[inline]
163 pub fn is_bearish(self) -> bool {
164 self == Self::Bearish
165 }
166
167 #[inline]
168 pub fn is_undetermined(self) -> bool {
169 self == Self::Undetermined
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
175pub enum StructureBreakType {
176 Bos,
178 Choch,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
184pub enum StructureBreakDirection {
185 Bullish,
187 Bearish,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
193pub enum SwingType {
194 High,
196 Low,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
202pub struct SmcSwingPoint {
203 pub bar: usize,
205 pub price: f64,
207 pub swing_type: SwingType,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
213pub struct StructureBreakEvent {
214 pub break_type: StructureBreakType,
216 pub direction: StructureBreakDirection,
218 pub level: f64,
220 pub broken_swing_bar: usize,
222 pub trigger_bar: usize,
224 pub trigger_price: f64,
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
230pub enum MitigationStatus {
231 Unmitigated,
233 PartiallyMitigated,
235 FullyMitigated,
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct ChochState {
246 pub trend: StructureTrend,
248 pub last_break: Option<StructureBreakEvent>,
250 pub last_swing_high: Option<SmcSwingPoint>,
252 pub last_swing_low: Option<SmcSwingPoint>,
254 pub bar_index: usize,
256}
257
258impl ChochState {
259 #[inline]
261 pub fn current_break(&self) -> Option<StructureBreakEvent> {
262 self.last_break.filter(|b| b.trigger_bar == self.bar_index)
263 }
264
265 #[inline]
266 pub fn is_bullish(&self) -> bool {
267 self.trend.is_bullish()
268 }
269
270 #[inline]
271 pub fn is_bearish(&self) -> bool {
272 self.trend.is_bearish()
273 }
274}
275
276#[derive(Debug, Clone)]
278pub struct Choch {
279 swing_strength: usize,
280 highs: VecDeque<f64>,
281 lows: VecDeque<f64>,
282 bar_index: usize,
283 trend: StructureTrend,
284 last_swing_high: Option<SmcSwingPoint>,
285 prev_swing_high: Option<SmcSwingPoint>,
286 last_swing_low: Option<SmcSwingPoint>,
287 prev_swing_low: Option<SmcSwingPoint>,
288 broken_high: bool,
289 broken_low: bool,
290 last_break: Option<StructureBreakEvent>,
291}
292
293pub type ChochDetector = Choch;
294
295impl Choch {
296 pub fn new(swing_strength: usize) -> Self {
301 let strength = swing_strength.max(1);
302 Self {
303 swing_strength: strength,
304 highs: VecDeque::with_capacity(2 * strength + 1),
305 lows: VecDeque::with_capacity(2 * strength + 1),
306 bar_index: 0,
307 trend: StructureTrend::Undetermined,
308 last_swing_high: None,
309 prev_swing_high: None,
310 last_swing_low: None,
311 prev_swing_low: None,
312 broken_high: false,
313 broken_low: false,
314 last_break: None,
315 }
316 }
317
318 pub fn reset(&mut self) {
320 self.highs.clear();
321 self.lows.clear();
322 self.bar_index = 0;
323 self.trend = StructureTrend::Undetermined;
324 self.last_swing_high = None;
325 self.prev_swing_high = None;
326 self.last_swing_low = None;
327 self.prev_swing_low = None;
328 self.broken_high = false;
329 self.broken_low = false;
330 self.last_break = None;
331 }
332
333 pub fn process_batch(&mut self, bars: &[(f64, f64, f64)]) -> Vec<ChochState> {
335 let mut results = Vec::with_capacity(bars.len());
336 for &bar in bars {
337 results.push(self.next(bar));
338 }
339 results
340 }
341}
342
343impl Default for Choch {
344 fn default() -> Self {
345 Self::new(5)
346 }
347}
348
349impl Next<(f64, f64, f64)> for Choch {
350 type Output = ChochState;
351
352 fn next(&mut self, input: (f64, f64, f64)) -> Self::Output {
353 let (high, low, close) = input;
354
355 if !high.is_finite() || !low.is_finite() || !close.is_finite() {
357 return ChochState {
358 trend: self.trend,
359 last_break: self.last_break,
360 last_swing_high: self.last_swing_high,
361 last_swing_low: self.last_swing_low,
362 bar_index: self.bar_index,
363 };
364 }
365
366 let curr_bar = self.bar_index;
367 let window_len = 2 * self.swing_strength + 1;
368
369 self.highs.push_back(high);
371 self.lows.push_back(low);
372 if self.highs.len() > window_len {
373 self.highs.pop_front();
374 self.lows.pop_front();
375 }
376
377 if self.highs.len() == window_len {
379 let candidate_bar = curr_bar - self.swing_strength;
380 let center_high = self.highs[self.swing_strength];
381 let center_low = self.lows[self.swing_strength];
382
383 let mut is_swing_high = true;
384 let mut is_swing_low = true;
385
386 for i in 0..window_len {
387 if i == self.swing_strength {
388 continue;
389 }
390 if i < self.swing_strength {
391 if self.highs[i] > center_high {
392 is_swing_high = false;
393 }
394 if self.lows[i] < center_low {
395 is_swing_low = false;
396 }
397 } else {
398 if self.highs[i] >= center_high {
400 is_swing_high = false;
401 }
402 if self.lows[i] <= center_low {
403 is_swing_low = false;
404 }
405 }
406 }
407
408 if is_swing_high {
409 let point = SmcSwingPoint {
410 bar: candidate_bar,
411 price: center_high,
412 swing_type: SwingType::High,
413 };
414 self.prev_swing_high = self.last_swing_high;
415 self.last_swing_high = Some(point);
416 self.broken_high = false;
417 }
418
419 if is_swing_low {
420 let point = SmcSwingPoint {
421 bar: candidate_bar,
422 price: center_low,
423 swing_type: SwingType::Low,
424 };
425 self.prev_swing_low = self.last_swing_low;
426 self.last_swing_low = Some(point);
427 self.broken_low = false;
428 }
429
430 if self.trend == StructureTrend::Undetermined
432 && let (Some(sh), Some(psh), Some(sl), Some(psl)) = (
433 self.last_swing_high,
434 self.prev_swing_high,
435 self.last_swing_low,
436 self.prev_swing_low,
437 )
438 {
439 if sh.price > psh.price && sl.price > psl.price {
440 self.trend = StructureTrend::Bullish;
441 } else if sh.price < psh.price && sl.price < psl.price {
442 self.trend = StructureTrend::Bearish;
443 }
444 }
445 }
446
447 let mut triggered_break = None;
449
450 if let Some(sh) = self.last_swing_high
452 && !self.broken_high
453 && close > sh.price
454 {
455 let (b_type, b_dir) = match self.trend {
456 StructureTrend::Bullish => {
457 (StructureBreakType::Bos, StructureBreakDirection::Bullish)
458 }
459 StructureTrend::Bearish => {
460 self.trend = StructureTrend::Bullish;
461 (StructureBreakType::Choch, StructureBreakDirection::Bullish)
462 }
463 StructureTrend::Undetermined => {
464 self.trend = StructureTrend::Bullish;
465 (StructureBreakType::Choch, StructureBreakDirection::Bullish)
466 }
467 };
468 self.broken_high = true;
469 let event = StructureBreakEvent {
470 break_type: b_type,
471 direction: b_dir,
472 level: sh.price,
473 broken_swing_bar: sh.bar,
474 trigger_bar: curr_bar,
475 trigger_price: close,
476 };
477 triggered_break = Some(event);
478 self.last_break = Some(event);
479 }
480
481 if triggered_break.is_none()
483 && let Some(sl) = self.last_swing_low
484 && !self.broken_low
485 && close < sl.price
486 {
487 let (b_type, b_dir) = match self.trend {
488 StructureTrend::Bearish => {
489 (StructureBreakType::Bos, StructureBreakDirection::Bearish)
490 }
491 StructureTrend::Bullish => {
492 self.trend = StructureTrend::Bearish;
493 (StructureBreakType::Choch, StructureBreakDirection::Bearish)
494 }
495 StructureTrend::Undetermined => {
496 self.trend = StructureTrend::Bearish;
497 (StructureBreakType::Choch, StructureBreakDirection::Bearish)
498 }
499 };
500 self.broken_low = true;
501 let event = StructureBreakEvent {
502 break_type: b_type,
503 direction: b_dir,
504 level: sl.price,
505 broken_swing_bar: sl.bar,
506 trigger_bar: curr_bar,
507 trigger_price: close,
508 };
509 self.last_break = Some(event);
510 }
511
512 self.bar_index += 1;
513
514 ChochState {
515 trend: self.trend,
516 last_break: self.last_break,
517 last_swing_high: self.last_swing_high,
518 last_swing_low: self.last_swing_low,
519 bar_index: curr_bar,
520 }
521 }
522}
523
524pub fn choch_batch(inputs: &[(f64, f64, f64)], swing_strength: usize) -> Vec<ChochState> {
526 let mut detector = Choch::new(swing_strength);
527 detector.process_batch(inputs)
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
536pub enum OrderBlockType {
537 Bullish,
539 Bearish,
541}
542
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
545pub enum OrderBlockZoneMode {
546 Wick,
548 Body,
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
554pub struct OrderBlockZone {
555 pub id: u64,
557 pub block_type: OrderBlockType,
559 pub bar_index: usize,
561 pub trigger_bar: usize,
563 pub open: f64,
565 pub high: f64,
567 pub low: f64,
569 pub close: f64,
571 pub zone_high: f64,
573 pub zone_low: f64,
575 pub mitigation: MitigationStatus,
577 pub mitigation_bar: Option<usize>,
579}
580
581impl OrderBlockZone {
582 #[inline]
583 pub fn is_unmitigated(&self) -> bool {
584 self.mitigation == MitigationStatus::Unmitigated
585 }
586
587 #[inline]
588 pub fn is_partially_mitigated(&self) -> bool {
589 self.mitigation == MitigationStatus::PartiallyMitigated
590 }
591
592 #[inline]
593 pub fn is_fully_mitigated(&self) -> bool {
594 self.mitigation == MitigationStatus::FullyMitigated
595 }
596}
597
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
600pub struct OrderBlockState {
601 pub active_blocks: Vec<OrderBlockZone>,
603 pub latest_formed: Option<OrderBlockZone>,
605 pub bar_index: usize,
607}
608
609impl OrderBlockState {
610 #[inline]
612 pub fn current_formed(&self) -> Option<&OrderBlockZone> {
613 self.latest_formed
614 .as_ref()
615 .filter(|b| b.trigger_bar == self.bar_index)
616 }
617}
618
619#[derive(Debug, Clone)]
621pub struct OrderBlockConfig {
622 pub max_active_blocks: usize,
624 pub zone_mode: OrderBlockZoneMode,
626 pub min_displacement_ratio: f64,
628}
629
630impl Default for OrderBlockConfig {
631 fn default() -> Self {
632 Self {
633 max_active_blocks: 20,
634 zone_mode: OrderBlockZoneMode::Wick,
635 min_displacement_ratio: 0.0,
636 }
637 }
638}
639
640#[derive(Debug, Clone)]
642pub struct OrderBlock {
643 config: OrderBlockConfig,
644 active_blocks: Vec<OrderBlockZone>,
645 latest_formed: Option<OrderBlockZone>,
646 prev_candle: Option<(f64, f64, f64, f64, usize)>, bar_index: usize,
648 next_id: u64,
649}
650
651pub type OrderBlockDetector = OrderBlock;
652
653impl OrderBlock {
654 pub fn new(max_active_blocks: usize) -> Self {
656 Self::with_config(OrderBlockConfig {
657 max_active_blocks: max_active_blocks.max(1),
658 ..OrderBlockConfig::default()
659 })
660 }
661
662 pub fn with_config(config: OrderBlockConfig) -> Self {
664 let max_active = config.max_active_blocks.max(1);
665 Self {
666 config: OrderBlockConfig {
667 max_active_blocks: max_active,
668 ..config
669 },
670 active_blocks: Vec::with_capacity(max_active),
671 latest_formed: None,
672 prev_candle: None,
673 bar_index: 0,
674 next_id: 1,
675 }
676 }
677
678 pub fn reset(&mut self) {
680 self.active_blocks.clear();
681 self.latest_formed = None;
682 self.prev_candle = None;
683 self.bar_index = 0;
684 self.next_id = 1;
685 }
686
687 pub fn process_batch(&mut self, bars: &[(f64, f64, f64, f64)]) -> Vec<OrderBlockState> {
689 let mut results = Vec::with_capacity(bars.len());
690 for &bar in bars {
691 results.push(self.next(bar));
692 }
693 results
694 }
695}
696
697impl Default for OrderBlock {
698 fn default() -> Self {
699 Self::new(20)
700 }
701}
702
703impl Next<(f64, f64, f64, f64)> for OrderBlock {
704 type Output = OrderBlockState;
705
706 fn next(&mut self, input: (f64, f64, f64, f64)) -> Self::Output {
707 let (open, high, low, close) = input;
708
709 if !open.is_finite() || !high.is_finite() || !low.is_finite() || !close.is_finite() {
711 return OrderBlockState {
712 active_blocks: self.active_blocks.clone(),
713 latest_formed: self.latest_formed,
714 bar_index: self.bar_index,
715 };
716 }
717
718 let curr_bar = self.bar_index;
719
720 self.active_blocks
722 .retain(|b| b.mitigation != MitigationStatus::FullyMitigated);
723
724 for block in &mut self.active_blocks {
726 match block.block_type {
727 OrderBlockType::Bullish => {
728 if low <= block.zone_high {
729 if close < block.zone_low {
730 block.mitigation = MitigationStatus::FullyMitigated;
732 block.mitigation_bar = Some(curr_bar);
733 } else if block.mitigation == MitigationStatus::Unmitigated {
734 block.mitigation = MitigationStatus::PartiallyMitigated;
736 block.mitigation_bar = Some(curr_bar);
737 }
738 }
739 }
740 OrderBlockType::Bearish => {
741 if high >= block.zone_low {
742 if close > block.zone_high {
743 block.mitigation = MitigationStatus::FullyMitigated;
745 block.mitigation_bar = Some(curr_bar);
746 } else if block.mitigation == MitigationStatus::Unmitigated {
747 block.mitigation = MitigationStatus::PartiallyMitigated;
749 block.mitigation_bar = Some(curr_bar);
750 }
751 }
752 }
753 }
754 }
755
756 if let Some((p_open, p_high, p_low, p_close, p_bar)) = self.prev_candle {
758 let prev_range = (p_high - p_low).max(1e-12);
759
760 if p_close < p_open && close > open && close > p_high {
762 let displacement_body = close - open;
763 if displacement_body / prev_range >= self.config.min_displacement_ratio {
764 let (z_low, z_high) = match self.config.zone_mode {
765 OrderBlockZoneMode::Wick => (p_low, p_high),
766 OrderBlockZoneMode::Body => (p_close, p_open),
767 };
768
769 let new_block = OrderBlockZone {
770 id: self.next_id,
771 block_type: OrderBlockType::Bullish,
772 bar_index: p_bar,
773 trigger_bar: curr_bar,
774 open: p_open,
775 high: p_high,
776 low: p_low,
777 close: p_close,
778 zone_high: z_high,
779 zone_low: z_low,
780 mitigation: MitigationStatus::Unmitigated,
781 mitigation_bar: None,
782 };
783 self.next_id += 1;
784 self.latest_formed = Some(new_block);
785 self.active_blocks.push(new_block);
786 }
787 }
788 else if p_close > p_open && close < open && close < p_low {
790 let displacement_body = open - close;
791 if displacement_body / prev_range >= self.config.min_displacement_ratio {
792 let (z_low, z_high) = match self.config.zone_mode {
793 OrderBlockZoneMode::Wick => (p_low, p_high),
794 OrderBlockZoneMode::Body => (p_open, p_close),
795 };
796
797 let new_block = OrderBlockZone {
798 id: self.next_id,
799 block_type: OrderBlockType::Bearish,
800 bar_index: p_bar,
801 trigger_bar: curr_bar,
802 open: p_open,
803 high: p_high,
804 low: p_low,
805 close: p_close,
806 zone_high: z_high,
807 zone_low: z_low,
808 mitigation: MitigationStatus::Unmitigated,
809 mitigation_bar: None,
810 };
811 self.next_id += 1;
812 self.latest_formed = Some(new_block);
813 self.active_blocks.push(new_block);
814 }
815 }
816 }
817
818 while self.active_blocks.len() > self.config.max_active_blocks {
820 self.active_blocks.remove(0);
821 }
822
823 self.prev_candle = Some((open, high, low, close, curr_bar));
824 self.bar_index += 1;
825
826 OrderBlockState {
827 active_blocks: self.active_blocks.clone(),
828 latest_formed: self.latest_formed,
829 bar_index: curr_bar,
830 }
831 }
832}
833
834pub fn order_block_batch(
836 inputs: &[(f64, f64, f64, f64)],
837 max_active_blocks: usize,
838) -> Vec<OrderBlockState> {
839 let mut detector = OrderBlock::new(max_active_blocks);
840 detector.process_batch(inputs)
841}
842
843#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
849pub enum FvgDirection {
850 Bullish,
852 Bearish,
854}
855
856#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
858pub struct FairValueGapZone {
859 pub id: u64,
861 pub direction: FvgDirection,
863 pub bar_index: usize,
865 pub zone_high: f64,
867 pub zone_low: f64,
869 pub gap_size: f64,
871 pub mitigation: MitigationStatus,
873 pub mitigation_bar: Option<usize>,
875}
876
877impl FairValueGapZone {
878 #[inline]
879 pub fn is_unmitigated(&self) -> bool {
880 self.mitigation == MitigationStatus::Unmitigated
881 }
882
883 #[inline]
884 pub fn is_partially_mitigated(&self) -> bool {
885 self.mitigation == MitigationStatus::PartiallyMitigated
886 }
887
888 #[inline]
889 pub fn is_fully_mitigated(&self) -> bool {
890 self.mitigation == MitigationStatus::FullyMitigated
891 }
892}
893
894#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
896pub struct FvgState {
897 pub active_gaps: Vec<FairValueGapZone>,
899 pub latest_gap: Option<FairValueGapZone>,
901 pub bar_index: usize,
903}
904
905impl FvgState {
906 #[inline]
908 pub fn current_gap(&self) -> Option<&FairValueGapZone> {
909 self.latest_gap
910 .as_ref()
911 .filter(|g| g.bar_index == self.bar_index)
912 }
913}
914
915#[derive(Debug, Clone)]
917pub struct FairValueGap {
918 min_gap_size: f64,
919 max_active_gaps: usize,
920 active_gaps: Vec<FairValueGapZone>,
921 latest_gap: Option<FairValueGapZone>,
922 history: VecDeque<(f64, f64, f64, usize)>, bar_index: usize,
924 next_id: u64,
925}
926
927pub type FairValueGapDetector = FairValueGap;
928
929impl FairValueGap {
930 pub fn new(min_gap_size: f64, max_active_gaps: usize) -> Self {
935 let max_active = max_active_gaps.max(1);
936 Self {
937 min_gap_size: min_gap_size.max(0.0),
938 max_active_gaps: max_active,
939 active_gaps: Vec::with_capacity(max_active),
940 latest_gap: None,
941 history: VecDeque::with_capacity(3),
942 bar_index: 0,
943 next_id: 1,
944 }
945 }
946
947 pub fn reset(&mut self) {
949 self.active_gaps.clear();
950 self.latest_gap = None;
951 self.history.clear();
952 self.bar_index = 0;
953 self.next_id = 1;
954 }
955
956 pub fn process_batch(&mut self, bars: &[(f64, f64, f64)]) -> Vec<FvgState> {
958 let mut results = Vec::with_capacity(bars.len());
959 for &bar in bars {
960 results.push(self.next(bar));
961 }
962 results
963 }
964}
965
966impl Default for FairValueGap {
967 fn default() -> Self {
968 Self::new(0.0, 20)
969 }
970}
971
972impl Next<(f64, f64, f64)> for FairValueGap {
973 type Output = FvgState;
974
975 fn next(&mut self, input: (f64, f64, f64)) -> Self::Output {
976 let (high, low, close) = input;
977
978 if !high.is_finite() || !low.is_finite() || !close.is_finite() {
980 return FvgState {
981 active_gaps: self.active_gaps.clone(),
982 latest_gap: self.latest_gap,
983 bar_index: self.bar_index,
984 };
985 }
986
987 let curr_bar = self.bar_index;
988
989 self.active_gaps
991 .retain(|g| g.mitigation != MitigationStatus::FullyMitigated);
992
993 for gap in &mut self.active_gaps {
995 match gap.direction {
996 FvgDirection::Bullish => {
997 if low < gap.zone_high {
998 if low <= gap.zone_low {
999 gap.mitigation = MitigationStatus::FullyMitigated;
1001 gap.mitigation_bar = Some(curr_bar);
1002 } else if gap.mitigation == MitigationStatus::Unmitigated {
1003 gap.mitigation = MitigationStatus::PartiallyMitigated;
1005 gap.mitigation_bar = Some(curr_bar);
1006 }
1007 }
1008 }
1009 FvgDirection::Bearish => {
1010 if high > gap.zone_low {
1011 if high >= gap.zone_high {
1012 gap.mitigation = MitigationStatus::FullyMitigated;
1014 gap.mitigation_bar = Some(curr_bar);
1015 } else if gap.mitigation == MitigationStatus::Unmitigated {
1016 gap.mitigation = MitigationStatus::PartiallyMitigated;
1018 gap.mitigation_bar = Some(curr_bar);
1019 }
1020 }
1021 }
1022 }
1023 }
1024
1025 if self.history.len() >= 2 {
1027 let (h_prev2, l_prev2, _, _) = self.history[self.history.len() - 2];
1028
1029 if low > h_prev2 {
1031 let gap_size = low - h_prev2;
1032 if gap_size >= self.min_gap_size && gap_size > 0.0 {
1033 let new_gap = FairValueGapZone {
1034 id: self.next_id,
1035 direction: FvgDirection::Bullish,
1036 bar_index: curr_bar,
1037 zone_high: low,
1038 zone_low: h_prev2,
1039 gap_size,
1040 mitigation: MitigationStatus::Unmitigated,
1041 mitigation_bar: None,
1042 };
1043 self.next_id += 1;
1044 self.latest_gap = Some(new_gap);
1045 self.active_gaps.push(new_gap);
1046 }
1047 }
1048 else if high < l_prev2 {
1050 let gap_size = l_prev2 - high;
1051 if gap_size >= self.min_gap_size && gap_size > 0.0 {
1052 let new_gap = FairValueGapZone {
1053 id: self.next_id,
1054 direction: FvgDirection::Bearish,
1055 bar_index: curr_bar,
1056 zone_high: l_prev2,
1057 zone_low: high,
1058 gap_size,
1059 mitigation: MitigationStatus::Unmitigated,
1060 mitigation_bar: None,
1061 };
1062 self.next_id += 1;
1063 self.latest_gap = Some(new_gap);
1064 self.active_gaps.push(new_gap);
1065 }
1066 }
1067 }
1068
1069 while self.active_gaps.len() > self.max_active_gaps {
1071 self.active_gaps.remove(0);
1072 }
1073
1074 self.history.push_back((high, low, close, curr_bar));
1076 if self.history.len() > 2 {
1077 self.history.pop_front();
1078 }
1079
1080 self.bar_index += 1;
1081
1082 FvgState {
1083 active_gaps: self.active_gaps.clone(),
1084 latest_gap: self.latest_gap,
1085 bar_index: curr_bar,
1086 }
1087 }
1088}
1089
1090pub fn fair_value_gap_batch(
1092 inputs: &[(f64, f64, f64)],
1093 min_gap_size: f64,
1094 max_active_gaps: usize,
1095) -> Vec<FvgState> {
1096 let mut detector = FairValueGap::new(min_gap_size, max_active_gaps);
1097 detector.process_batch(inputs)
1098}
1099
1100#[cfg(test)]
1105mod tests {
1106 use super::*;
1107 use proptest::prelude::*;
1108
1109 #[test]
1110 fn test_choch_swing_detection_basic() {
1111 let mut choch = Choch::new(2);
1112
1113 let bars = vec![
1121 (100.0, 95.0, 98.0),
1122 (110.0, 105.0, 108.0),
1123 (120.0, 115.0, 118.0),
1124 (112.0, 107.0, 110.0),
1125 (108.0, 102.0, 105.0),
1126 ];
1127
1128 let mut states = Vec::new();
1129 for bar in bars {
1130 states.push(choch.next(bar));
1131 }
1132
1133 let state_4 = &states[4];
1134 assert!(state_4.last_swing_high.is_some());
1135 let sh = state_4.last_swing_high.unwrap();
1136 assert_eq!(sh.bar, 2);
1137 assert!((sh.price - 120.0).abs() < 1e-6);
1138 assert_eq!(sh.swing_type, SwingType::High);
1139 }
1140
1141 #[test]
1142 fn test_choch_bullish_bos_and_bearish_choch() {
1143 let mut choch = Choch::new(2);
1144
1145 let bars = vec![
1148 (100.0, 95.0, 98.0),
1149 (110.0, 105.0, 108.0),
1150 (120.0, 115.0, 118.0), (112.0, 107.0, 110.0),
1152 (108.0, 102.0, 105.0), ];
1154 for bar in bars {
1155 choch.next(bar);
1156 }
1157
1158 let state_5 = choch.next((126.0, 118.0, 125.0));
1161 assert_eq!(state_5.trend, StructureTrend::Bullish);
1162 assert!(state_5.last_break.is_some());
1163 let brk5 = state_5.last_break.unwrap();
1164 assert_eq!(brk5.direction, StructureBreakDirection::Bullish);
1165 assert_eq!(brk5.broken_swing_bar, 2);
1166 assert_eq!(brk5.trigger_bar, 5);
1167
1168 let bars_bull = vec![
1171 (130.0, 120.0, 128.0), (140.0, 130.0, 138.0), (135.0, 128.0, 132.0), (132.0, 124.0, 126.0), ];
1176 for bar in bars_bull {
1177 choch.next(bar);
1178 }
1179
1180 let state_10 = choch.next((146.0, 138.0, 145.0));
1182 assert_eq!(state_10.trend, StructureTrend::Bullish);
1183 let brk10 = state_10.last_break.unwrap();
1184 assert_eq!(brk10.break_type, StructureBreakType::Bos);
1185 assert_eq!(brk10.direction, StructureBreakDirection::Bullish);
1186 assert_eq!(brk10.broken_swing_bar, 7);
1187 assert_eq!(brk10.trigger_bar, 10);
1188
1189 let bars_reversal = vec![
1191 (144.0, 136.0, 140.0), (138.0, 125.0, 128.0), (135.0, 127.0, 132.0), (134.0, 126.0, 130.0), ];
1196 for bar in bars_reversal {
1197 choch.next(bar);
1198 }
1199
1200 let state_15 = choch.next((126.0, 118.0, 120.0));
1202 assert_eq!(state_15.trend, StructureTrend::Bearish);
1203 let brk15 = state_15.last_break.unwrap();
1204 assert_eq!(brk15.break_type, StructureBreakType::Choch);
1205 assert_eq!(brk15.direction, StructureBreakDirection::Bearish);
1206 assert_eq!(brk15.broken_swing_bar, 12);
1207 assert_eq!(brk15.trigger_bar, 15);
1208 }
1209
1210 #[test]
1211 fn test_order_block_bullish_and_bearish_lifecycle() {
1212 let mut ob = OrderBlock::new(10);
1213
1214 let s0 = ob.next((100.0, 102.0, 95.0, 96.0));
1216 assert!(s0.active_blocks.is_empty());
1217
1218 let s1 = ob.next((97.0, 110.0, 96.0, 108.0));
1221 assert_eq!(s1.active_blocks.len(), 1);
1222 let active = &s1.active_blocks[0];
1223 assert_eq!(active.block_type, OrderBlockType::Bullish);
1224 assert_eq!(active.bar_index, 0);
1225 assert_eq!(active.trigger_bar, 1);
1226 assert!((active.zone_high - 102.0).abs() < 1e-6);
1227 assert!((active.zone_low - 95.0).abs() < 1e-6);
1228 assert_eq!(active.mitigation, MitigationStatus::Unmitigated);
1229
1230 let s2 = ob.next((107.0, 108.0, 100.0, 101.0));
1233 assert_eq!(s2.active_blocks.len(), 1);
1234 assert_eq!(
1235 s2.active_blocks[0].mitigation,
1236 MitigationStatus::PartiallyMitigated
1237 );
1238 assert_eq!(s2.active_blocks[0].mitigation_bar, Some(2));
1239
1240 let s3 = ob.next((101.0, 102.0, 91.0, 92.0));
1243 assert_eq!(s3.active_blocks.len(), 1);
1244 assert_eq!(
1245 s3.active_blocks[0].mitigation,
1246 MitigationStatus::FullyMitigated
1247 );
1248 assert_eq!(s3.active_blocks[0].mitigation_bar, Some(3));
1249
1250 let s4 = ob.next((92.0, 94.0, 89.0, 90.0));
1252 assert!(s4.active_blocks.is_empty());
1253 }
1254
1255 #[test]
1256 fn test_order_block_bearish_formation_and_mitigation() {
1257 let mut ob = OrderBlock::new(10);
1258
1259 ob.next((100.0, 106.0, 99.0, 105.0));
1261
1262 let s1 = ob.next((104.0, 105.0, 90.0, 92.0));
1265 assert_eq!(s1.active_blocks.len(), 1);
1266 let block = &s1.active_blocks[0];
1267 assert_eq!(block.block_type, OrderBlockType::Bearish);
1268 assert_eq!(block.bar_index, 0);
1269 assert!((block.zone_low - 99.0).abs() < 1e-6);
1270 assert!((block.zone_high - 106.0).abs() < 1e-6);
1271 assert_eq!(block.mitigation, MitigationStatus::Unmitigated);
1272
1273 let s2 = ob.next((93.0, 101.0, 92.0, 100.0));
1275 assert_eq!(
1276 s2.active_blocks[0].mitigation,
1277 MitigationStatus::PartiallyMitigated
1278 );
1279
1280 let s3 = ob.next((100.0, 110.0, 99.0, 108.0));
1282 assert_eq!(
1283 s3.active_blocks[0].mitigation,
1284 MitigationStatus::FullyMitigated
1285 );
1286
1287 let s4 = ob.next((108.0, 112.0, 107.0, 111.0));
1289 assert!(s4.active_blocks.is_empty());
1290 }
1291
1292 #[test]
1293 fn test_fair_value_gap_bullish_and_bearish() {
1294 let mut fvg = FairValueGap::new(0.0, 10);
1295
1296 let s0 = fvg.next((100.0, 90.0, 95.0));
1302 assert!(s0.active_gaps.is_empty());
1303
1304 let s1 = fvg.next((120.0, 98.0, 118.0));
1305 assert!(s1.active_gaps.is_empty());
1306
1307 let s2 = fvg.next((130.0, 105.0, 128.0));
1308 assert_eq!(s2.active_gaps.len(), 1);
1309 let gap = &s2.active_gaps[0];
1310 assert_eq!(gap.direction, FvgDirection::Bullish);
1311 assert!((gap.zone_low - 100.0).abs() < 1e-6);
1312 assert!((gap.zone_high - 105.0).abs() < 1e-6);
1313 assert!((gap.gap_size - 5.0).abs() < 1e-6);
1314 assert_eq!(gap.mitigation, MitigationStatus::Unmitigated);
1315
1316 let s3 = fvg.next((125.0, 102.0, 120.0));
1318 assert_eq!(s3.active_gaps.len(), 1);
1319 assert_eq!(
1320 s3.active_gaps[0].mitigation,
1321 MitigationStatus::PartiallyMitigated
1322 );
1323 assert_eq!(s3.active_gaps[0].mitigation_bar, Some(3));
1324
1325 let s4 = fvg.next((120.0, 98.0, 105.0));
1327 assert_eq!(s4.active_gaps.len(), 1);
1328 assert_eq!(
1329 s4.active_gaps[0].mitigation,
1330 MitigationStatus::FullyMitigated
1331 );
1332
1333 let s5 = fvg.next((110.0, 104.0, 108.0));
1335 assert!(s5.active_gaps.is_empty());
1336
1337 fvg.next((106.0, 85.0, 88.0)); let s7 = fvg.next((101.0, 80.0, 82.0)); assert_eq!(s7.active_gaps.len(), 1);
1345 let b_gap = &s7.active_gaps[0];
1346 assert_eq!(b_gap.direction, FvgDirection::Bearish);
1347 assert!((b_gap.zone_low - 101.0).abs() < 1e-6);
1348 assert!((b_gap.zone_high - 104.0).abs() < 1e-6);
1349 assert_eq!(b_gap.mitigation, MitigationStatus::Unmitigated);
1350
1351 let s8 = fvg.next((102.0, 85.0, 90.0));
1353 assert_eq!(
1354 s8.active_gaps[0].mitigation,
1355 MitigationStatus::PartiallyMitigated
1356 );
1357
1358 let s9 = fvg.next((105.0, 88.0, 95.0));
1360 assert_eq!(
1361 s9.active_gaps[0].mitigation,
1362 MitigationStatus::FullyMitigated
1363 );
1364 }
1365
1366 #[test]
1367 fn test_batch_equivalence() {
1368 let bars = vec![
1369 (100.0, 90.0, 95.0),
1370 (110.0, 95.0, 108.0),
1371 (120.0, 105.0, 115.0),
1372 (115.0, 100.0, 102.0),
1373 (105.0, 92.0, 94.0),
1374 (130.0, 102.0, 128.0),
1375 ];
1376
1377 let mut streaming = Choch::new(2);
1378 let streaming_results: Vec<_> = bars.iter().map(|&b| streaming.next(b)).collect();
1379 let batch_results = choch_batch(&bars, 2);
1380
1381 assert_eq!(streaming_results, batch_results);
1382
1383 let ohlc_bars = vec![
1384 (100.0, 102.0, 95.0, 96.0),
1385 (97.0, 110.0, 96.0, 108.0),
1386 (107.0, 108.0, 100.0, 101.0),
1387 (101.0, 102.0, 91.0, 92.0),
1388 ];
1389
1390 let mut ob_streaming = OrderBlock::new(10);
1391 let ob_stream_results: Vec<_> = ohlc_bars.iter().map(|&b| ob_streaming.next(b)).collect();
1392 let ob_batch_results = order_block_batch(&ohlc_bars, 10);
1393
1394 assert_eq!(ob_stream_results, ob_batch_results);
1395
1396 let mut fvg_streaming = FairValueGap::new(0.0, 10);
1397 let fvg_stream_results: Vec<_> = bars.iter().map(|&b| fvg_streaming.next(b)).collect();
1398 let fvg_batch_results = fair_value_gap_batch(&bars, 0.0, 10);
1399
1400 assert_eq!(fvg_stream_results, fvg_batch_results);
1401 }
1402
1403 proptest! {
1404 #[test]
1405 fn test_choch_parity(
1406 highs in prop::collection::vec(10.0..500.0f64, 10..80),
1407 low_offsets in prop::collection::vec(1.0..10.0f64, 10..80),
1408 close_pcts in prop::collection::vec(0.0..1.0f64, 10..80),
1409 ) {
1410 let n = highs.len().min(low_offsets.len()).min(close_pcts.len());
1411 let mut bars = Vec::with_capacity(n);
1412 for i in 0..n {
1413 let h = highs[i];
1414 let l = (h - low_offsets[i]).max(0.1);
1415 let c = l + (h - l) * close_pcts[i];
1416 bars.push((h, l, c));
1417 }
1418
1419 let mut streaming = Choch::new(3);
1420 let mut stream_outs = Vec::with_capacity(n);
1421 for &bar in &bars {
1422 stream_outs.push(streaming.next(bar));
1423 }
1424
1425 let batch_outs = choch_batch(&bars, 3);
1426 prop_assert_eq!(stream_outs, batch_outs);
1427 }
1428
1429 #[test]
1430 fn test_order_block_parity(
1431 prices in prop::collection::vec((10.0..500.0f64, 10.0..500.0f64, 10.0..500.0f64, 10.0..500.0f64), 5..60),
1432 ) {
1433 let mut ohlc = Vec::with_capacity(prices.len());
1434 for (p0, p1, p2, p3) in prices {
1435 let open = p0;
1436 let close = p1;
1437 let high = open.max(close).max(p2);
1438 let low = open.min(close).min(p3).max(0.01);
1439 ohlc.push((open, high, low, close));
1440 }
1441
1442 let mut streaming = OrderBlock::new(15);
1443 let stream_outs: Vec<_> = ohlc.iter().map(|&b| streaming.next(b)).collect();
1444 let batch_outs = order_block_batch(&ohlc, 15);
1445
1446 prop_assert_eq!(stream_outs, batch_outs);
1447 }
1448
1449 #[test]
1450 fn test_fair_value_gap_parity(
1451 highs in prop::collection::vec(10.0..500.0f64, 5..60),
1452 low_offsets in prop::collection::vec(1.0..10.0f64, 5..60),
1453 close_pcts in prop::collection::vec(0.0..1.0f64, 5..60),
1454 ) {
1455 let n = highs.len().min(low_offsets.len()).min(close_pcts.len());
1456 let mut bars = Vec::with_capacity(n);
1457 for i in 0..n {
1458 let h = highs[i];
1459 let l = (h - low_offsets[i]).max(0.1);
1460 let c = l + (h - l) * close_pcts[i];
1461 bars.push((h, l, c));
1462 }
1463
1464 let mut streaming = FairValueGap::new(0.5, 10);
1465 let stream_outs: Vec<_> = bars.iter().map(|&b| streaming.next(b)).collect();
1466 let batch_outs = fair_value_gap_batch(&bars, 0.5, 10);
1467
1468 prop_assert_eq!(stream_outs, batch_outs);
1469 }
1470 }
1471}