use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::core::signal::kind::StructureSub;
use crate::core::signal::{Direction, SignalKind};
use std::collections::VecDeque;
#[derive(Clone, Debug)]
pub struct FvgEventDetector {
bars: VecDeque<(f64, f64)>,
last_signal: i8,
}
impl FvgEventDetector {
pub fn new() -> Self {
Self {
bars: VecDeque::with_capacity(3),
last_signal: 0,
}
}
pub fn detect_from_values(&mut self, high: f64, low: f64) -> Option<(SignalKind, Direction)> {
self.bars.push_back((high, low));
if self.bars.len() > 3 {
self.bars.pop_front();
}
if self.bars.len() < 3 {
self.last_signal = 0;
return None;
}
let (h0, l0) = self.bars[0]; let (h1, l1) = self.bars[1]; let (h2, l2) = self.bars[2];
let bull = l1 > h0 && l1 > h2;
let bear = h1 < l0 && h1 < l2;
if bull {
self.last_signal = 1;
Some((SignalKind::Structure(StructureSub::FVG), Direction::Up))
} else if bear {
self.last_signal = -1;
Some((SignalKind::Structure(StructureSub::FVG), Direction::Down))
} else {
self.last_signal = 0;
None
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_triplet(
&mut self,
_o0: f64, h0: f64, l0: f64, _c0: f64,
_o1: f64, h1: f64, l1: f64, _c1: f64,
_o2: f64, h2: f64, l2: f64, _c2: f64,
) -> (bool, bool) {
let bull = l1 > h0 && l1 > h2;
let bear = h1 < l0 && h1 < l2;
self.last_signal = if bull { 1 } else if bear { -1 } else { 0 };
(bull, bear)
}
pub fn update_bar(&mut self, _o: f64, h: f64, l: f64, _c: f64, _v: f64) -> IndicatorValue {
match self.detect_from_values(h, l) {
Some((_, Direction::Up)) => IndicatorValue::Signal(1),
Some((_, Direction::Down)) => IndicatorValue::Signal(-1),
_ => IndicatorValue::Signal(0),
}
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Signal(self.last_signal)
}
pub fn is_ready(&self) -> bool {
self.bars.len() == 3
}
pub fn reset(&mut self) {
self.bars.clear();
self.last_signal = 0;
}
}
impl Default for FvgEventDetector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bull_fvg_detected() {
let mut det = FvgEventDetector::new();
det.detect_from_values(100.0, 98.0);
det.detect_from_values(108.0, 105.0);
let result = det.detect_from_values(102.0, 100.0);
assert_eq!(
result,
Some((SignalKind::Structure(StructureSub::FVG), Direction::Up)),
"should detect bullish FVG"
);
}
#[test]
fn bear_fvg_detected() {
let mut det = FvgEventDetector::new();
det.detect_from_values(102.0, 100.0);
det.detect_from_values(95.0, 90.0);
let result = det.detect_from_values(100.0, 98.0);
assert_eq!(
result,
Some((SignalKind::Structure(StructureSub::FVG), Direction::Down)),
"should detect bearish FVG"
);
}
#[test]
fn no_gap_returns_none() {
let mut det = FvgEventDetector::new();
det.detect_from_values(102.0, 99.0);
det.detect_from_values(103.0, 100.0);
let result = det.detect_from_values(104.0, 101.0);
assert_eq!(result, None, "overlapping bars should produce no FVG");
}
#[test]
fn not_ready_until_three_bars() {
let mut det = FvgEventDetector::new();
assert!(!det.is_ready());
det.detect_from_values(100.0, 99.0);
assert!(!det.is_ready());
det.detect_from_values(101.0, 100.0);
assert!(!det.is_ready());
det.detect_from_values(102.0, 101.0);
assert!(det.is_ready());
}
#[test]
fn reset_clears_state() {
let mut det = FvgEventDetector::new();
det.detect_from_values(100.0, 98.0);
det.detect_from_values(108.0, 105.0);
det.detect_from_values(102.0, 100.0);
det.reset();
assert!(!det.is_ready());
assert_eq!(det.value(), IndicatorValue::Signal(0));
}
}