use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::instance_factory::IndicatorInstance;
type SwingPoint = (usize, f64, f64);
fn compute_slope(values: &[f64]) -> f64 {
let n = values.len();
if n < 2 {
return 0.0;
}
let n_f = n as f64;
let sum_x: f64 = (0..n).map(|i| i as f64).sum();
let sum_y: f64 = values.iter().sum();
let sum_xy: f64 = values.iter().enumerate().map(|(i, &y)| i as f64 * y).sum();
let sum_x2: f64 = (0..n).map(|i| (i as f64).powi(2)).sum();
let denom = n_f * sum_x2 - sum_x * sum_x;
if denom.abs() < 1e-12 {
return 0.0;
}
(n_f * sum_xy - sum_x * sum_y) / denom
}
fn detect_multi_pivot_divergence(swings: &[SwingPoint], n: usize) -> Option<f64> {
if swings.len() < n {
return None;
}
let last_n = &swings[swings.len() - n..];
let prices: Vec<f64> = last_n.iter().map(|s| s.1).collect();
let oscs: Vec<f64> = last_n.iter().map(|s| s.2).collect();
let price_slope = compute_slope(&prices);
let osc_slope = compute_slope(&oscs);
if price_slope < 0.0 && osc_slope > 0.0 {
Some(2.0) } else if price_slope > 0.0 && osc_slope < 0.0 {
Some(-2.0) } else {
None
}
}
fn compute_strength(
s0: &SwingPoint,
s1: &SwingPoint,
osc_range: f64,
price_range: f64,
atr_value: Option<f64>,
price_mean_in_window: f64,
) -> f64 {
const EPS: f64 = 1e-9;
let delta_osc = (s1.2 - s0.2).abs();
let delta_price = (s1.1 - s0.1).abs();
let osc_norm = if osc_range > EPS {
delta_osc / osc_range
} else {
0.0
};
let price_norm = if price_range > EPS {
delta_price / price_range
} else {
0.0
};
let angle_score = if price_norm > EPS {
(osc_norm / price_norm).min(1.0)
} else {
0.0
};
let swing_quality = match atr_value {
Some(atr) if atr > EPS => {
((s1.1 - price_mean_in_window).abs() / atr).min(1.0)
}
_ => 0.0,
};
let distance_bars = (s1.0.saturating_sub(s0.0)) as f64;
let distance_score = (-((distance_bars - 10.0) / 5.0).powi(2)).exp();
(0.4 * angle_score + 0.3 * swing_quality + 0.3 * distance_score).clamp(0.0, 1.0)
}
#[derive(Clone)]
pub struct OscillatorWithDivergence {
inner: Box<IndicatorInstance>,
swing_lookback: usize,
detect_regular: bool,
detect_hidden: bool,
with_strength: bool,
price_buf: Vec<f64>,
osc_buf: Vec<f64>,
swing_highs: Vec<SwingPoint>,
swing_lows: Vec<SwingPoint>,
bar_counter: usize,
atr: Option<Box<IndicatorInstance>>,
compare_swings: usize,
}
impl std::fmt::Debug for OscillatorWithDivergence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OscillatorWithDivergence")
.field("swing_lookback", &self.swing_lookback)
.field("detect_regular", &self.detect_regular)
.field("detect_hidden", &self.detect_hidden)
.field("with_strength", &self.with_strength)
.field("price_buf_len", &self.price_buf.len())
.field("osc_buf_len", &self.osc_buf.len())
.field("bar_counter", &self.bar_counter)
.finish()
}
}
impl OscillatorWithDivergence {
const BUF_CAP: usize = 512;
const MAX_SWINGS: usize = 4;
pub fn new(
inner: Box<IndicatorInstance>,
swing_lookback: usize,
detect_regular: bool,
detect_hidden: bool,
with_strength: bool,
atr: Option<Box<IndicatorInstance>>,
) -> Self {
Self::with_compare_swings(inner, swing_lookback, detect_regular, detect_hidden, with_strength, atr, 2)
}
pub fn with_compare_swings(
inner: Box<IndicatorInstance>,
swing_lookback: usize,
detect_regular: bool,
detect_hidden: bool,
with_strength: bool,
atr: Option<Box<IndicatorInstance>>,
compare_swings: usize,
) -> Self {
let cs = compare_swings.clamp(2, Self::MAX_SWINGS);
Self {
inner,
swing_lookback: swing_lookback.max(2),
detect_regular,
detect_hidden,
with_strength,
price_buf: Vec::with_capacity(Self::BUF_CAP),
osc_buf: Vec::with_capacity(Self::BUF_CAP),
swing_highs: Vec::with_capacity(Self::MAX_SWINGS + 1),
swing_lows: Vec::with_capacity(Self::MAX_SWINGS + 1),
bar_counter: 0,
atr,
compare_swings: cs,
}
}
pub fn update_bar(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> IndicatorValue {
let osc_val = self
.inner
.update_bar(open, high, low, close, volume, None)
.main();
let atr_now = if let Some(atr_ind) = &mut self.atr {
atr_ind.update_bar(open, high, low, close, volume, None).main()
} else {
0.0
};
if self.price_buf.len() >= Self::BUF_CAP {
self.price_buf.remove(0);
self.osc_buf.remove(0);
}
self.price_buf.push(close);
self.osc_buf.push(osc_val);
self.bar_counter += 1;
let mut new_signal: f64 = 0.0;
let mut new_strength: f64 = 0.0;
let min_len = 2 * self.swing_lookback + 1;
if self.price_buf.len() >= min_len {
let buf_len = self.price_buf.len();
let check_idx = buf_len - self.swing_lookback - 1;
let abs_idx = self.bar_counter - 1 - self.swing_lookback;
let center_price = self.price_buf[check_idx];
let center_osc = self.osc_buf[check_idx];
let mut is_high = true;
let mut is_low = true;
let lo = check_idx.saturating_sub(self.swing_lookback);
let hi = (check_idx + self.swing_lookback).min(buf_len - 1);
for i in lo..=hi {
if i == check_idx {
continue;
}
if self.price_buf[i] >= center_price {
is_high = false;
}
if self.price_buf[i] <= center_price {
is_low = false;
}
}
if is_high {
self.swing_highs.push((abs_idx, center_price, center_osc));
if self.swing_highs.len() > Self::MAX_SWINGS {
self.swing_highs.remove(0);
}
if self.compare_swings <= 2 {
if self.swing_highs.len() >= 2 {
let n = self.swing_highs.len();
let s0 = self.swing_highs[n - 2];
let s1 = self.swing_highs[n - 1];
let bearish_regular =
self.detect_regular && s1.1 > s0.1 && s1.2 < s0.2;
let bearish_hidden =
self.detect_hidden && s1.1 < s0.1 && s1.2 > s0.2;
if bearish_regular {
new_signal = -2.0;
} else if bearish_hidden {
new_signal = -1.0;
}
if new_signal != 0.0 && self.with_strength {
new_strength = self.calc_strength(&s0, &s1, atr_now);
}
}
} else if self.detect_regular && self.swing_highs.len() >= self.compare_swings {
if let Some(sig) = detect_multi_pivot_divergence(&self.swing_highs, self.compare_swings) {
new_signal = sig;
if self.with_strength && self.swing_highs.len() >= 2 {
let n = self.swing_highs.len();
let s0 = self.swing_highs[n - 2];
let s1 = self.swing_highs[n - 1];
new_strength = self.calc_strength(&s0, &s1, atr_now);
}
}
}
}
if is_low {
self.swing_lows.push((abs_idx, center_price, center_osc));
if self.swing_lows.len() > Self::MAX_SWINGS {
self.swing_lows.remove(0);
}
if self.compare_swings <= 2 {
if self.swing_lows.len() >= 2 {
let n = self.swing_lows.len();
let s0 = self.swing_lows[n - 2];
let s1 = self.swing_lows[n - 1];
let bull_regular =
self.detect_regular && s1.1 < s0.1 && s1.2 > s0.2;
let bull_hidden =
self.detect_hidden && s1.1 > s0.1 && s1.2 < s0.2;
if new_signal == 0.0 {
if bull_regular {
new_signal = 2.0;
} else if bull_hidden {
new_signal = 1.0;
}
if new_signal != 0.0 && self.with_strength {
new_strength = self.calc_strength(&s0, &s1, atr_now);
}
}
}
} else if self.detect_regular && new_signal == 0.0
&& self.swing_lows.len() >= self.compare_swings
{
if let Some(sig) = detect_multi_pivot_divergence(&self.swing_lows, self.compare_swings) {
new_signal = sig;
if self.with_strength && self.swing_lows.len() >= 2 {
let n = self.swing_lows.len();
let s0 = self.swing_lows[n - 2];
let s1 = self.swing_lows[n - 1];
new_strength = self.calc_strength(&s0, &s1, atr_now);
}
}
}
}
}
if self.with_strength {
IndicatorValue::Triple(osc_val, new_signal, new_strength)
} else {
IndicatorValue::Double(osc_val, new_signal)
}
}
fn calc_strength(&self, s0: &SwingPoint, s1: &SwingPoint, atr_val: f64) -> f64 {
let oldest_abs = self.bar_counter.saturating_sub(self.price_buf.len());
if s0.0 < oldest_abs || s1.0 < oldest_abs {
return 0.0;
}
let i0 = s0.0 - oldest_abs;
let i1 = s1.0 - oldest_abs;
if i0 >= self.price_buf.len() || i1 >= self.price_buf.len() || i0 > i1 {
return 0.0;
}
let price_slice = &self.price_buf[i0..=i1];
let osc_slice = &self.osc_buf[i0..=i1];
let price_max = price_slice.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let price_min = price_slice.iter().cloned().fold(f64::INFINITY, f64::min);
let osc_max = osc_slice.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let osc_min = osc_slice.iter().cloned().fold(f64::INFINITY, f64::min);
let price_range = price_max - price_min;
let osc_range = osc_max - osc_min;
let price_mean = if price_slice.is_empty() {
0.0
} else {
price_slice.iter().sum::<f64>() / price_slice.len() as f64
};
let atr_opt = if self.atr.is_some() && atr_val > 1e-9 {
Some(atr_val)
} else {
None
};
compute_strength(s0, s1, osc_range, price_range, atr_opt, price_mean)
}
pub fn value(&self) -> IndicatorValue {
let osc_val = self.osc_buf.last().copied().unwrap_or(0.0);
if self.with_strength {
IndicatorValue::Triple(osc_val, 0.0, 0.0)
} else {
IndicatorValue::Double(osc_val, 0.0)
}
}
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
pub fn reset(&mut self) {
self.inner.reset();
self.price_buf.clear();
self.osc_buf.clear();
self.swing_highs.clear();
self.swing_lows.clear();
self.bar_counter = 0;
if let Some(atr) = &mut self.atr {
atr.reset();
}
}
pub fn swing_lookback(&self) -> usize {
self.swing_lookback
}
pub fn detects_regular(&self) -> bool {
self.detect_regular
}
pub fn detects_hidden(&self) -> bool {
self.detect_hidden
}
pub fn compare_swings(&self) -> usize {
self.compare_swings
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bar_indicators::bar_indicator_id::BarIndicatorId;
use crate::bar_indicators::instance_factory::{IndicatorConfig, IndicatorInstance};
fn rsi_inner(period: usize) -> Box<IndicatorInstance> {
Box::new(
IndicatorInstance::create(&IndicatorConfig::new(
BarIndicatorId::Rsi,
"Rsi".into(),
vec![period],
))
.expect("RSI creation must succeed"),
)
}
fn atr_inner() -> Box<IndicatorInstance> {
Box::new(
IndicatorInstance::create(&IndicatorConfig::new(
BarIndicatorId::Atr,
"Atr".into(),
vec![14],
))
.expect("ATR creation must succeed"),
)
}
fn feed(ind: &mut OscillatorWithDivergence, close: f64, volume: f64) -> IndicatorValue {
ind.update_bar(close, close + 0.5, close - 0.5, close, volume)
}
#[test]
fn smoke_layer_2_returns_double() {
let mut ind =
OscillatorWithDivergence::new(rsi_inner(14), 3, true, true, false, None);
let mut last = IndicatorValue::Single(0.0);
for i in 0..20u32 {
let p = 100.0 + i as f64;
last = ind.update_bar(p, p + 1.0, p - 1.0, p, 1000.0);
}
match last {
IndicatorValue::Double(_, _) => {}
other => panic!("expected Double, got {:?}", other),
}
}
#[test]
fn smoke_layer_3_returns_triple() {
let mut ind =
OscillatorWithDivergence::new(rsi_inner(14), 3, true, true, true, None);
let mut last = IndicatorValue::Single(0.0);
for i in 0..20u32 {
let p = 100.0 + i as f64;
last = ind.update_bar(p, p + 1.0, p - 1.0, p, 1000.0);
}
match last {
IndicatorValue::Triple(_, _, _) => {}
other => panic!("expected Triple, got {:?}", other),
}
}
#[test]
fn reset_clears_buffers() {
let mut ind = OscillatorWithDivergence::new(rsi_inner(14), 3, true, true, false, None);
for i in 0..20u32 {
let p = 100.0 + i as f64;
ind.update_bar(p, p, p, p, 1000.0);
}
assert!(ind.is_ready());
ind.reset();
assert!(!ind.is_ready());
}
fn sma_inner(period: usize) -> Box<IndicatorInstance> {
Box::new(
IndicatorInstance::create(&IndicatorConfig::new(
BarIndicatorId::Sma,
"Sma".into(),
vec![period],
))
.expect("SMA creation must succeed"),
)
}
fn collect_signals(vals: &[IndicatorValue]) -> Vec<f64> {
vals.iter()
.map(|v| match v {
IndicatorValue::Double(_, s) => *s,
IndicatorValue::Triple(_, s, _) => *s,
_ => 0.0,
})
.collect()
}
fn feed_flat(
ind: &mut OscillatorWithDivergence,
price: f64,
n: usize,
) -> Vec<IndicatorValue> {
(0..n).map(|_| feed(ind, price, 1000.0)).collect()
}
fn swing_low_segment(
ind: &mut OscillatorWithDivergence,
dip: f64,
surround: f64,
) -> Vec<IndicatorValue> {
let mut out = feed_flat(ind, surround, 4);
out.push(feed(ind, dip, 1000.0));
out.extend(feed_flat(ind, surround, 3));
out
}
fn swing_high_segment(
ind: &mut OscillatorWithDivergence,
peak: f64,
surround: f64,
) -> Vec<IndicatorValue> {
let mut out = feed_flat(ind, surround, 4);
out.push(feed(ind, peak, 1000.0));
out.extend(feed_flat(ind, surround, 3));
out
}
#[test]
fn synthetic_bullish_regular_divergence() {
let mut ind =
OscillatorWithDivergence::new(sma_inner(5), 3, true, true, false, None);
let mut all: Vec<IndicatorValue> = Vec::new();
all.extend(feed_flat(&mut ind, 100.0, 8));
all.extend(feed_flat(&mut ind, 110.0, 4));
all.extend(swing_low_segment(&mut ind, 80.0, 120.0));
all.extend(feed_flat(&mut ind, 140.0, 4));
all.extend(swing_low_segment(&mut ind, 70.0, 150.0));
let signals = collect_signals(&all);
assert!(
signals.iter().any(|&s| s == 2.0),
"expected +2.0 (bullish regular); signals: {:?}",
signals
);
assert!(
signals.iter().all(|&s| s >= 0.0),
"unexpected bearish signal; signals: {:?}",
signals
);
}
#[test]
fn synthetic_bearish_regular_divergence() {
let mut ind =
OscillatorWithDivergence::new(sma_inner(5), 3, true, true, false, None);
let mut all: Vec<IndicatorValue> = Vec::new();
all.extend(feed_flat(&mut ind, 100.0, 8));
all.extend(feed_flat(&mut ind, 90.0, 4));
all.extend(swing_high_segment(&mut ind, 120.0, 80.0));
all.extend(feed_flat(&mut ind, 55.0, 4));
all.extend(swing_high_segment(&mut ind, 130.0, 60.0));
let signals = collect_signals(&all);
assert!(
signals.iter().any(|&s| s == -2.0),
"expected -2.0 (bearish regular); signals: {:?}",
signals
);
assert!(
signals.iter().all(|&s| s <= 0.0),
"unexpected bullish signal; signals: {:?}",
signals
);
}
#[test]
fn synthetic_bullish_hidden_divergence() {
let mut ind =
OscillatorWithDivergence::new(sma_inner(5), 3, true, true, false, None);
let mut all: Vec<IndicatorValue> = Vec::new();
all.extend(feed_flat(&mut ind, 100.0, 8));
all.extend(feed_flat(&mut ind, 145.0, 4));
all.extend(swing_low_segment(&mut ind, 80.0, 150.0));
all.extend(feed_flat(&mut ind, 130.0, 4));
all.extend(swing_low_segment(&mut ind, 90.0, 120.0));
let signals = collect_signals(&all);
assert!(
signals.iter().any(|&s| s == 1.0),
"expected +1.0 (bullish hidden); signals: {:?}",
signals
);
assert!(
signals.iter().all(|&s| s >= 0.0),
"unexpected bearish signal; signals: {:?}",
signals
);
}
#[test]
fn synthetic_bearish_hidden_divergence() {
let mut ind =
OscillatorWithDivergence::new(sma_inner(5), 3, true, true, false, None);
let mut all: Vec<IndicatorValue> = Vec::new();
all.extend(feed_flat(&mut ind, 100.0, 8));
all.extend(feed_flat(&mut ind, 75.0, 4));
all.extend(swing_high_segment(&mut ind, 120.0, 80.0));
all.extend(feed_flat(&mut ind, 85.0, 4));
all.extend(swing_high_segment(&mut ind, 110.0, 90.0));
let signals = collect_signals(&all);
assert!(
signals.iter().any(|&s| s == -1.0),
"expected -1.0 (bearish hidden); signals: {:?}",
signals
);
assert!(
signals.iter().all(|&s| s <= 0.0),
"unexpected bullish signal; signals: {:?}",
signals
);
}
#[test]
fn no_divergence_emits_zero_on_monotonic_up() {
let mut ind =
OscillatorWithDivergence::new(sma_inner(5), 3, true, true, false, None);
let signals: Vec<f64> = (0..60u32)
.map(|i| {
let p = 100.0 + i as f64 * 1.5;
match feed(&mut ind, p, 1000.0) {
IndicatorValue::Double(_, s) => s,
other => panic!("unexpected {:?}", other),
}
})
.collect();
assert!(
signals.iter().all(|&s| s == 0.0),
"monotonic uptrend should emit no divergence signals; got: {:?}",
signals
);
}
#[test]
fn layer_3_returns_triple_with_positive_strength() {
let mut ind = OscillatorWithDivergence::new(
sma_inner(5),
3,
true,
true,
true, Some(atr_inner()),
);
let mut all: Vec<IndicatorValue> = Vec::new();
all.extend(feed_flat(&mut ind, 100.0, 8));
all.extend(feed_flat(&mut ind, 110.0, 4));
all.extend(swing_low_segment(&mut ind, 80.0, 120.0));
all.extend(feed_flat(&mut ind, 140.0, 4));
all.extend(swing_low_segment(&mut ind, 70.0, 150.0));
let signal_bars: Vec<(f64, f64)> = all
.iter()
.filter_map(|v| match v {
IndicatorValue::Triple(_, s, str_) if *s != 0.0 => Some((*s, *str_)),
_ => None,
})
.collect();
assert!(
!signal_bars.is_empty(),
"expected at least one non-zero signal in layer-3 mode"
);
assert!(
signal_bars.iter().any(|(_, str_)| *str_ > 0.0),
"expected strength > 0.0 on signal bar; got: {:?}",
signal_bars
);
for v in &all {
assert!(
matches!(v, IndicatorValue::Triple(_, _, _)),
"expected Triple for all bars, got {:?}",
v
);
}
}
#[test]
fn compare_swings_2_identical_to_classic_behavior() {
let mut classic =
OscillatorWithDivergence::new(sma_inner(5), 3, true, true, false, None);
let mut multi =
OscillatorWithDivergence::with_compare_swings(sma_inner(5), 3, true, true, false, None, 2);
let mut classic_vals: Vec<IndicatorValue> = Vec::new();
let mut multi_vals: Vec<IndicatorValue> = Vec::new();
let feed_both = |c: &mut OscillatorWithDivergence, m: &mut OscillatorWithDivergence,
vc: &mut Vec<IndicatorValue>, vm: &mut Vec<IndicatorValue>, price: f64| {
vc.push(feed(c, price, 1000.0));
vm.push(feed(m, price, 1000.0));
};
macro_rules! feed_flat_both {
($c:expr, $m:expr, $vc:expr, $vm:expr, $p:expr, $n:expr) => {
for _ in 0..$n { feed_both($c, $m, $vc, $vm, $p); }
};
}
feed_flat_both!(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 100.0, 8);
feed_flat_both!(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 110.0, 4);
feed_flat_both!(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 120.0, 4);
feed_both(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 80.0);
feed_flat_both!(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 120.0, 3);
feed_flat_both!(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 140.0, 4);
feed_flat_both!(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 150.0, 4);
feed_both(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 70.0);
feed_flat_both!(&mut classic, &mut multi, &mut classic_vals, &mut multi_vals, 150.0, 3);
let c_sigs = collect_signals(&classic_vals);
let m_sigs = collect_signals(&multi_vals);
assert!(c_sigs.iter().any(|&s| s == 2.0), "classic missing +2: {:?}", c_sigs);
assert!(m_sigs.iter().any(|&s| s == 2.0), "multi cs=2 missing +2: {:?}", m_sigs);
}
#[test]
fn compare_swings_3_detects_trend_across_3_points() {
let mut ind =
OscillatorWithDivergence::with_compare_swings(sma_inner(5), 3, true, true, false, None, 3);
let mut all: Vec<IndicatorValue> = Vec::new();
all.extend(feed_flat(&mut ind, 100.0, 8));
all.extend(feed_flat(&mut ind, 115.0, 4));
all.extend(swing_low_segment(&mut ind, 90.0, 120.0));
all.extend(feed_flat(&mut ind, 125.0, 4));
all.extend(swing_low_segment(&mut ind, 80.0, 130.0));
all.extend(feed_flat(&mut ind, 135.0, 4));
all.extend(swing_low_segment(&mut ind, 70.0, 140.0));
let signals = collect_signals(&all);
assert!(
signals.iter().any(|&s| s == 2.0),
"compare_swings=3 should detect bullish regular via regression; signals: {:?}",
signals
);
}
#[test]
fn detect_regular_false_suppresses_regular_signal() {
let mut ind =
OscillatorWithDivergence::new(sma_inner(5), 3, false, true, false, None);
let mut all: Vec<IndicatorValue> = Vec::new();
all.extend(feed_flat(&mut ind, 100.0, 8));
all.extend(feed_flat(&mut ind, 110.0, 4));
all.extend(swing_low_segment(&mut ind, 80.0, 120.0));
all.extend(feed_flat(&mut ind, 140.0, 4));
all.extend(swing_low_segment(&mut ind, 70.0, 150.0));
let signals = collect_signals(&all);
assert!(
signals.iter().all(|&s| s != 2.0),
"detect_regular=false must suppress +2.0 signals; got: {:?}",
signals
);
}
}