1use super::primitives::*;
26use super::squeeze::{run_clock, ClockSeries};
27use crate::indicators::smf_derivative;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CompressionMode {
32 Classic,
34 MultiScale,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum FlipMode {
42 AoColour,
44 BlendedPersistent,
46}
47
48#[derive(Debug, Clone, Copy)]
50pub struct FeatureConfig {
51 pub compression: CompressionMode,
52 pub flip: FlipMode,
53}
54
55impl Default for FeatureConfig {
56 fn default() -> Self {
57 Self { compression: CompressionMode::MultiScale, flip: FlipMode::BlendedPersistent }
58 }
59}
60
61pub const FEATURE_NAMES: &[&str] = &[
64 "m_ao", "m_ttm", "m_macd", "m_roc", "m_rsi", "m_smf8", "m_trend", "m_tsmom",
66 "m_blend", "m_agree",
68 "fire_x_blend", "fire_x_decay_blend",
71 "energy_x_blend", "depth_x_blend",
74 "stretch",
76 "semi",
78];
79
80pub const DEFAULT_BLEND: [f64; 8] = [0.125; 8];
82
83pub struct FeatureFrame {
85 pub n: usize,
86 pub close: Vec<f64>,
87 pub atr: Vec<f64>,
88 pub vol20: Vec<f64>,
90 pub vol_robust: Vec<f64>,
95 pub vol126: Vec<f64>,
97 pub q21: Vec<f64>,
98 pub q10: Vec<f64>,
99 pub comp: Vec<u8>,
100 pub clock: ClockSeries,
101 pub energy: Vec<f64>,
103 pub width_pct: Vec<f64>,
105 pub mom: Vec<[f64; 8]>,
107 pub blend: Vec<f64>,
108 pub agree: Vec<f64>,
109 pub decay: Vec<f64>,
111 pub stretch: Vec<f64>,
112 pub semi: Vec<f64>,
113}
114
115pub fn tier(q: f64) -> u8 {
117 if !q.is_finite() {
118 0
119 } else if q <= 1.0 {
120 3
121 } else if q <= 1.5 {
122 2
123 } else if q <= 2.0 {
124 1
125 } else {
126 0
127 }
128}
129
130pub fn decay_kernel(phase: f64) -> f64 {
137 if !phase.is_finite() || phase < 0.0 {
138 return 0.0;
139 }
140 let x = (phase - 1.5).max(0.0);
143 (-x * std::f64::consts::LN_2).exp()
144}
145
146impl FeatureFrame {
147 pub fn compute(bars: &[OHLCV], cfg: FeatureConfig) -> Self {
148 let n = bars.len();
149 let close: Vec<f64> = bars.iter().map(|b| b.close).collect();
150 let hl2: Vec<f64> = bars.iter().map(|b| (b.high + b.low) / 2.0).collect();
151 let high: Vec<f64> = bars.iter().map(|b| b.high).collect();
152 let low: Vec<f64> = bars.iter().map(|b| b.low).collect();
153 let atr21 = atr(bars, 21);
154 let atr10 = atr(bars, 10);
155 let std21 = rolling_std(&close, 21);
156 let std10 = rolling_std(&close, 10);
157 let q21: Vec<f64> = (0..n).map(|i| 2.0 * std21[i] / atr21[i]).collect();
158 let q10: Vec<f64> = (0..n).map(|i| 2.0 * std10[i] / atr10[i]).collect();
159 let comp: Vec<u8> = (0..n)
160 .map(|i| match cfg.compression {
161 CompressionMode::Classic => tier(q21[i]),
162 CompressionMode::MultiScale => {
167 let (slow, fast) = (tier(q21[i]), tier(q10[i]));
168 if slow > 0 || fast == 3 { slow.max(fast) } else { slow }
169 }
170 })
171 .collect();
172
173 let ao: Vec<f64> = sma(&hl2, 5).iter().zip(sma(&hl2, 34)).map(|(a, b)| a - b).collect();
175 let sma20 = sma(&close, 20);
176 let (hh, ll) = (highest(&high, 20), lowest(&low, 20));
177 let ttm_src: Vec<f64> =
178 (0..n).map(|i| close[i] - ((hh[i] + ll[i]) / 2.0 + sma20[i]) / 2.0).collect();
179 let ttm = linreg_value(&ttm_src, 20);
180 let macd = macd_hist(&close);
181 let roc = diff(&close, 10);
182 let rsi14 = rsi(&close, 14);
183 let ema8 = ema(&close, 8);
184 let ema21 = ema(&close, 21);
185 let d = |x: &[f64]| -> Vec<f64> {
186 std::iter::once(0.0).chain(x.windows(2).map(|w| w[1] - w[0])).collect()
187 };
188 let smf8 = smf_derivative(&d(&ema8), 21);
189 let smf_price = smf_derivative(&d(&close), 21);
190 let smf21 = smf_derivative(&d(&ema21), 21);
191 let logret: Vec<f64> = (0..n)
192 .map(|i| if i == 0 { 0.0 } else { (close[i] / close[i - 1]).ln() })
193 .collect();
194 let vol20 = rolling_std(&logret, 20);
195 let ret5: Vec<f64> = (0..n)
196 .map(|i| if i >= 5 { (close[i] / close[i - 5]).ln() } else { f64::NAN })
197 .collect();
198 let vol_robust: Vec<f64> = rolling_std(&ret5, 60).iter().map(|v| v / 5f64.sqrt()).collect();
199 let vol126 = rolling_std(&logret, 126);
200
201 let mut mom = vec![[0.0; 8]; n];
202 for i in 0..n {
203 let a = atr21[i];
204 let tsm = if i >= 126 && vol126[i] > 0.0 {
205 (close[i] / close[i - 126]).ln() / (vol126[i] * 126f64.sqrt())
206 } else {
207 f64::NAN
208 };
209 mom[i] = [
212 squash(ao[i] / a, 1.5),
213 squash(ttm[i] / a, 1.0),
214 squash(macd[i] / a, 0.25),
215 squash(roc[i] / (a * 10f64.sqrt()), 0.8),
216 squash((rsi14[i] - 50.0) / 50.0, 0.3),
217 squash(smf8[i] / a, 0.15),
218 squash((ema8[i] - ema21[i]) / a, 1.0),
219 squash(tsm, 1.0),
220 ];
221 }
222 let blend: Vec<f64> =
223 mom.iter().map(|m| m.iter().zip(DEFAULT_BLEND).map(|(x, w)| x * w).sum()).collect();
224 let agree: Vec<f64> = mom
225 .iter()
226 .map(|m| m.iter().map(|x| x.signum()).sum::<f64>() / m.len() as f64)
227 .collect();
228
229 let flip: Vec<bool> = match cfg.flip {
231 FlipMode::AoColour => {
232 let up = |i: usize| i >= 1 && ao[i] > ao[i - 1];
233 let dn = |i: usize| i >= 1 && ao[i] < ao[i - 1];
234 (0..n).map(|i| i >= 1 && ((up(i) && !up(i - 1)) || (dn(i) && !dn(i - 1)))).collect()
235 }
236 FlipMode::BlendedPersistent => (0..n)
239 .map(|i| {
240 i >= 2
241 && blend[i].signum() == blend[i - 1].signum()
242 && blend[i - 1].signum() != blend[i - 2].signum()
243 })
244 .collect(),
245 };
246 let clock = run_clock(&comp, &flip);
247
248 let mut energy = vec![0.0; n];
252 for i in 0..n {
253 let prev = if i > 0 { energy[i - 1] } else { 0.0 };
254 let depth = (2.0 - q21[i]).max(0.0).min(2.0) + 0.5 * (2.0 - q10[i]).max(0.0).min(2.0);
255 energy[i] = if clock.sq_start[i] {
256 depth
257 } else if clock.is_fired[i] {
258 if clock.bars_since_fired[i] as f64 > 3.0 * clock.theta[i].max(1.0) { 0.0 } else { prev }
259 } else if comp[i] > 0 {
260 prev + depth
261 } else {
262 0.0
263 };
264 }
265
266 let width: Vec<f64> = (0..n).map(|i| std21[i] / close[i]).collect();
267 let width_pct: Vec<f64> = (0..n)
268 .map(|i| {
269 if i < 252 || !width[i].is_finite() {
270 return f64::NAN;
271 }
272 let w = &width[i - 251..=i];
273 w.iter().filter(|v| **v < width[i]).count() as f64 / 251.0
274 })
275 .collect();
276
277 let decay: Vec<f64> = (0..n)
278 .map(|i| {
279 if !clock.is_fired[i] {
280 return 0.0;
281 }
282 decay_kernel(clock.bars_since_fired[i] as f64 / clock.theta[i].max(1.0))
283 })
284 .collect();
285
286 let stretch: Vec<f64> = (0..n).map(|i| squash((close[i] - ema21[i]) / atr21[i], 3.0)).collect();
287 let semi: Vec<f64> = (0..n)
288 .map(|i| {
289 let dist = (close[i] - ema21[i]).abs() / ema21[i];
290 let exhausted = dist >= 0.005 && smf21[i] <= smf_price[i];
291 if exhausted && close[i] < ema21[i] {
292 1.0
293 } else if exhausted && close[i] > ema21[i] {
294 -1.0
295 } else {
296 0.0
297 }
298 })
299 .collect();
300
301 Self {
302 n,
303 close,
304 atr: atr21,
305 vol20,
306 vol_robust,
307 vol126,
308 q21,
309 q10,
310 comp,
311 clock,
312 energy,
313 width_pct,
314 mom,
315 blend,
316 agree,
317 decay,
318 stretch,
319 semi,
320 }
321 }
322
323 pub const WARMUP: usize = 260;
325
326 pub fn fire_alive(&self, i: usize) -> bool {
328 self.clock.is_active[i]
329 }
330
331 pub fn row(&self, i: usize) -> Vec<f64> {
333 let m = &self.mom[i];
334 let b = self.blend[i];
335 let alive = if self.fire_alive(i) { 1.0 } else { 0.0 };
336 let fired = if self.clock.is_fired[i] { 1.0 } else { 0.0 };
337 let energy = squash(self.energy[i], 8.0);
338 let depth = squash((2.0 - self.q21[i]).max(0.0), 1.0);
339 let mut r = Vec::with_capacity(FEATURE_NAMES.len());
340 r.extend_from_slice(m);
341 r.push(b);
342 r.push(self.agree[i]);
343 r.push(alive * b);
344 r.push(fired * self.decay[i] * b);
345 r.push(energy * b);
346 r.push(depth * b);
347 r.push(self.stretch[i]);
348 r.push(self.semi[i]);
349 r
350 }
351
352 pub fn row_is_valid(&self, i: usize) -> bool {
353 i >= Self::WARMUP && self.row(i).iter().all(|v| v.is_finite()) && self.atr[i] > 0.0
354 }
355}