Skip to main content

indicators/
engine.rs

1//! Core Indicators Engine — Layers 1–4, 9–11.
2//!
3//! Faithful port of the Python `Indicators` class from `indicators.py`.
4//!
5//! Layers:
6//! - **L1** VWAP (daily reset)
7//! - **L2** EMA (configurable period)
8//! - **L3** ML SuperTrend — KMeans-adaptive ATR multiplier
9//! - **L4** Trend Speed — dynamic EMA + RMA wave tracking + HMA
10//! - **L9** Awesome Oscillator + wave/momentum percentile gates
11//! - **L10** Hurst exponent (R/S analysis, recomputed every 10 bars)
12//! - **L11** Price acceleration (2nd derivative, normalised)
13
14use crate::settings::BotSettings;
15use crate::types::Candle;
16use crate::vol_regime::PercentileTracker;
17use chrono::{NaiveDate, TimeZone, Utc};
18use std::collections::VecDeque;
19
20// ── Helpers ───────────────────────────────────────────────────────────────────
21#[inline]
22#[allow(dead_code)]
23fn ema_step(prev: Option<f64>, val: f64, len: usize) -> f64 {
24    let k = 2.0 / (len as f64 + 1.0);
25    prev.map_or(val, |p| val * k + p * (1.0 - k))
26}
27
28#[inline]
29fn rma_step(prev: Option<f64>, val: f64, len: usize) -> f64 {
30    let k = 1.0 / len as f64;
31    prev.map_or(val, |p| val * k + p * (1.0 - k))
32}
33
34fn wma(arr: &[f64]) -> f64 {
35    if arr.is_empty() {
36        return 0.0;
37    }
38    let n = arr.len() as f64;
39    let weights_sum = n * (n + 1.0) / 2.0;
40    arr.iter()
41        .enumerate()
42        .map(|(i, &v)| v * (i as f64 + 1.0))
43        .sum::<f64>()
44        / weights_sum
45}
46
47/// R/S Hurst exponent for a single window of closes.
48fn hurst_scalar(closes: &[f64], max_lag: usize) -> f64 {
49    let n = closes.len();
50    if n < max_lag * 2 + 1 {
51        return 0.5;
52    }
53    let mut log_lags: Vec<f64> = Vec::new();
54    let mut log_rs: Vec<f64> = Vec::new();
55
56    for lag in 2..=max_lag {
57        let chunks = n / lag;
58        if chunks < 1 {
59            continue;
60        }
61        let mut rs_vals: Vec<f64> = Vec::new();
62        for ci in 0..chunks {
63            let chunk = &closes[ci * lag..(ci + 1) * lag];
64            if chunk.len() < 2 {
65                continue;
66            }
67            let _mean = chunk.iter().sum::<f64>() / chunk.len() as f64;
68            let rets: Vec<f64> = chunk.windows(2).map(|w| w[1] - w[0]).collect();
69            let ret_mean = rets.iter().sum::<f64>() / rets.len() as f64;
70            let devs: Vec<f64> = {
71                let mut cum = 0.0;
72                rets.iter()
73                    .map(|&r| {
74                        cum += r - ret_mean;
75                        cum
76                    })
77                    .collect()
78            };
79            let r = devs.iter().copied().fold(f64::NEG_INFINITY, f64::max)
80                - devs.iter().copied().fold(f64::INFINITY, f64::min);
81            let ddof = rets.len() as f64 - 1.0;
82            let s = if ddof > 0.0 {
83                let var = rets.iter().map(|&x| (x - ret_mean).powi(2)).sum::<f64>() / ddof;
84                var.sqrt()
85            } else {
86                0.0
87            };
88            if s > 1e-12 {
89                rs_vals.push(r / s);
90            }
91        }
92        if !rs_vals.is_empty() {
93            log_lags.push((lag as f64).ln());
94            log_rs.push(rs_vals.iter().sum::<f64>().ln() - (rs_vals.len() as f64).ln());
95        }
96    }
97
98    if log_lags.len() < 3 {
99        return 0.5;
100    }
101    // Linear regression slope
102    let n = log_lags.len() as f64;
103    let mx = log_lags.iter().sum::<f64>() / n;
104    let my = log_rs.iter().sum::<f64>() / n;
105    let num: f64 = log_lags
106        .iter()
107        .zip(log_rs.iter())
108        .map(|(&x, &y)| (x - mx) * (y - my))
109        .sum();
110    let den: f64 = log_lags.iter().map(|&x| (x - mx).powi(2)).sum();
111    if den < 1e-12 {
112        return 0.5;
113    }
114    (num / den).clamp(0.0, 1.0)
115}
116
117// ── Indicators ────────────────────────────────────────────────────────────────
118
119/// Full indicator engine (Layers 1–4, 9–11).
120///
121/// Call [`Indicators::update`] once per closed candle.
122/// After `training_period` candles, [`Indicators::st`] and related fields become `Some`.
123pub struct Indicators {
124    s: BotSettings,
125    maxlen: usize,
126
127    pub opens: VecDeque<f64>,
128    pub highs: VecDeque<f64>,
129    pub lows: VecDeque<f64>,
130    pub closes: VecDeque<f64>,
131    pub volumes: VecDeque<f64>,
132    pub times: VecDeque<i64>,
133    bar: usize,
134
135    // L1 VWAP
136    vwap_vol: f64,
137    vwap_tpv: f64,
138    vwap_date: Option<NaiveDate>,
139
140    // L2 EMA
141    ema9: Option<f64>,
142
143    // L3 SuperTrend
144    rma_atr: Option<f64>,
145    st_upper: Option<f64>,
146    st_lower: Option<f64>,
147    st_dir: i8,
148    st_value: Option<f64>,
149    kmeans_centroids: Option<[f64; 3]>,
150    kmeans_last_bar: usize,
151
152    // L4 TrendSpeed
153    dyn_ema: Option<f64>,
154    prev_close: Option<f64>,
155    max_abs_buf: VecDeque<f64>,
156    delta_buf: VecDeque<f64>,
157    rma_c: Option<f64>,
158    rma_o: Option<f64>,
159    wave_speed: f64,
160    wave_pos: i8,
161    speed_norm: VecDeque<f64>,
162    hma_buf: VecDeque<f64>,
163    bull_waves: VecDeque<f64>,
164    bear_waves: VecDeque<f64>,
165    wr_tracker: PercentileTracker,
166    mom_tracker: PercentileTracker,
167    cur_ratio: f64,
168
169    // L10 Hurst
170    hurst_last_bar: usize,
171
172    // L11 Price acceleration
173    vel_buf: VecDeque<f64>,
174
175    // ── Published fields ─────────────────────────────────────────────────────
176    /// Layer 1 — intraday VWAP, resets at UTC midnight.
177    pub vwap: Option<f64>,
178    /// Layer 2 — EMA of configurable period.
179    pub ema: Option<f64>,
180    /// Layer 3 — SuperTrend line value.
181    pub st: Option<f64>,
182    /// Layer 3 — SuperTrend direction: `-1` = bullish (price above ST), `+1` = bearish.
183    pub st_dir_pub: i8,
184    /// Layer 3 — RMA ATR used for SuperTrend.
185    pub atr: Option<f64>,
186    /// Layer 3 — KMeans cluster index (0 = high vol, 1 = mid, 2 = low vol).
187    pub cluster: usize,
188    /// Layer 4 — dynamic EMA.
189    pub dyn_ema_pub: Option<f64>,
190    /// Layer 4 — HMA-smoothed wave speed.
191    pub ts_speed: f64,
192    /// Layer 4 — wave speed normalised 0–1.
193    pub ts_norm: f64,
194    /// Layer 4 — true when wave speed is positive.
195    pub ts_bullish: bool,
196    /// Layer 4 — average bull wave magnitude.
197    pub bull_avg: f64,
198    /// Layer 4 — average bear wave magnitude.
199    pub bear_avg: f64,
200    /// Layer 4 — bull_avg - |bear_avg|.
201    pub dominance: f64,
202    /// Layer 9 — Awesome Oscillator value.
203    pub ao: f64,
204    /// Layer 9 — true when AO is rising.
205    pub ao_rising: bool,
206    /// Layer 9 — wave ratio percentile.
207    pub wr_pct: f64,
208    /// Layer 9 — momentum percentile.
209    pub mom_pct: f64,
210    pub wave_ok_long: bool,
211    pub wave_ok_short: bool,
212    pub mom_ok_long: bool,
213    pub mom_ok_short: bool,
214    /// Layer 10 — Hurst exponent (0.5 = random, >0.52 = trending).
215    pub hurst: f64,
216    /// Layer 11 — normalised price acceleration (−1 to +1).
217    pub price_accel: f64,
218}
219
220impl Indicators {
221    pub fn new(s: &BotSettings) -> Self {
222        let maxlen = s.history_candles.max(s.training_period + 50).max(300);
223        let mut wr_tracker = PercentileTracker::new(200);
224        // seed tracker so first reads aren't degenerate
225        for i in 0..100 {
226            wr_tracker.push(if i % 2 == 0 { 0.5 } else { 2.0 });
227        }
228        Self {
229            s: s.clone(),
230            maxlen,
231            opens: VecDeque::with_capacity(maxlen),
232            highs: VecDeque::with_capacity(maxlen),
233            lows: VecDeque::with_capacity(maxlen),
234            closes: VecDeque::with_capacity(maxlen),
235            volumes: VecDeque::with_capacity(maxlen),
236            times: VecDeque::with_capacity(maxlen),
237            bar: 0,
238            vwap_vol: 0.0,
239            vwap_tpv: 0.0,
240            vwap_date: None,
241            ema9: None,
242            rma_atr: None,
243            st_upper: None,
244            st_lower: None,
245            st_dir: 1,
246            st_value: None,
247            kmeans_centroids: None,
248            kmeans_last_bar: 0,
249            dyn_ema: None,
250            prev_close: None,
251            max_abs_buf: VecDeque::with_capacity(200),
252            delta_buf: VecDeque::with_capacity(200),
253            rma_c: None,
254            rma_o: None,
255            wave_speed: 0.0,
256            wave_pos: 0,
257            speed_norm: VecDeque::with_capacity(s.ts_collen),
258            hma_buf: VecDeque::new(),
259            bull_waves: VecDeque::with_capacity(s.ts_lookback * 4),
260            bear_waves: VecDeque::with_capacity(s.ts_lookback * 4),
261            wr_tracker,
262            mom_tracker: PercentileTracker::seeded(200, 0.5, 0.5),
263            cur_ratio: 0.0,
264            hurst_last_bar: 0,
265            vel_buf: VecDeque::with_capacity(110),
266            vwap: None,
267            ema: None,
268            st: None,
269            st_dir_pub: 1,
270            atr: None,
271            cluster: 1,
272            dyn_ema_pub: None,
273            ts_speed: 0.0,
274            ts_norm: 0.5,
275            ts_bullish: false,
276            bull_avg: 0.0,
277            bear_avg: 0.0,
278            dominance: 0.0,
279            ao: 0.0,
280            ao_rising: false,
281            wr_pct: 0.5,
282            mom_pct: 0.5,
283            wave_ok_long: true,
284            wave_ok_short: true,
285            mom_ok_long: true,
286            mom_ok_short: true,
287            hurst: 0.5,
288            price_accel: 0.0,
289        }
290    }
291
292    // ── L1 VWAP ───────────────────────────────────────────────────────────────
293
294    fn upd_vwap(&mut self, candle: &Candle) -> f64 {
295        let dt = Utc
296            .timestamp_millis_opt(candle.time)
297            .single()
298            .unwrap_or_else(Utc::now)
299            .date_naive();
300        if Some(dt) != self.vwap_date {
301            self.vwap_vol = 0.0;
302            self.vwap_tpv = 0.0;
303            self.vwap_date = Some(dt);
304        }
305        let tp = candle.typical_price();
306        self.vwap_vol += candle.volume;
307        self.vwap_tpv += tp * candle.volume;
308        if self.vwap_vol > 0.0 {
309            self.vwap_tpv / self.vwap_vol
310        } else {
311            candle.close
312        }
313    }
314
315    // ── L3 RMA ATR ────────────────────────────────────────────────────────────
316
317    fn upd_atr(&mut self, candle: &Candle) -> f64 {
318        let prev_c = self
319            .closes
320            .iter()
321            .rev()
322            .nth(1)
323            .copied()
324            .unwrap_or(candle.close);
325        let tr = (candle.high - candle.low)
326            .max((candle.high - prev_c).abs())
327            .max((candle.low - prev_c).abs());
328        self.rma_atr = Some(rma_step(self.rma_atr, tr, self.s.atr_len));
329        self.rma_atr.unwrap()
330    }
331
332    // ── L3 KMeans ─────────────────────────────────────────────────────────────
333
334    fn kmeans_atr(&mut self, atr_val: f64) -> f64 {
335        if self.kmeans_centroids.is_none() || (self.bar - self.kmeans_last_bar) >= 10 {
336            self.kmeans_centroids = Some(self.compute_kmeans_centroids());
337            self.kmeans_last_bar = self.bar;
338        }
339        let [c_h, c_m, c_l] = self.kmeans_centroids.unwrap();
340        let dists = [
341            (c_h - atr_val).abs(),
342            (c_m - atr_val).abs(),
343            (c_l - atr_val).abs(),
344        ];
345        self.cluster = dists
346            .iter()
347            .enumerate()
348            .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
349            .map_or(1, |(i, _)| i);
350        [c_h, c_m, c_l][self.cluster]
351    }
352
353    fn compute_kmeans_centroids(&self) -> [f64; 3] {
354        let n = self.s.training_period.min(self.closes.len());
355        let ha: Vec<f64> = self.highs.iter().rev().take(n).copied().collect();
356        let la: Vec<f64> = self.lows.iter().rev().take(n).copied().collect();
357        let ca: Vec<f64> = self.closes.iter().rev().take(n).copied().collect();
358
359        // Build RMA ATR series
360        let mut trs = vec![ha[0] - la[0]];
361        for i in 1..n {
362            trs.push(
363                (ha[i] - la[i])
364                    .max((ha[i] - ca[i - 1]).abs())
365                    .max((la[i] - ca[i - 1]).abs()),
366            );
367        }
368        let alpha = 1.0 / self.s.atr_len as f64;
369        let mut atr_w = vec![trs[0]];
370        for i in 1..trs.len() {
371            atr_w.push(alpha * trs[i] + (1.0 - alpha) * atr_w[i - 1]);
372        }
373
374        let lo = atr_w.iter().copied().fold(f64::INFINITY, f64::min);
375        let hi = atr_w.iter().copied().fold(f64::NEG_INFINITY, f64::max);
376        let rng = if (hi - lo).abs() > 1e-9 {
377            hi - lo
378        } else {
379            1e-9
380        };
381
382        let mut c_h = lo + rng * self.s.highvol_pct;
383        let mut c_m = lo + rng * self.s.midvol_pct;
384        let mut c_l = lo + rng * self.s.lowvol_pct;
385
386        for _ in 0..100 {
387            let mut g: [Vec<f64>; 3] = [Vec::new(), Vec::new(), Vec::new()];
388            for &v in &atr_w {
389                let dists = [(v - c_h).abs(), (v - c_m).abs(), (v - c_l).abs()];
390                let idx = dists
391                    .iter()
392                    .enumerate()
393                    .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
394                    .map_or(1, |(i, _)| i);
395                g[idx].push(v);
396            }
397            let nh = if g[0].is_empty() {
398                c_h
399            } else {
400                g[0].iter().sum::<f64>() / g[0].len() as f64
401            };
402            let nm = if g[1].is_empty() {
403                c_m
404            } else {
405                g[1].iter().sum::<f64>() / g[1].len() as f64
406            };
407            let nl = if g[2].is_empty() {
408                c_l
409            } else {
410                g[2].iter().sum::<f64>() / g[2].len() as f64
411            };
412            if (nh - c_h).abs() < 1e-9 && (nm - c_m).abs() < 1e-9 && (nl - c_l).abs() < 1e-9 {
413                break;
414            }
415            c_h = nh;
416            c_m = nm;
417            c_l = nl;
418        }
419        [c_h, c_m, c_l]
420    }
421
422    // ── L3 SuperTrend ─────────────────────────────────────────────────────────
423
424    fn upd_supertrend(&mut self, adaptive_atr: f64, close: f64) -> (f64, i8) {
425        let hl2 = (self.highs.back().copied().unwrap_or(close)
426            + self.lows.back().copied().unwrap_or(close))
427            / 2.0;
428        let factor = self.s.st_factor;
429        let raw_upper = hl2 + factor * adaptive_atr;
430        let raw_lower = hl2 - factor * adaptive_atr;
431
432        let prev_u = self.st_upper.unwrap_or(raw_upper);
433        let prev_l = self.st_lower.unwrap_or(raw_lower);
434        let prev_st = self.st_value.unwrap_or(raw_upper);
435
436        let prev_c = self.closes.iter().rev().nth(1).copied().unwrap_or(close);
437
438        let lower = if raw_lower > prev_l || prev_c < prev_l {
439            raw_lower
440        } else {
441            prev_l
442        };
443        let upper = if raw_upper < prev_u || prev_c > prev_u {
444            raw_upper
445        } else {
446            prev_u
447        };
448
449        let direction = if prev_st == prev_u {
450            if close > upper { -1 } else { 1 }
451        } else {
452            if close < lower { 1 } else { -1 }
453        };
454
455        let st_val = if direction == -1 { lower } else { upper };
456        self.st_upper = Some(upper);
457        self.st_lower = Some(lower);
458        self.st_dir = direction;
459        self.st_value = Some(st_val);
460        (st_val, direction)
461    }
462
463    // ── L4 Trend Speed ────────────────────────────────────────────────────────
464
465    fn upd_trend_speed(&mut self, candle: &Candle) {
466        let cl = candle.close;
467        let op = candle.open;
468        let prev_c = self.prev_close.unwrap_or(cl);
469
470        // Dynamic EMA length based on candle body normalised by recent max
471        let abs_cd = (cl - op).abs();
472        if self.max_abs_buf.len() == 200 {
473            self.max_abs_buf.pop_front();
474        }
475        self.max_abs_buf.push_back(abs_cd);
476        let max_abs = self
477            .max_abs_buf
478            .iter()
479            .copied()
480            .fold(f64::NEG_INFINITY, f64::max)
481            .max(1.0);
482        let cd_norm = (abs_cd + max_abs) / (2.0 * max_abs);
483        let dyn_len = 5.0 + cd_norm * (self.s.ts_max_length as f64 - 5.0);
484
485        let delta = (cl - prev_c).abs();
486        if self.delta_buf.len() == 200 {
487            self.delta_buf.pop_front();
488        }
489        self.delta_buf.push_back(delta);
490        let max_d = self
491            .delta_buf
492            .iter()
493            .copied()
494            .fold(f64::NEG_INFINITY, f64::max)
495            .max(1.0);
496        let accel = delta / max_d;
497
498        let alpha = (2.0 / (dyn_len + 1.0) * (1.0 + accel * self.s.ts_accel_mult)).min(1.0);
499        self.dyn_ema = Some(match self.dyn_ema {
500            None => cl,
501            Some(prev) => alpha * cl + (1.0 - alpha) * prev,
502        });
503        self.dyn_ema_pub = self.dyn_ema;
504
505        self.rma_c = Some(rma_step(self.rma_c, cl, self.s.ts_rma_len));
506        self.rma_o = Some(rma_step(self.rma_o, op, self.s.ts_rma_len));
507
508        let trend = self.dyn_ema.unwrap();
509        let prev_cl = self.closes.iter().rev().nth(1).copied().unwrap_or(cl);
510        let c_rma = self.rma_c.unwrap_or(0.0);
511        let o_rma = self.rma_o.unwrap_or(0.0);
512
513        if cl > trend && prev_cl <= trend {
514            if self.wave_pos != 0 {
515                if self.bear_waves.len() == self.s.ts_lookback * 4 {
516                    self.bear_waves.pop_front();
517                }
518                self.bear_waves.push_back(self.wave_speed);
519            }
520            self.wave_pos = 1;
521            self.wave_speed = c_rma - o_rma;
522        } else if cl < trend && prev_cl >= trend {
523            if self.wave_pos != 0 {
524                if self.bull_waves.len() == self.s.ts_lookback * 4 {
525                    self.bull_waves.pop_front();
526                }
527                self.bull_waves.push_back(self.wave_speed);
528            }
529            self.wave_pos = -1;
530            self.wave_speed = c_rma - o_rma;
531        } else {
532            self.wave_speed += c_rma - o_rma;
533        }
534
535        if self.speed_norm.len() == self.s.ts_collen {
536            self.speed_norm.pop_front();
537        }
538        self.speed_norm.push_back(self.wave_speed);
539
540        // HMA of speed_norm
541        self.ts_speed = self.hma_smooth(self.s.ts_hma_len);
542        self.ts_bullish = self.ts_speed > 0.0;
543
544        let sp_min = self
545            .speed_norm
546            .iter()
547            .copied()
548            .fold(f64::INFINITY, f64::min);
549        let sp_max = self
550            .speed_norm
551            .iter()
552            .copied()
553            .fold(f64::NEG_INFINITY, f64::max);
554        let sp_rng = if (sp_max - sp_min).abs() > 1e-9 {
555            sp_max - sp_min
556        } else {
557            1.0
558        };
559        self.ts_norm = (self.wave_speed - sp_min) / sp_rng;
560
561        let lb = self.s.ts_lookback;
562        let bull_r: Vec<f64> = self.bull_waves.iter().rev().take(lb).copied().collect();
563        let bear_r: Vec<f64> = self.bear_waves.iter().rev().take(lb).copied().collect();
564        self.bull_avg = if bull_r.is_empty() {
565            0.0
566        } else {
567            bull_r.iter().sum::<f64>() / bull_r.len() as f64
568        };
569        self.bear_avg = if bear_r.is_empty() {
570            0.0
571        } else {
572            bear_r.iter().sum::<f64>() / bear_r.len() as f64
573        };
574        self.dominance = self.bull_avg - self.bear_avg.abs();
575        self.prev_close = Some(cl);
576
577        // Wave ratio percentile
578        let bear_abs = self.bear_avg.abs().max(1e-9);
579        let wave_ratio = if self.bull_avg > 0.0 {
580            self.bull_avg / bear_abs
581        } else {
582            1.0 / bear_abs
583        };
584        self.wr_tracker.push(wave_ratio);
585        self.wr_pct = self.wr_tracker.pct(wave_ratio);
586
587        // Momentum percentile
588        self.cur_ratio = if self.wave_speed > 0.0 && self.bull_avg > 0.0 {
589            self.wave_speed / self.bull_avg
590        } else if self.wave_speed < 0.0 && bear_abs > 0.0 {
591            -self.wave_speed.abs() / bear_abs
592        } else {
593            0.0
594        };
595        self.mom_tracker.push(self.cur_ratio.abs());
596        self.mom_pct = self.mom_tracker.pct(self.cur_ratio.abs());
597
598        let wl = self.s.wave_pct_l.clamp(0.01, 0.99);
599        let ws = (1.0 - self.s.wave_pct_s).clamp(0.01, 0.99);
600        let ml = self.s.mom_pct_min.clamp(0.01, 0.99);
601
602        self.wave_ok_long = self.wr_pct >= wl;
603        self.wave_ok_short = self.wr_pct <= ws;
604        self.mom_ok_long = self.mom_pct >= ml && self.cur_ratio > 0.0;
605        self.mom_ok_short = self.mom_pct >= ml && self.cur_ratio < 0.0;
606    }
607
608    /// HMA: 2*WMA(n/2) - WMA(n), then WMA(√n) of that.
609    fn hma_smooth(&mut self, length: usize) -> f64 {
610        let sn: Vec<f64> = self.speed_norm.iter().copied().collect();
611        if sn.len() < 2 {
612            return *sn.last().unwrap_or(&0.0);
613        }
614        let half = (length / 2).max(1);
615        let sqrt_n = (length as f64).sqrt().round() as usize;
616        let raw = 2.0 * wma(&sn[sn.len().saturating_sub(half)..])
617            - wma(&sn[sn.len().saturating_sub(length)..]);
618        if self.hma_buf.len() == sqrt_n {
619            self.hma_buf.pop_front();
620        }
621        self.hma_buf.push_back(raw);
622        let hma_arr: Vec<f64> = self.hma_buf.iter().copied().collect();
623        wma(&hma_arr)
624    }
625
626    // ── L9 Awesome Oscillator ────────────────────────────────────────────────
627
628    fn upd_ao(&mut self) {
629        if self.highs.len() < 34 {
630            return;
631        }
632        let hs: Vec<f64> = self.highs.iter().copied().collect();
633        let ls: Vec<f64> = self.lows.iter().copied().collect();
634        let hl2: Vec<f64> = hs
635            .iter()
636            .zip(ls.iter())
637            .map(|(h, l)| (h + l) / 2.0)
638            .collect();
639        let n = hl2.len();
640        let ao_new =
641            hl2[n - 5..].iter().sum::<f64>() / 5.0 - hl2[n - 34..].iter().sum::<f64>() / 34.0;
642        self.ao_rising = ao_new > self.ao;
643        self.ao = ao_new;
644    }
645
646    // ── L10 Hurst ─────────────────────────────────────────────────────────────
647
648    fn upd_hurst(&mut self) {
649        let lb = self.s.hurst_lookback;
650        let min_bars = lb * 2 + 1;
651        if self.closes.len() < min_bars || (self.bar - self.hurst_last_bar) < 10 {
652            return;
653        }
654        let cl_arr: Vec<f64> = self.closes.iter().rev().take(min_bars).copied().collect();
655        self.hurst = hurst_scalar(&cl_arr, lb);
656        self.hurst_last_bar = self.bar;
657    }
658
659    // ── L11 Price acceleration ────────────────────────────────────────────────
660
661    fn upd_accel(&mut self) {
662        let k = 3usize;
663        let n = self.closes.len();
664        if n <= k * 2 {
665            return;
666        }
667        let cl: Vec<f64> = self.closes.iter().copied().collect();
668        let vel_now = (cl[n - 1] - cl[n - 1 - k]) / (cl[n - 1 - k] + 1e-10);
669        let vel_prev = (cl[n - 1 - k] - cl[n - 1 - k * 2]) / (cl[n - 1 - k * 2] + 1e-10);
670        if self.vel_buf.len() == 110 {
671            self.vel_buf.pop_front();
672        }
673        self.vel_buf.push_back(vel_now);
674        let accel = vel_now - vel_prev;
675        let vel_std = if self.vel_buf.len() > 1 {
676            let vv: Vec<f64> = self.vel_buf.iter().copied().collect();
677            let mean = vv.iter().sum::<f64>() / vv.len() as f64;
678            let var = vv.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / vv.len() as f64;
679            var.sqrt()
680        } else {
681            1.0
682        };
683        self.price_accel = (accel / (vel_std + 1e-10) / 3.0).clamp(-1.0, 1.0);
684    }
685
686    // ── Main update ───────────────────────────────────────────────────────────
687
688    /// Feed one closed candle. Returns `true` once SuperTrend is ready.
689    pub fn update(&mut self, candle: &Candle) -> bool {
690        let cap = self.maxlen;
691        macro_rules! push {
692            ($buf:expr, $val:expr) => {
693                if $buf.len() == cap {
694                    $buf.pop_front();
695                }
696                $buf.push_back($val);
697            };
698        }
699        push!(self.opens, candle.open);
700        push!(self.highs, candle.high);
701        push!(self.lows, candle.low);
702        push!(self.closes, candle.close);
703        push!(self.volumes, candle.volume);
704        push!(self.times, candle.time);
705        self.bar += 1;
706
707        self.vwap = Some(self.upd_vwap(candle));
708
709        let k = 2.0 / (self.s.ema_len as f64 + 1.0);
710        self.ema9 = Some(match self.ema9 {
711            None => candle.close,
712            Some(e) => candle.close * k + e * (1.0 - k),
713        });
714        self.ema = self.ema9;
715
716        let atr_val = self.upd_atr(candle);
717        self.atr = Some(atr_val);
718
719        self.upd_trend_speed(candle);
720        self.upd_ao();
721        self.upd_hurst();
722        self.upd_accel();
723
724        if self.closes.len() < self.s.training_period {
725            return false;
726        }
727
728        let adaptive_atr = self.kmeans_atr(atr_val);
729        let (st, dir) = self.upd_supertrend(adaptive_atr, candle.close);
730        self.st = Some(st);
731        self.st_dir_pub = dir;
732
733        true
734    }
735
736    /// Returns `true` if a speed-exit condition is triggered for the given position.
737    /// `position`: +1 = long, -1 = short.
738    pub fn check_speed_exit(&self, position: i32) -> bool {
739        let Some(thr) = self.s.ts_speed_exit_threshold else {
740            return false;
741        };
742        if position > 0 && self.ts_speed < -thr.abs() {
743            return true;
744        }
745        position < 0 && self.ts_speed > thr.abs()
746    }
747}