portfolio_analyzer/sd2/
squeeze.rs1pub const THETA_WINDOW: usize = 125;
28
29#[derive(Debug, Clone, Default)]
31pub struct ClockSeries {
32 pub highest_comp: Vec<u8>,
33 pub sq_start: Vec<bool>,
34 pub is_fired: Vec<bool>,
35 pub fired_start: Vec<bool>,
36 pub bars_since_fired: Vec<usize>,
38 pub bars_since_start: Vec<usize>,
40 pub mom_changed: Vec<bool>,
41 pub theta: Vec<f64>,
43 pub is_active: Vec<bool>,
44}
45
46pub fn run_clock(comp: &[u8], mom_flip: &[bool]) -> ClockSeries {
51 let n = comp.len();
52 let mut s = ClockSeries {
53 highest_comp: vec![0; n],
54 sq_start: vec![false; n],
55 is_fired: vec![false; n],
56 fired_start: vec![false; n],
57 bars_since_fired: vec![0; n],
58 bars_since_start: vec![0; n],
59 mom_changed: vec![false; n],
60 theta: vec![0.0; n],
61 is_active: vec![false; n],
62 };
63 for i in 0..n {
64 let c = comp[i];
65 let c_prev = if i > 0 { comp[i - 1] } else { 0 };
66 let hc_prev = if i > 0 { s.highest_comp[i - 1] } else { 0 };
67 let start_base = c > 0 && c_prev == 0;
68 let unfired = c > c_prev && c_prev < hc_prev && hc_prev > 0;
69 s.sq_start[i] = start_base || unfired;
70 s.highest_comp[i] = if s.sq_start[i] || c > hc_prev { c } else { hc_prev };
71 s.is_fired[i] = c < s.highest_comp[i] && s.highest_comp[i] > 0;
72 s.fired_start[i] = s.is_fired[i] && (i == 0 || !s.is_fired[i - 1]);
73 s.bars_since_fired[i] = if s.fired_start[i] {
74 1
75 } else if s.is_fired[i] {
76 s.bars_since_fired[i - 1] + 1
77 } else {
78 0
79 };
80 s.bars_since_start[i] = if s.sq_start[i] {
81 1
82 } else if c > 0 || s.is_fired[i] {
83 if i > 0 { s.bars_since_start[i - 1] + 1 } else { 1 }
84 } else {
85 0
86 };
87 s.mom_changed[i] = if s.fired_start[i] {
88 false
89 } else if s.is_fired[i] && mom_flip[i] {
90 true
91 } else {
92 i > 0 && s.mom_changed[i - 1]
93 };
94 }
95
96 let prefix = |f: &dyn Fn(usize) -> bool| {
99 let mut p = vec![0.0; n + 1];
100 for i in 0..n {
101 p[i + 1] = p[i] + if f(i) { 1.0 } else { 0.0 };
102 }
103 p
104 };
105 let in_comp = prefix(&|i| comp[i] > 0);
106 let starts = prefix(&|i| s.sq_start[i]);
107 let at_mid = prefix(&|i| comp[i] == 2);
108 let into_mid = prefix(&|i| comp[i] == 2 && (i == 0 || comp[i - 1] != 2));
109 let at_low = prefix(&|i| comp[i] == 1);
110 let into_low = prefix(&|i| comp[i] == 1 && (i == 0 || comp[i - 1] != 1));
111 for i in 0..n {
112 let a = i.saturating_sub(THETA_WINDOW - 1);
113 let w = |p: &Vec<f64>| p[i + 1] - p[a];
114 let total = w(&in_comp) / w(&starts).max(1.0);
115 let mid = w(&at_mid) / w(&into_mid).max(1.0);
116 let low = w(&at_low) / w(&into_low).max(1.0);
117 s.theta[i] = match comp[i] {
118 2 if mid >= total / 3.0 => mid,
119 2 => low,
120 1 if low >= total / 3.0 => low,
121 1 => total,
122 _ => total,
123 };
124 s.is_active[i] = s.is_fired[i]
125 && s.bars_since_fired[i] <= s.theta[i].ceil() as usize
126 && !s.mom_changed[i];
127 }
128 s
129}
130
131pub fn group_c(eff_highest: u8, comp: u8) -> u8 {
134 match (eff_highest, comp) {
135 (3, 3) => 1,
136 (3, 2) => 2,
137 (3, 1) => 3,
138 (3, 0) => 4,
139 (2, 2) => 5,
140 (2, 1) => 6,
141 (2, 0) => 7,
142 (1, 1) => 8,
143 (1, 0) => 9,
144 _ => 0,
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn a_squeeze_fires_when_it_drops_below_its_highest_tier() {
154 let comp = [0, 1, 3, 3, 2, 0, 0];
155 let s = run_clock(&comp, &[false; 7]);
156 assert_eq!(s.is_fired, vec![false, false, false, false, true, true, true]);
159 assert_eq!(s.bars_since_fired, vec![0, 0, 0, 0, 1, 2, 3]);
160 }
161
162 #[test]
163 fn a_momentum_flip_after_firing_ends_the_active_window() {
164 let comp = [0, 3, 3, 1, 1, 1];
165 let flips = [false, false, false, false, true, false];
166 let s = run_clock(&comp, &flips);
167 assert!(s.is_active[3]);
168 assert!(!s.is_active[4] && !s.is_active[5]);
169 }
170}