portfolio_analyzer/sd2/
classic.rs1use super::primitives::*;
13use super::squeeze::{group_c, run_clock, ClockSeries};
14use crate::indicators::{
15 awesome_oscillator, bollinger_bands, compute_compression, keltner_channels, smf_derivative,
16};
17use crate::sd::{GroupA, GroupB, SDState};
18
19pub struct ClassicSeries {
21 pub comp: Vec<u8>,
22 pub clock: ClockSeries,
23 pub ao: Vec<f64>,
24 pub states: Vec<SDState>,
25 pub score: Vec<f64>,
26}
27
28pub fn classic_series(bars: &[OHLCV]) -> ClassicSeries {
29 let n = bars.len();
30 let close: Vec<f64> = bars.iter().map(|b| b.close).collect();
31 let bb21 = bollinger_bands(&close, 21, 2.0);
32 let kc_hi = keltner_channels(bars, 21, 1.0);
33 let kc_mid = keltner_channels(bars, 21, 1.5);
34 let kc_lo = keltner_channels(bars, 21, 2.0);
35 let comp = compute_compression(&bb21, &kc_hi, &kc_mid, &kc_lo);
36 let ao = awesome_oscillator(bars);
37 let up = |i: usize| i >= 1 && ao[i] > ao[i - 1];
38 let down = |i: usize| i >= 1 && ao[i] < ao[i - 1];
39 let flip: Vec<bool> = (0..n)
40 .map(|i| i >= 1 && ((up(i) && !up(i - 1)) || (down(i) && !down(i - 1))))
41 .collect();
42 let clock = run_clock(&comp, &flip);
43
44 let ema8 = ema(&close, 8);
45 let ema21 = ema(&close, 21);
46 let d = |x: &[f64]| -> Vec<f64> {
47 std::iter::once(0.0).chain(x.windows(2).map(|w| w[1] - w[0])).collect()
48 };
49 let deriv_price = smf_derivative(&d(&close), 21);
50 let deriv_ema8 = smf_derivative(&d(&ema8), 21);
51 let deriv_ema21 = smf_derivative(&d(&ema21), 21);
52 let bb20 = bollinger_bands(&close, 20, 2.0);
53 let kc20 = keltner_channels(bars, 20, 2.0);
54
55 let mut states = Vec::with_capacity(n);
56 for i in 0..n {
57 let active = clock.is_active[i];
58 let group_a = if active && up(i) && deriv_ema8[i] >= 0.0 {
59 GroupA::Plus
60 } else if active && down(i) && deriv_ema8[i] <= 0.0 {
61 GroupA::Minus
62 } else {
63 GroupA::Tilde
64 };
65 let dist = if ema21[i] != 0.0 { (close[i] - ema21[i]).abs() / ema21[i] } else { 0.0 };
66 let cond1 = dist >= 0.005 && deriv_ema21[i] <= deriv_price[i];
67 let plus = (cond1 && close[i] < ema21[i])
68 || (!kc_lo.upper[i].is_nan()
69 && !bb21.upper[i].is_nan()
70 && (close[i] > kc_lo.upper[i] || close[i] > bb21.upper[i]));
71 let minus = (cond1 && close[i] > ema21[i])
72 || (!kc20.lower[i].is_nan()
73 && !bb20.lower[i].is_nan()
74 && (close[i] < kc20.lower[i] || close[i] < bb20.lower[i]));
75 let group_b = if group_a != GroupA::Tilde {
76 GroupB::SemiTilde
77 } else if plus {
78 GroupB::SemiPlus
79 } else if minus {
80 GroupB::SemiMinus
81 } else {
82 GroupB::SemiTilde
83 };
84 let eff = if active { clock.highest_comp[i] } else { comp[i] };
85 states.push(SDState { group_a, group_b, group_c: group_c(eff, comp[i]) });
86 }
87 let score = states.iter().map(SDState::score).collect();
88 ClassicSeries { comp, clock, ao, states, score }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::sd::SahastaDirectionality;
95
96 fn synthetic(n: usize) -> Vec<OHLCV> {
97 (0..n)
98 .map(|i| {
99 let x = i as f64;
100 let c = 100.0 + 8.0 * (x / 17.0).sin() * (x / 90.0).cos() + 0.05 * x;
101 OHLCV { open: c - 0.3, high: c + 0.9 + 0.4 * (x / 5.0).sin().abs(), low: c - 1.0, close: c, volume: 1.0 }
102 })
103 .collect()
104 }
105
106 #[test]
107 fn matches_sd_rs_on_the_last_bar() {
108 let bars = synthetic(400);
109 let series = classic_series(&bars);
110 for end in [120, 200, 287, 333, 400] {
111 let reference = SahastaDirectionality::new(bars[..end].to_vec()).compute_state().unwrap();
112 let ours = &series.states[end - 1];
113 assert_eq!(reference.group_a, ours.group_a, "group A at {end}");
114 assert_eq!(reference.group_b, ours.group_b, "group B at {end}");
115 assert_eq!(reference.group_c, ours.group_c, "group C at {end}");
116 }
117 }
118}