1use std::collections::VecDeque;
11
12pub struct PercentileTracker {
16 buf: VecDeque<f64>,
17}
18
19impl PercentileTracker {
20 pub fn new(maxlen: usize) -> Self {
21 Self {
22 buf: VecDeque::with_capacity(maxlen),
23 }
24 }
25
26 pub fn seeded(maxlen: usize, seed_lo: f64, seed_hi: f64) -> Self {
28 let mut t = Self::new(maxlen);
29 for i in 0..(maxlen / 2) {
30 t.buf.push_back(if i % 2 == 0 { seed_lo } else { seed_hi });
31 }
32 t
33 }
34
35 pub fn push(&mut self, val: f64) {
36 if self.buf.len() == self.buf.capacity() {
37 self.buf.pop_front();
38 }
39 self.buf.push_back(val);
40 }
41
42 pub fn pct(&self, val: f64) -> f64 {
44 let n = self.buf.len();
45 if n == 0 {
46 return 0.5;
47 }
48 self.buf.iter().filter(|&&v| v < val).count() as f64 / n as f64
49 }
50}
51
52pub struct VolatilityPercentile {
57 tracker: PercentileTracker,
58 pub vol_pct: f64,
59 pub vol_regime: &'static str,
60 pub vol_mult: f64,
61 pub conf_adj: f64,
63}
64
65impl VolatilityPercentile {
66 pub fn new(maxlen: usize) -> Self {
67 let tracker = PercentileTracker::seeded(maxlen, 20.0, 200.0);
68 Self {
69 tracker,
70 vol_pct: 0.5,
71 vol_regime: "MED",
72 vol_mult: 1.2,
73 conf_adj: 1.0,
74 }
75 }
76
77 pub fn update(&mut self, atr: Option<f64>) {
78 let Some(v) = atr else { return };
79 if v <= 0.0 {
80 return;
81 }
82 self.tracker.push(v);
83 let p = self.tracker.pct(v);
84 self.vol_pct = p;
85 (self.vol_regime, self.vol_mult, self.conf_adj) = if p >= 0.8 {
86 ("VERY HIGH", 1.8, 1.15)
87 } else if p >= 0.6 {
88 ("HIGH", 1.5, 1.05)
89 } else if p <= 0.2 {
90 ("VERY LOW", 0.8, 0.9)
91 } else if p <= 0.4 {
92 ("LOW", 1.0, 0.95)
93 } else {
94 ("MED", 1.2, 1.0)
95 };
96 }
97}
98
99pub struct MarketRegimeTracker {
106 closes: VecDeque<f64>,
107 ma200_hist: VecDeque<f64>,
108 ret_hist: VecDeque<f64>,
109
110 pub regime: &'static str,
111 pub is_trending_u: bool,
112 pub is_trending_d: bool,
113 pub is_ranging: bool,
114 pub is_volatile: bool,
115}
116
117impl MarketRegimeTracker {
118 pub fn new() -> Self {
119 Self {
120 closes: VecDeque::with_capacity(220),
121 ma200_hist: VecDeque::with_capacity(120),
122 ret_hist: VecDeque::with_capacity(110),
123 regime: "NEUTRAL",
124 is_trending_u: false,
125 is_trending_d: false,
126 is_ranging: false,
127 is_volatile: false,
128 }
129 }
130
131 pub fn update(&mut self, close: f64) {
132 let prev_cl = self.closes.back().copied().unwrap_or(close);
133
134 if self.closes.len() == 220 {
135 self.closes.pop_front();
136 }
137 self.closes.push_back(close);
138
139 if self.closes.len() < 200 {
140 return;
141 }
142
143 let ma200: f64 = self.closes.iter().rev().take(200).sum::<f64>() / 200.0;
145
146 if self.ma200_hist.len() == 120 {
147 self.ma200_hist.pop_front();
148 }
149 self.ma200_hist.push_back(ma200);
150
151 let ret = if prev_cl != 0.0 {
152 (close - prev_cl) / prev_cl
153 } else {
154 0.0
155 };
156 if self.ret_hist.len() == 110 {
157 self.ret_hist.pop_front();
158 }
159 self.ret_hist.push_back(ret);
160
161 if self.ma200_hist.len() < 21 || self.ret_hist.len() < 51 {
162 return;
163 }
164
165 let ma_arr: Vec<f64> = self.ma200_hist.iter().copied().collect();
167 let diffs: Vec<f64> = ma_arr.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
168 let avg_chg = if diffs.is_empty() {
169 1e-9
170 } else {
171 let tail: Vec<f64> = diffs.iter().rev().take(100).copied().collect();
172 tail.iter().sum::<f64>() / tail.len() as f64
173 };
174 let slope_n = if avg_chg > 0.0 {
175 (ma200 - ma_arr[ma_arr.len() - 21]) / (avg_chg * 20.0)
176 } else {
177 0.0
178 };
179
180 let ret_arr: Vec<f64> = self.ret_hist.iter().copied().collect();
182 let tail100: Vec<f64> = ret_arr.iter().rev().take(100).copied().collect();
183 let ret_s = std_dev(&tail100);
184 let tail50: Vec<f64> = ret_arr.iter().rev().take(50).map(|r| r.abs()).collect();
185 let ret_sma = if tail50.is_empty() {
186 ret_s.max(1e-9)
187 } else {
188 (tail50.iter().sum::<f64>() / tail50.len() as f64).max(1e-9)
189 };
190 let vol_n = ret_s / ret_sma;
191
192 self.regime = if slope_n > 1.0 {
193 "TRENDING↑"
194 } else if slope_n < -1.0 {
195 "TRENDING↓"
196 } else if vol_n > 1.5 {
197 "VOLATILE"
198 } else if vol_n < 0.8 {
199 "RANGING"
200 } else {
201 "NEUTRAL"
202 };
203
204 self.is_trending_u = self.regime == "TRENDING↑";
205 self.is_trending_d = self.regime == "TRENDING↓";
206 self.is_ranging = self.regime == "RANGING";
207 self.is_volatile = self.regime == "VOLATILE";
208 }
209}
210
211impl Default for MarketRegimeTracker {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217fn std_dev(data: &[f64]) -> f64 {
220 if data.len() < 2 {
221 return 0.0;
222 }
223 let mean = data.iter().sum::<f64>() / data.len() as f64;
224 let var = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / data.len() as f64;
225 var.sqrt()
226}