use std::fmt;
use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::instance_factory::IndicatorInstance;
use crate::events::candle_pattern::CandlePatternKind;
pub enum LineSource {
Indicator(Box<IndicatorInstance>),
Constant(f64),
}
impl Clone for LineSource {
fn clone(&self) -> Self {
match self {
LineSource::Indicator(b) => LineSource::Indicator(b.clone()),
LineSource::Constant(k) => LineSource::Constant(*k),
}
}
}
impl fmt::Debug for LineSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LineSource::Indicator(_) => write!(f, "LineSource::Indicator(...)"),
LineSource::Constant(k) => write!(f, "LineSource::Constant({k})"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TouchMode {
CloseAbove,
CloseBelow,
WickThrough,
WickReject,
Touch {
tolerance: f64,
},
WithCandle(CandlePatternKind),
}
#[derive(Clone)]
pub struct PriceLineCross {
line: LineSource,
mode: TouchMode,
prev_close_above: Option<bool>,
last_signal: i8,
candle_detector: Option<crate::events::candle_pattern::CandlePatternDetector>,
}
impl fmt::Debug for PriceLineCross {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PriceLineCross")
.field("line", &self.line)
.field("mode", &self.mode)
.field("prev_close_above", &self.prev_close_above)
.field("last_signal", &self.last_signal)
.finish()
}
}
impl PriceLineCross {
pub fn new(line: LineSource, mode: TouchMode) -> Self {
let candle_detector = if let TouchMode::WithCandle(kind) = mode {
Some(crate::events::candle_pattern::CandlePatternDetector::new(kind))
} else {
None
};
Self {
line,
mode,
prev_close_above: None,
last_signal: 0,
candle_detector,
}
}
pub fn update_bar(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> IndicatorValue {
let line_val = match &mut self.line {
LineSource::Indicator(b) => b.update_bar(open, high, low, close, volume, None).main(),
LineSource::Constant(k) => *k,
};
let line_ready = match &self.line {
LineSource::Indicator(b) => b.is_ready(),
LineSource::Constant(_) => true,
};
let signal: i8 = if !line_ready {
0
} else {
match self.mode {
TouchMode::CloseAbove => {
match self.prev_close_above {
Some(false) if close > line_val => 1,
_ => 0,
}
}
TouchMode::CloseBelow => {
match self.prev_close_above {
Some(true) if close < line_val => -1,
_ => 0,
}
}
TouchMode::WickThrough => {
if low <= line_val && high >= line_val {
if close >= line_val { 1 } else { -1 }
} else if high > line_val && matches!(self.prev_close_above, Some(false)) {
1
} else if low < line_val && matches!(self.prev_close_above, Some(true)) {
-1
} else {
0
}
}
TouchMode::WickReject => {
let bullish = low < line_val
&& close > line_val
&& matches!(self.prev_close_above, Some(true) | None);
let bearish = high > line_val
&& close < line_val
&& matches!(self.prev_close_above, Some(false) | None);
if bullish { 1 } else if bearish { -1 } else { 0 }
}
TouchMode::Touch { tolerance } => {
let near_high = (high - line_val).abs() <= tolerance;
let near_low = (low - line_val).abs() <= tolerance;
if near_high || near_low { 1 } else { 0 }
}
TouchMode::WithCandle(_) => {
let crossed = match self.prev_close_above {
Some(false) if close > line_val => true,
Some(true) if close < line_val => true,
_ => false,
};
let pattern_match = if let Some(ref mut det) = self.candle_detector {
det.detect_from_values(open, high, low, close).is_some()
} else {
false
};
if crossed && pattern_match {
if close > line_val { 1 } else { -1 }
} else {
0
}
}
}
};
self.prev_close_above = Some(close > line_val);
self.last_signal = signal;
IndicatorValue::Triple(line_val, close, signal as f64)
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Triple(0.0, 0.0, self.last_signal as f64)
}
pub fn is_ready(&self) -> bool {
match &self.line {
LineSource::Indicator(b) => b.is_ready(),
LineSource::Constant(_) => self.prev_close_above.is_some(),
}
}
pub fn reset(&mut self) {
if let LineSource::Indicator(b) = &mut self.line {
b.reset();
}
self.prev_close_above = None;
self.last_signal = 0;
if let Some(ref mut det) = self.candle_detector {
det.reset();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bar_indicators::bar_indicator_id::BarIndicatorId;
use crate::bar_indicators::instance_factory::{IndicatorConfig, IndicatorInstance};
fn make_sma(period: usize) -> IndicatorInstance {
let cfg = IndicatorConfig::new(BarIndicatorId::Sma, "Sma".into(), vec![period]);
IndicatorInstance::create(&cfg).expect("SMA factory")
}
fn signal_of(v: IndicatorValue) -> f64 {
match v {
IndicatorValue::Triple(_, _, s) => s,
_ => panic!("expected Triple"),
}
}
fn feed(plc: &mut PriceLineCross, prices: &[f64]) {
for &p in prices {
plc.update_bar(p, p, p, p, 0.0);
}
}
#[test]
fn close_above_fires_on_transition() {
let mut plc = PriceLineCross::new(LineSource::Constant(100.0), TouchMode::CloseAbove);
feed(&mut plc, &[90.0, 90.0, 90.0]);
let v = plc.update_bar(100.0, 105.0, 99.0, 105.0, 0.0);
assert_eq!(signal_of(v), 1.0, "CloseAbove must fire +1 on up-transition");
let v2 = plc.update_bar(105.0, 106.0, 104.0, 105.0, 0.0);
assert_eq!(signal_of(v2), 0.0, "CloseAbove must not repeat while above");
}
#[test]
fn close_above_no_signal_when_already_above() {
let mut plc = PriceLineCross::new(LineSource::Constant(100.0), TouchMode::CloseAbove);
feed(&mut plc, &[110.0; 5]);
let v = plc.update_bar(110.0, 111.0, 109.0, 110.0, 0.0);
assert_eq!(signal_of(v), 0.0, "CloseAbove: no signal when always above");
}
#[test]
fn close_below_fires_on_transition() {
let mut plc = PriceLineCross::new(LineSource::Constant(100.0), TouchMode::CloseBelow);
feed(&mut plc, &[110.0, 110.0, 110.0]);
let v = plc.update_bar(100.0, 101.0, 95.0, 95.0, 0.0);
assert_eq!(signal_of(v), -1.0, "CloseBelow must fire -1 on down-transition");
let v2 = plc.update_bar(95.0, 96.0, 94.0, 95.0, 0.0);
assert_eq!(signal_of(v2), 0.0, "CloseBelow must not repeat while below");
}
#[test]
fn wick_through_high_fires() {
let mut plc = PriceLineCross::new(LineSource::Constant(100.0), TouchMode::WickThrough);
feed(&mut plc, &[90.0; 3]);
let v = plc.update_bar(90.0, 110.0, 89.0, 91.0, 0.0);
assert_eq!(signal_of(v), -1.0, "bar below with high spike: straddle resolves bearish (close < line)");
}
#[test]
fn wick_through_low_fires() {
let mut plc = PriceLineCross::new(LineSource::Constant(100.0), TouchMode::WickThrough);
feed(&mut plc, &[110.0; 3]);
let v = plc.update_bar(110.0, 111.0, 95.0, 109.0, 0.0);
assert_eq!(signal_of(v), 1.0, "bar above with low spike: straddle resolves bullish (close > line)");
}
#[test]
fn wick_reject_bullish() {
let mut plc = PriceLineCross::new(LineSource::Constant(100.0), TouchMode::WickReject);
feed(&mut plc, &[110.0; 3]); let v = plc.update_bar(108.0, 112.0, 95.0, 105.0, 0.0);
assert_eq!(signal_of(v), 1.0, "bullish wick reject: low below line, close above");
}
#[test]
fn wick_reject_bearish() {
let mut plc = PriceLineCross::new(LineSource::Constant(100.0), TouchMode::WickReject);
feed(&mut plc, &[90.0; 3]); let v = plc.update_bar(92.0, 115.0, 91.0, 95.0, 0.0);
assert_eq!(signal_of(v), -1.0, "bearish wick reject: high above line, close below");
}
#[test]
fn touch_fires_within_tolerance() {
let mut plc = PriceLineCross::new(
LineSource::Constant(100.0),
TouchMode::Touch { tolerance: 2.0 },
);
let v = plc.update_bar(98.0, 101.5, 97.0, 98.5, 0.0);
assert_eq!(signal_of(v), 1.0, "Touch: bar within tolerance must fire");
}
#[test]
fn touch_no_fire_outside_tolerance() {
let mut plc = PriceLineCross::new(
LineSource::Constant(100.0),
TouchMode::Touch { tolerance: 1.0 },
);
let v = plc.update_bar(95.0, 97.0, 93.0, 94.0, 0.0);
assert_eq!(signal_of(v), 0.0, "Touch: bar outside tolerance must not fire");
}
#[test]
fn with_candle_hammer_requires_cross() {
let mut plc = PriceLineCross::new(
LineSource::Constant(100.0),
TouchMode::WithCandle(CandlePatternKind::Hammer),
);
feed(&mut plc, &[90.0; 3]);
let v = plc.update_bar(99.0, 100.6, 90.0, 100.5, 0.0);
assert_eq!(signal_of(v), 1.0, "WithCandle(Hammer): must fire +1 when hammer + cross");
}
#[test]
fn with_candle_no_pattern_no_fire() {
let mut plc = PriceLineCross::new(
LineSource::Constant(100.0),
TouchMode::WithCandle(CandlePatternKind::Hammer),
);
feed(&mut plc, &[90.0; 3]);
let v = plc.update_bar(90.0, 110.0, 89.0, 110.0, 0.0);
assert_eq!(signal_of(v), 0.0, "WithCandle: must not fire without pattern match");
}
#[test]
fn with_indicator_line_close_above() {
let mut plc = PriceLineCross::new(
LineSource::Indicator(Box::new(make_sma(5))),
TouchMode::CloseAbove,
);
for _ in 0..10 {
plc.update_bar(95.0, 95.0, 95.0, 95.0, 0.0);
}
let mut fired = false;
for _ in 0..10 {
let v = plc.update_bar(120.0, 120.0, 120.0, 120.0, 0.0);
if signal_of(v) > 0.0 { fired = true; }
}
assert!(fired, "CloseAbove with SMA line: must fire when price surges above SMA");
}
#[test]
fn reset_clears_state() {
let mut plc = PriceLineCross::new(LineSource::Constant(100.0), TouchMode::CloseAbove);
feed(&mut plc, &[110.0; 5]);
plc.reset();
assert!(!plc.is_ready());
assert_eq!(signal_of(plc.value()), 0.0);
}
}