use std::fmt;
use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::instance_factory::IndicatorInstance;
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, Eq)]
pub enum CrossMode {
Momentary,
Sticky,
}
#[derive(Clone)]
pub struct LineCross {
left: LineSource,
right: LineSource,
mode: CrossMode,
prev_left: f64,
prev_right: f64,
has_prev: bool,
last_sticky: i8,
}
impl fmt::Debug for LineCross {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LineCross")
.field("left", &self.left)
.field("right", &self.right)
.field("mode", &self.mode)
.field("prev_left", &self.prev_left)
.field("prev_right", &self.prev_right)
.field("has_prev", &self.has_prev)
.field("last_sticky", &self.last_sticky)
.finish()
}
}
impl LineCross {
pub fn new(left: LineSource, right: LineSource, mode: CrossMode) -> Self {
Self {
left,
right,
mode,
prev_left: 0.0,
prev_right: 0.0,
has_prev: false,
last_sticky: 0,
}
}
pub fn update_bar(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> IndicatorValue {
let a = match &mut self.left {
LineSource::Indicator(b) => b.update_bar(open, high, low, close, volume, None).main(),
LineSource::Constant(k) => *k,
};
let b = match &mut self.right {
LineSource::Indicator(b) => b.update_bar(open, high, low, close, volume, None).main(),
LineSource::Constant(k) => *k,
};
let crossover_signal: i8 = if self.has_prev {
if self.prev_left <= self.prev_right && a > b {
1
} else if self.prev_left >= self.prev_right && a < b {
-1
} else {
0
}
} else {
0
};
self.prev_left = a;
self.prev_right = b;
self.has_prev = true;
let out = match self.mode {
CrossMode::Momentary => crossover_signal,
CrossMode::Sticky => {
if crossover_signal != 0 {
self.last_sticky = crossover_signal;
}
self.last_sticky
}
};
IndicatorValue::Triple(a, b, out as f64)
}
pub fn value(&self) -> IndicatorValue {
let signal = match self.mode {
CrossMode::Momentary => 0i8,
CrossMode::Sticky => self.last_sticky,
};
IndicatorValue::Triple(self.prev_left, self.prev_right, signal as f64)
}
pub fn is_ready(&self) -> bool {
let left_ready = match &self.left {
LineSource::Indicator(b) => b.is_ready(),
LineSource::Constant(_) => true,
};
let right_ready = match &self.right {
LineSource::Indicator(b) => b.is_ready(),
LineSource::Constant(_) => true,
};
self.has_prev && left_ready && right_ready
}
pub fn reset(&mut self) {
if let LineSource::Indicator(b) = &mut self.left {
b.reset();
}
if let LineSource::Indicator(b) = &mut self.right {
b.reset();
}
self.prev_left = 0.0;
self.prev_right = 0.0;
self.has_prev = false;
self.last_sticky = 0;
}
}
#[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 feed(lc: &mut LineCross, prices: &[f64]) {
for &p in prices {
lc.update_bar(p, p, p, p, 0.0);
}
}
fn signal_of(v: IndicatorValue) -> f64 {
match v {
IndicatorValue::Triple(_, _, s) => s,
_ => panic!("expected Triple"),
}
}
#[test]
fn two_constants_no_cross() {
let mut lc = LineCross::new(
LineSource::Constant(5.0),
LineSource::Constant(10.0),
CrossMode::Momentary,
);
for _ in 0..10 {
let v = lc.update_bar(0.0, 0.0, 0.0, 0.0, 0.0);
assert_eq!(signal_of(v), 0.0, "constants never cross");
}
}
#[test]
fn constant_vs_indicator_cross_up() {
let mut lc = LineCross::new(
LineSource::Indicator(Box::new(make_sma(3))),
LineSource::Constant(100.0),
CrossMode::Momentary,
);
feed(&mut lc, &[90.0, 92.0, 94.0]);
let mut saw_cross = false;
for _ in 0..10 {
let v = lc.update_bar(120.0, 120.0, 120.0, 120.0, 0.0);
if signal_of(v) > 0.0 {
saw_cross = true;
}
}
assert!(saw_cross, "SMA must cross above constant 100 when prices surge to 120");
}
#[test]
fn sticky_mode_holds_sign() {
let mut lc = LineCross::new(
LineSource::Indicator(Box::new(make_sma(3))),
LineSource::Constant(100.0),
CrossMode::Sticky,
);
feed(&mut lc, &[90.0, 90.0, 90.0, 90.0]);
feed(&mut lc, &[120.0, 120.0]);
let v = lc.update_bar(120.0, 120.0, 120.0, 120.0, 0.0);
assert_eq!(signal_of(v), 1.0, "sticky should hold +1 after up-cross");
feed(&mut lc, &[50.0, 50.0, 50.0, 50.0, 50.0]);
let v2 = lc.update_bar(50.0, 50.0, 50.0, 50.0, 0.0);
assert_eq!(signal_of(v2), -1.0, "sticky should flip to -1 after down-cross");
}
#[test]
fn momentary_zeroes_after_cross() {
let mut lc = LineCross::new(
LineSource::Indicator(Box::new(make_sma(3))),
LineSource::Constant(100.0),
CrossMode::Momentary,
);
feed(&mut lc, &[90.0, 90.0, 90.0]);
feed(&mut lc, &[120.0, 120.0]);
let v = lc.update_bar(120.0, 120.0, 120.0, 120.0, 0.0);
assert_eq!(signal_of(v), 0.0, "momentary returns 0 bars after the crossover");
}
#[test]
fn reset_clears_state() {
let mut lc = LineCross::new(
LineSource::Indicator(Box::new(make_sma(3))),
LineSource::Constant(50.0),
CrossMode::Sticky,
);
feed(&mut lc, &[80.0; 10]);
lc.reset();
assert!(!lc.is_ready());
assert_eq!(signal_of(lc.value()), 0.0);
}
}