use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::instance_factory::IndicatorInstance;
use crate::core::signal::direction::Direction;
use crate::core::signal::kind::{SignalKind, TrendSub};
#[derive(Clone)]
pub struct RelativePosition {
subject: Box<IndicatorInstance>,
reference: Box<IndicatorInstance>,
last_trend: i8,
ready: bool,
}
impl std::fmt::Debug for RelativePosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RelativePosition")
.field("last_trend", &self.last_trend)
.field("ready", &self.ready)
.finish()
}
}
impl RelativePosition {
pub fn new(subject: IndicatorInstance, reference: IndicatorInstance) -> Self {
Self {
subject: Box::new(subject),
reference: Box::new(reference),
last_trend: 0,
ready: false,
}
}
pub fn update_bar(&mut self, open: f64, high: f64, low: f64, close: f64, volume: f64) -> i8 {
let s = self
.subject
.update_bar(open, high, low, close, volume, None)
.main();
let r = self
.reference
.update_bar(open, high, low, close, volume, None)
.main();
if self.subject.is_ready() && self.reference.is_ready() {
let new_trend = if s > r { 1 } else if s < r { -1 } else { 0 };
if new_trend != 0 && new_trend != self.last_trend {
self.last_trend = new_trend;
}
self.ready = true;
}
self.last_trend
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Signal(self.last_trend)
}
pub fn detect(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> Option<(SignalKind, Direction)> {
self.update_bar(open, high, low, close, volume);
if !self.ready {
return None;
}
match self.last_trend {
1 => Some((SignalKind::Trend(TrendSub::MaCross), Direction::Up)),
-1 => Some((SignalKind::Trend(TrendSub::MaCross), Direction::Down)),
_ => None,
}
}
pub fn is_ready(&self) -> bool {
self.ready
}
pub fn reset(&mut self) {
self.subject.reset();
self.reference.reset();
self.last_trend = 0;
self.ready = false;
}
pub fn detect_from_values(
&mut self,
subject: f64,
reference: f64,
) -> Option<(SignalKind, Direction)> {
let new_trend = if subject > reference {
1i8
} else if subject < reference {
-1
} else {
0
};
if new_trend != 0 && new_trend != self.last_trend {
self.last_trend = new_trend;
}
self.ready = true;
match self.last_trend {
1 => Some((SignalKind::Trend(TrendSub::MaCross), Direction::Up)),
-1 => Some((SignalKind::Trend(TrendSub::MaCross), Direction::Down)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bar_indicators::bar_indicator_id::BarIndicatorId;
use crate::bar_indicators::instance_factory::{IndicatorConfig, IndicatorInstance};
fn sma(period: usize) -> IndicatorInstance {
let cfg = IndicatorConfig::new(BarIndicatorId::Sma, "Sma".into(), vec![period]);
IndicatorInstance::create(&cfg).unwrap()
}
fn ema(period: usize) -> IndicatorInstance {
let cfg = IndicatorConfig::new(BarIndicatorId::Ema, "Ema".into(), vec![period]);
IndicatorInstance::create(&cfg).unwrap()
}
#[test]
fn uptrend_state_holds_plus_one() {
let mut rp = RelativePosition::new(sma(5), sma(20));
for i in 1..=50 {
let p = 100.0 + i as f64 * 2.0;
let _ = rp.update_bar(p, p, p, p, 0.0);
}
assert!(rp.is_ready());
assert_eq!(rp.value(), IndicatorValue::Signal(1));
}
#[test]
fn downtrend_state_holds_minus_one() {
let mut rp = RelativePosition::new(sma(5), sma(20));
for i in 1..=50 {
let p = 200.0 - i as f64 * 2.0;
let _ = rp.update_bar(p, p, p, p, 0.0);
}
assert!(rp.is_ready());
assert_eq!(rp.value(), IndicatorValue::Signal(-1));
}
#[test]
fn parity_with_legacy_macross_uptrend() {
let mut rp = RelativePosition::new(ema(9), ema(21));
let cfg_fast = IndicatorConfig::new(BarIndicatorId::Ema, "EmaFast".into(), vec![9]);
let cfg_slow = IndicatorConfig::new(BarIndicatorId::Ema, "EmaSlow".into(), vec![21]);
let _ = (cfg_fast, cfg_slow);
for i in 1..=40 {
let p = 100.0 + i as f64 * 2.0;
let _ = rp.update_bar(p, p + 1.0, p - 1.0, p, 1000.0);
}
assert!(rp.is_ready());
assert_eq!(rp.value(), IndicatorValue::Signal(1), "uptrend → +1 like legacy MaCross");
}
#[test]
fn parity_with_legacy_macross_downtrend() {
let mut rp = RelativePosition::new(ema(9), ema(21));
for i in 1..=40 {
let p = 200.0 - i as f64 * 2.0;
let _ = rp.update_bar(p, p + 1.0, p - 1.0, p, 1000.0);
}
assert_eq!(rp.value(), IndicatorValue::Signal(-1));
}
#[test]
fn state_sticks_across_oscillation() {
let mut rp = RelativePosition::new(sma(5), sma(20));
for i in 1..=80 {
let p = 100.0 + (i as f64 * 0.5).sin() * 8.0;
let _ = rp.update_bar(p, p, p, p, 0.0);
}
assert!(rp.is_ready());
let v = rp.value();
match v {
IndicatorValue::Signal(s) => assert!(s == 1 || s == -1, "sticky sign after oscillation"),
_ => panic!("expected Signal"),
}
}
#[test]
fn reset_clears_trend_state() {
let mut rp = RelativePosition::new(sma(5), sma(20));
for i in 1..=30 {
let p = 100.0 + i as f64;
let _ = rp.update_bar(p, p, p, p, 0.0);
}
rp.reset();
assert!(!rp.is_ready());
assert_eq!(rp.value(), IndicatorValue::Signal(0));
}
}