Skip to main content

volas_compute/indicators/
group_b.rs

1//! Group B indicators (gap report 2026-06-07, §9): convention-sensitive market
2//! indicators. Each kernel pins ONE authoritative convention, cited inline and matched
3//! bit-for-shape by the source-pinned oracle (`test/oracle_reference.py`). Finite-memory
4//! members refresh on `append` through the engine's windowed fast-path (no state-carry);
5//! recursive members carry a `*_final_state` / `*_resume` pair like Group A.
6
7use crate::indicators::av;
8use crate::kernels;
9
10/// Rolling `n`-window sum that skips a leading-NaN prefix — so a series with an undefined
11/// first bar (`tr`, prev-close diffs) sums from its first finite value — via `sma * n`.
12fn rolling_sum(x: &[f64], n: usize) -> Vec<f64> {
13    (kernels::sma(av(x), n) * n as f64).to_vec()
14}
15
16/// Vortex Indicator (+VI / −VI): `+VM = |high − prev low|`, `−VM = |low − prev high|`;
17/// `+VI = Σₙ(+VM) / Σₙ(TR)`, `−VI = Σₙ(−VM) / Σₙ(TR)`. `plus` selects the +VI line.
18/// Source: StockCharts ChartSchool / Wikipedia — Vortex Indicator.
19pub fn vortex(high: &[f64], low: &[f64], close: &[f64], n: usize, plus: bool) -> Vec<f64> {
20    let len = high.len();
21    let tr = super::tr(high, low, close); // NaN at bar 0
22    let vm: Vec<f64> = (0..len)
23        .map(|i| {
24            if i == 0 {
25                f64::NAN
26            } else if plus {
27                (high[i] - low[i - 1]).abs()
28            } else {
29                (low[i] - high[i - 1]).abs()
30            }
31        })
32        .collect();
33    let sum_vm = rolling_sum(&vm, n);
34    let sum_tr = rolling_sum(&tr, n);
35    (0..len).map(|i| sum_vm[i] / sum_tr[i]).collect()
36}
37
38/// BRAR — AR (人气指标) = `Σₙ(H − O) / Σₙ(O − L) × 100`. `H − O` and `O − L` are always ≥ 0.
39/// Source: 通达信 / MBA智库 — 人气意愿指标 (BRAR).
40pub fn brar_ar(open: &[f64], high: &[f64], low: &[f64], n: usize) -> Vec<f64> {
41    let len = high.len();
42    let ho: Vec<f64> = (0..len).map(|i| high[i] - open[i]).collect();
43    let ol: Vec<f64> = (0..len).map(|i| open[i] - low[i]).collect();
44    let (s_ho, s_ol) = (rolling_sum(&ho, n), rolling_sum(&ol, n));
45    (0..len).map(|i| s_ho[i] / s_ol[i] * 100.0).collect()
46}
47
48/// BRAR — BR (意愿指标) = `Σₙ max(0, H − Cᵧ) / Σₙ max(0, Cᵧ − L) × 100`, `Cᵧ` = prior close.
49/// The `max(0, …)` clamp is the 通达信 convention (a high below — or low above — the prior
50/// close contributes nothing). Source: 通达信 / MBA智库 — 人气意愿指标 (BRAR).
51pub fn brar_br(high: &[f64], low: &[f64], close: &[f64], n: usize) -> Vec<f64> {
52    let len = high.len();
53    let up: Vec<f64> = (0..len)
54        .map(|i| if i == 0 { f64::NAN } else { (high[i] - close[i - 1]).max(0.0) })
55        .collect();
56    let dn: Vec<f64> = (0..len)
57        .map(|i| if i == 0 { f64::NAN } else { (close[i - 1] - low[i]).max(0.0) })
58        .collect();
59    let (s_up, s_dn) = (rolling_sum(&up, n), rolling_sum(&dn, n));
60    (0..len).map(|i| s_up[i] / s_dn[i] * 100.0).collect()
61}
62
63/// VR 成交量比率 = `(UVS + ½·PVS) / (DVS + ½·PVS) × 100` over `n` bars, where UVS / DVS / PVS
64/// are the summed volumes of up- / down- / flat-close days (vs the prior close).
65/// Source: MBA智库 — 成交量比率 (VR).
66pub fn vr(close: &[f64], volume: &[f64], n: usize) -> Vec<f64> {
67    let len = close.len();
68    let (mut uv, mut dv, mut pv) = (vec![f64::NAN; len], vec![f64::NAN; len], vec![f64::NAN; len]);
69    for i in 1..len {
70        let (mut u, mut d, mut p) = (0.0, 0.0, 0.0);
71        if close[i] > close[i - 1] {
72            u = volume[i];
73        } else if close[i] < close[i - 1] {
74            d = volume[i];
75        } else {
76            p = volume[i];
77        }
78        uv[i] = u;
79        dv[i] = d;
80        pv[i] = p;
81    }
82    let (suv, sdv, spv) = (rolling_sum(&uv, n), rolling_sum(&dv, n), rolling_sum(&pv, n));
83    (0..len)
84        .map(|i| (suv[i] + 0.5 * spv[i]) / (sdv[i] + 0.5 * spv[i]) * 100.0)
85        .collect()
86}
87
88/// Coppock Curve = `WMA_w(ROC_long + ROC_short)`, `ROC_p = (C / C₋ₚ − 1) × 100`.
89/// Source: StockCharts ChartSchool / Wikipedia — Coppock Curve.
90pub fn coppock(close: &[f64], w: usize, roc_long: usize, roc_short: usize) -> Vec<f64> {
91    let len = close.len();
92    let roc = |p: usize| -> Vec<f64> {
93        (0..len)
94            .map(|i| if i >= p { (close[i] / close[i - p] - 1.0) * 100.0 } else { f64::NAN })
95            .collect::<Vec<_>>()
96    };
97    let (rl, rs) = (roc(roc_long), roc(roc_short));
98    let sum: Vec<f64> = (0..len).map(|i| rl[i] + rs[i]).collect();
99    super::wma(&sum, w)
100}
101
102/// 4-bar symmetric weighted MA, weights `[1, 2, 2, 1] / 6` (newest first); NaN until 3 prior
103/// bars exist. The RVI numerator / denominator / signal smoother.
104fn swma4(x: &[f64]) -> Vec<f64> {
105    (0..x.len())
106        .map(|i| {
107            if i >= 3 {
108                (x[i] + 2.0 * x[i - 1] + 2.0 * x[i - 2] + x[i - 3]) / 6.0
109            } else {
110                f64::NAN
111            }
112        })
113        .collect()
114}
115
116/// Relative Vigor Index = `SMAₙ(swma4(C − O)) / SMAₙ(swma4(H − L))`.
117/// Source: Fidelity / MetaTrader — Relative Vigor Index.
118pub fn relative_vigor(open: &[f64], high: &[f64], low: &[f64], close: &[f64], n: usize) -> Vec<f64> {
119    let len = close.len();
120    let co: Vec<f64> = (0..len).map(|i| close[i] - open[i]).collect();
121    let hl: Vec<f64> = (0..len).map(|i| high[i] - low[i]).collect();
122    let num = kernels::sma(av(&swma4(&co)), n);
123    let den = kernels::sma(av(&swma4(&hl)), n);
124    (0..len).map(|i| num[i] / den[i]).collect()
125}
126
127/// RVI signal line = `swma4(RVI)`. Source: Fidelity / MetaTrader — Relative Vigor Index.
128pub fn relative_vigor_signal(
129    open: &[f64],
130    high: &[f64],
131    low: &[f64],
132    close: &[f64],
133    n: usize,
134) -> Vec<f64> {
135    swma4(&relative_vigor(open, high, low, close, n))
136}
137
138/// DKX 多空线 = `WMA(MID, 20)`, `MID = (3·C + L + O + H) / 6`. The fixed 20-period linear
139/// weights 20…1 are exactly TA-Lib WMA's. Source: 百度百科 / 东方财富 — 多空线 (DKX).
140pub fn dkx(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<f64> {
141    let mid: Vec<f64> = (0..close.len())
142        .map(|i| (3.0 * close[i] + low[i] + open[i] + high[i]) / 6.0)
143        .collect();
144    super::wma(&mid, 20)
145}
146
147/// MADKX = `SMA_m(DKX)`, the DKX signal line. Source: 百度百科 / 东方财富 — 多空线 (DKX).
148pub fn dkx_ma(open: &[f64], high: &[f64], low: &[f64], close: &[f64], m: usize) -> Vec<f64> {
149    super::ma(&dkx(open, high, low, close), m)
150}
151
152/// WVAD 威廉变异离散量 = `Σₙ( (C − O) / (H − L) · V )`. Source: 通达信 / MBA智库 — WVAD.
153pub fn wvad(
154    open: &[f64],
155    high: &[f64],
156    low: &[f64],
157    close: &[f64],
158    volume: &[f64],
159    n: usize,
160) -> Vec<f64> {
161    let w: Vec<f64> = (0..close.len())
162        .map(|i| (close[i] - open[i]) / (high[i] - low[i]) * volume[i])
163        .collect();
164    rolling_sum(&w, n)
165}
166
167/// Which CDP support/resistance line to emit.
168#[derive(Clone, Copy)]
169pub enum CdpLine {
170    Cdp,
171    Ah,
172    Nh,
173    Nl,
174    Al,
175}
176
177/// CDP 逆势操作: from the PRIOR bar, `CDP = (H + L + 2C) / 4`, then `AH = CDP + (H − L)`,
178/// `NH = 2·CDP − L`, `NL = 2·CDP − H`, `AL = CDP − (H − L)` — five intraday levels.
179/// Source: 百度百科 / 维基 — 逆势操作 (CDP).
180pub fn cdp(high: &[f64], low: &[f64], close: &[f64], line: CdpLine) -> Vec<f64> {
181    (0..high.len())
182        .map(|i| {
183            if i == 0 {
184                return f64::NAN;
185            }
186            let (h, l, c) = (high[i - 1], low[i - 1], close[i - 1]);
187            let cdp = (h + l + 2.0 * c) / 4.0;
188            match line {
189                CdpLine::Cdp => cdp,
190                CdpLine::Ah => cdp + (h - l),
191                CdpLine::Nh => 2.0 * cdp - l,
192                CdpLine::Nl => 2.0 * cdp - h,
193                CdpLine::Al => cdp - (h - l),
194            }
195        })
196        .collect()
197}
198
199/// WAD Williams Accumulation/Distribution (威廉多空力度线), cumulative: on an up close
200/// `+= C − min(prev C, L)`, on a down close `+= C − max(prev C, H)`, otherwise unchanged.
201/// Distinct from TA-Lib's AD. Source: Larry Williams / Tulip Indicators — Williams A/D.
202pub fn wad(high: &[f64], low: &[f64], close: &[f64]) -> Vec<f64> {
203    let mut out = vec![0.0; close.len()];
204    let mut acc = 0.0;
205    for i in 1..close.len() {
206        acc += wad_step(high[i], low[i], close[i], close[i - 1]);
207        out[i] = acc;
208    }
209    out
210}
211
212/// One WAD increment vs the prior close.
213fn wad_step(h: f64, l: f64, c: f64, prev_c: f64) -> f64 {
214    if c > prev_c {
215        c - prev_c.min(l)
216    } else if c < prev_c {
217        c - prev_c.max(h)
218    } else {
219        0.0
220    }
221}
222
223/// `[wad, prev_close]` after a full [`wad`] — `None` for an empty series.
224pub fn wad_final_state(high: &[f64], low: &[f64], close: &[f64]) -> Option<Vec<f64>> {
225    let n = close.len();
226    if n == 0 {
227        return None;
228    }
229    let mut acc = 0.0;
230    for i in 1..n {
231        acc += wad_step(high[i], low[i], close[i], close[i - 1]);
232    }
233    Some(vec![acc, close[n - 1]])
234}
235
236/// Resume [`wad`] from `[wad_{from-1}, close_{from-1}]` over `[from, n)`, bit-identical to a
237/// full recompute.
238pub fn wad_resume(
239    high: &[f64],
240    low: &[f64],
241    close: &[f64],
242    from: usize,
243    state: &[f64],
244) -> (Vec<f64>, Vec<f64>) {
245    let mut acc = state[0];
246    let mut prev = state[1];
247    let mut out = Vec::with_capacity(close.len().saturating_sub(from));
248    for i in from..close.len() {
249        acc += wad_step(high[i], low[i], close[i], prev);
250        out.push(acc);
251        prev = close[i];
252    }
253    (out, vec![acc, prev])
254}
255
256/// One Wilder Swing Index increment. `op` / `cp` are the prior bar's open / close; `t` is the
257/// limit-move scaling. `SI = 50·(N/R)·(K/T)` with Wilder's R selected by the largest of
258/// `|H−Cp|`, `|L−Cp|`, `|H−L|`.
259fn asi_si(o: f64, h: f64, l: f64, c: f64, op: f64, cp: f64, t: f64) -> f64 {
260    let n = (c - cp) + 0.5 * (c - o) + 0.25 * (cp - op);
261    let (tr1, tr2, tr3) = ((h - cp).abs(), (l - cp).abs(), (h - l).abs());
262    let move_term = 0.25 * (cp - op).abs();
263    let r = if tr1 >= tr2 && tr1 >= tr3 {
264        tr1 - 0.5 * tr2 + move_term
265    } else if tr2 >= tr1 && tr2 >= tr3 {
266        tr2 - 0.5 * tr1 + move_term
267    } else {
268        tr3 + move_term
269    };
270    let k = tr1.max(tr2);
271    50.0 * (n / r) * (k / t)
272}
273
274/// ASI Accumulative Swing Index (Wilder, 振动升降指标), cumulative: `ASI = Σ SI`, with the
275/// Swing Index per [`asi_si`]. `t` is Wilder's limit-move parameter. Source: J. Welles Wilder,
276/// *New Concepts in Technical Trading Systems*.
277pub fn asi(open: &[f64], high: &[f64], low: &[f64], close: &[f64], t: f64) -> Vec<f64> {
278    let mut out = vec![0.0; close.len()];
279    let mut acc = 0.0;
280    for i in 1..close.len() {
281        acc += asi_si(open[i], high[i], low[i], close[i], open[i - 1], close[i - 1], t);
282        out[i] = acc;
283    }
284    out
285}
286
287/// `[asi, prev_close, prev_open]` after a full [`asi`] — `None` for an empty series.
288pub fn asi_final_state(
289    open: &[f64],
290    high: &[f64],
291    low: &[f64],
292    close: &[f64],
293    t: f64,
294) -> Option<Vec<f64>> {
295    let n = close.len();
296    if n == 0 {
297        return None;
298    }
299    let mut acc = 0.0;
300    for i in 1..n {
301        acc += asi_si(open[i], high[i], low[i], close[i], open[i - 1], close[i - 1], t);
302    }
303    Some(vec![acc, close[n - 1], open[n - 1]])
304}
305
306/// Resume [`asi`] from `[asi_{from-1}, close_{from-1}, open_{from-1}]` over `[from, n)`.
307pub fn asi_resume(
308    open: &[f64],
309    high: &[f64],
310    low: &[f64],
311    close: &[f64],
312    t: f64,
313    from: usize,
314    state: &[f64],
315) -> (Vec<f64>, Vec<f64>) {
316    let (mut acc, mut pc, mut po) = (state[0], state[1], state[2]);
317    let mut out = Vec::with_capacity(close.len().saturating_sub(from));
318    for i in from..close.len() {
319        acc += asi_si(open[i], high[i], low[i], close[i], po, pc, t);
320        out.push(acc);
321        pc = close[i];
322        po = open[i];
323    }
324    (out, vec![acc, pc, po])
325}
326
327/// Which standard pivot-point level to emit.
328#[derive(Clone, Copy)]
329pub enum PivotLine {
330    Pp,
331    R1,
332    S1,
333    R2,
334    S2,
335    R3,
336    S3,
337}
338
339/// Standard floor Pivot Points, from the PRIOR bar: `PP = (H + L + C) / 3`; `R1 = 2·PP − L`,
340/// `S1 = 2·PP − H`, `R2 = PP + (H − L)`, `S2 = PP − (H − L)`, `R3 = H + 2·(PP − L)`,
341/// `S3 = L − 2·(H − PP)`. Source: Investopedia / floor-trader standard — Pivot Points.
342pub fn pivot_points(high: &[f64], low: &[f64], close: &[f64], line: PivotLine) -> Vec<f64> {
343    (0..high.len())
344        .map(|i| {
345            if i == 0 {
346                return f64::NAN;
347            }
348            let (h, l, c) = (high[i - 1], low[i - 1], close[i - 1]);
349            let pp = (h + l + c) / 3.0;
350            match line {
351                PivotLine::Pp => pp,
352                PivotLine::R1 => 2.0 * pp - l,
353                PivotLine::S1 => 2.0 * pp - h,
354                PivotLine::R2 => pp + (h - l),
355                PivotLine::S2 => pp - (h - l),
356                PivotLine::R3 => h + 2.0 * (pp - l),
357                PivotLine::S3 => l - 2.0 * (h - pp),
358            }
359        })
360        .collect()
361}
362
363/// Which MIKE support / resistance line to emit.
364#[derive(Clone, Copy)]
365pub enum MikeLine {
366    WeakR,
367    MidR,
368    StrongR,
369    WeakS,
370    MidS,
371    StrongS,
372}
373
374/// MIKE 麦克支撑压力: `TYP = (H+L+C)/3`, `HH`/`LL` = n-day high-of-high / low-of-low.
375/// Resistance WEKR = TYP+(TYP−LL), MIDR = TYP+(HH−LL), STOR = 2·HH−LL; support mirrors them:
376/// WEKS = TYP−(HH−TYP), MIDS = TYP−(HH−LL), STOS = 2·LL−HH. Source: 百度百科 / MBA智库 — 麦克指标.
377pub fn mike(high: &[f64], low: &[f64], close: &[f64], n: usize, line: MikeLine) -> Vec<f64> {
378    let len = high.len();
379    let hh = super::hhv(high, n);
380    let ll = super::llv(low, n);
381    (0..len)
382        .map(|i| {
383            let typ = (high[i] + low[i] + close[i]) / 3.0;
384            let (hi, lo) = (hh[i], ll[i]);
385            match line {
386                MikeLine::WeakR => typ + (typ - lo),
387                MikeLine::MidR => typ + (hi - lo),
388                MikeLine::StrongR => 2.0 * hi - lo,
389                MikeLine::WeakS => typ - (hi - typ),
390                MikeLine::MidS => typ - (hi - lo),
391                MikeLine::StrongS => 2.0 * lo - hi,
392            }
393        })
394        .collect()
395}
396
397/// Keltner Channel band (modern convention): `EMA(close, ema_period) ± mult · ATR(atr_period)`.
398/// `upper` selects the `+` band. The middle line is just `EMA(close)`, so callers emit that
399/// via `ema` directly. Source: StockCharts ChartSchool — Keltner Channels.
400pub fn keltner_band(
401    close: &[f64],
402    high: &[f64],
403    low: &[f64],
404    ema_period: usize,
405    atr_period: usize,
406    mult: f64,
407    upper: bool,
408) -> Vec<f64> {
409    let ema = super::ema(close, ema_period);
410    let atr = super::atr(high, low, close, atr_period);
411    let sign = if upper { 1.0 } else { -1.0 };
412    (0..close.len()).map(|i| ema[i] + sign * mult * atr[i]).collect()
413}
414
415/// Keltner Channel Width (TradingView `ta.kcw`): the channel span normalized by
416/// the basis, `(upper - lower) / middle = 2·mult·ATR(atr_period) / EMA(ema_period)`.
417/// This is the width of volas's OWN Keltner Channel (EMA basis + ATR range), so
418/// `kcw` always equals `(keltner.upper - keltner.lower) / keltner` for the same
419/// parameters. Pine's `ta.kcw` uses an EMA-of-range basis instead of ATR — a
420/// documented divergence that keeps volas's Keltner family internally consistent.
421pub fn kcw(
422    close: &[f64],
423    high: &[f64],
424    low: &[f64],
425    ema_period: usize,
426    atr_period: usize,
427    mult: f64,
428) -> Vec<f64> {
429    let ema = super::ema(close, ema_period);
430    let atr = super::atr(high, low, close, atr_period);
431    (0..close.len())
432        .map(|i| {
433            if ema[i] != 0.0 {
434                2.0 * mult * atr[i] / ema[i]
435            } else {
436                f64::NAN
437            }
438        })
439        .collect()
440}
441
442/// Keltner band state `[ema, atr]` after a full compute, or `None` before both seed. The
443/// middle line reuses `ema_final_state`. Source: StockCharts ChartSchool — Keltner Channels.
444pub fn keltner_band_final_state(
445    close: &[f64],
446    high: &[f64],
447    low: &[f64],
448    ema_period: usize,
449    atr_period: usize,
450) -> Option<Vec<f64>> {
451    let e = super::ema_final_state(close, ema_period)?;
452    let a = super::atr_final_state(high, low, close, atr_period)?;
453    Some(vec![e[0], a[0]])
454}
455
456/// Resume a Keltner band over `[from, n)` by composing the EMA and ATR resumes — bit-identical
457/// to a full recompute. `None` at `from == 0` (the ATR resume declines).
458// One parameter per series/state the resume reads; a struct would only repackage them.
459#[allow(clippy::too_many_arguments)]
460pub fn keltner_band_resume(
461    close: &[f64],
462    high: &[f64],
463    low: &[f64],
464    ema_period: usize,
465    atr_period: usize,
466    mult: f64,
467    upper: bool,
468    from: usize,
469    state: &[f64],
470) -> Option<(Vec<f64>, Vec<f64>)> {
471    let (e_vals, e_state) = super::ema_resume(close, ema_period, from, &state[0..1]);
472    let (a_vals, a_state) = super::atr_resume(high, low, close, atr_period, from, &state[1..2])?;
473    let sign = if upper { 1.0 } else { -1.0 };
474    let vals: Vec<f64> = (0..e_vals.len()).map(|i| e_vals[i] + sign * mult * a_vals[i]).collect();
475    Some((vals, vec![e_state[0], a_state[0]]))
476}
477
478/// Stochastic Momentum Index (Blau / LazyBear): `HH`/`LL` = k-day high/low; `D = C − (HH+LL)/2`,
479/// `Ds = EMA_d(EMA_d(D))`, `Dhl = EMA_d(EMA_d(HH−LL))`; `SMI = Ds / (Dhl/2) × 100`. The short
480/// double-EMA smoothing keeps it effectively finite-memory. Source: William Blau / LazyBear — SMI.
481pub fn stoch_momentum(high: &[f64], low: &[f64], close: &[f64], k: usize, d: usize) -> Vec<f64> {
482    let len = high.len();
483    let hh = super::hhv(high, k);
484    let ll = super::llv(low, k);
485    let rdiff: Vec<f64> = (0..len).map(|i| close[i] - (hh[i] + ll[i]) / 2.0).collect();
486    let diff: Vec<f64> = (0..len).map(|i| hh[i] - ll[i]).collect();
487    let ds = super::ema(&super::ema(&rdiff, d), d);
488    let dhl = super::ema(&super::ema(&diff, d), d);
489    (0..len).map(|i| ds[i] / (dhl[i] * 0.5) * 100.0).collect()
490}
491
492/// SMI signal line = `EMA_signal(SMI)`. Source: William Blau / LazyBear — SMI.
493pub fn stoch_momentum_signal(
494    high: &[f64],
495    low: &[f64],
496    close: &[f64],
497    k: usize,
498    d: usize,
499    signal: usize,
500) -> Vec<f64> {
501    super::ema(&stoch_momentum(high, low, close, k, d), signal)
502}
503
504/// Shift a series forward by `k` bars: `out[i] = x[i-k]` (NaN for the first `k`). Used to
505/// displace Ichimoku's leading spans to the bar where they are read (a causal, leading-NaN
506/// line — the future projection beyond the data is a charting concern, not a value one).
507fn shift_forward(x: &[f64], k: usize) -> Vec<f64> {
508    (0..x.len()).map(|i| if i >= k { x[i - k] } else { f64::NAN }).collect()
509}
510
511/// Which Ichimoku line to emit.
512#[derive(Clone, Copy)]
513pub enum IchimokuLine {
514    Tenkan,
515    Kijun,
516    SenkouA,
517    SenkouB,
518    Chikou,
519}
520
521/// Ichimoku Cloud. Tenkan = `(HH_t + LL_t)/2`, Kijun = `(HH_k + LL_k)/2`; Senkou A =
522/// `(Tenkan+Kijun)/2` and Senkou B = `(HH_sb + LL_sb)/2`, each displaced forward `kijun`
523/// bars to the bar where they apply; Chikou = the close (the lagging span — plotted `kijun`
524/// bars back, returned causally so it never depends on a future bar). Source: StockCharts /
525/// Fidelity — Ichimoku Cloud.
526pub fn ichimoku(
527    high: &[f64],
528    low: &[f64],
529    close: &[f64],
530    tenkan: usize,
531    kijun: usize,
532    senkou_b: usize,
533    line: IchimokuLine,
534) -> Vec<f64> {
535    let midpoint = |period: usize| -> Vec<f64> {
536        let hh = super::hhv(high, period);
537        let ll = super::llv(low, period);
538        (0..high.len()).map(|i| (hh[i] + ll[i]) / 2.0).collect()
539    };
540    match line {
541        IchimokuLine::Tenkan => midpoint(tenkan),
542        IchimokuLine::Kijun => midpoint(kijun),
543        IchimokuLine::SenkouA => {
544            let (t, k) = (midpoint(tenkan), midpoint(kijun));
545            let raw: Vec<f64> = (0..high.len()).map(|i| (t[i] + k[i]) / 2.0).collect();
546            shift_forward(&raw, kijun)
547        }
548        IchimokuLine::SenkouB => shift_forward(&midpoint(senkou_b), kijun),
549        IchimokuLine::Chikou => close.to_vec(),
550    }
551}
552
553/// TTM Squeeze momentum = `linregₙ( C − ((HHₙ + LLₙ)/2 + SMAₙ(C)) / 2 )` (John Carter /
554/// thinkorswim). Source: John Carter / StockCharts — TTM Squeeze.
555pub fn ttm_squeeze_momentum(high: &[f64], low: &[f64], close: &[f64], n: usize) -> Vec<f64> {
556    let len = high.len();
557    let hh = super::hhv(high, n);
558    let ll = super::llv(low, n);
559    let sma = super::ma(close, n);
560    let delta: Vec<f64> = (0..len)
561        .map(|i| close[i] - ((hh[i] + ll[i]) / 2.0 + sma[i]) / 2.0)
562        .collect();
563    // `delta` warms up with NaN; linearreg's running sum would propagate it, so run the
564    // regression over the finite tail and place it back at the matching offset.
565    let start = delta.iter().position(|x| !x.is_nan()).unwrap_or(len);
566    let mut out = vec![f64::NAN; len];
567    if start < len {
568        let sub = super::linearreg(&delta[start..], n);
569        out[start..].copy_from_slice(&sub);
570    }
571    out
572}
573
574/// TTM Squeeze on/off: `1.0` when the Bollinger Bands (n, `bb_mult`·σ) sit inside the Keltner
575/// Channels (n, `kc_mult`·SMAₙ(TR)), else `0.0`; NaN until both warm up. Source: John Carter /
576/// StockCharts — TTM Squeeze (Carter's original SMA-of-range Keltner).
577pub fn ttm_squeeze_on(
578    high: &[f64],
579    low: &[f64],
580    close: &[f64],
581    n: usize,
582    bb_mult: f64,
583    kc_mult: f64,
584) -> Vec<f64> {
585    let len = high.len();
586    let sma = super::ma(close, n);
587    let sd = super::stddev(close, n, 1.0);
588    let tr = super::tr(high, low, close);
589    let atr = kernels::sma(av(&tr), n).to_vec(); // SMA of TR (skips the leading-NaN tr[0])
590    (0..len)
591        .map(|i| {
592            let bb_u = sma[i] + bb_mult * sd[i];
593            let bb_l = sma[i] - bb_mult * sd[i];
594            let kc_u = sma[i] + kc_mult * atr[i];
595            let kc_l = sma[i] - kc_mult * atr[i];
596            if bb_u.is_nan() || kc_u.is_nan() {
597                f64::NAN
598            } else if bb_l > kc_l && bb_u < kc_u {
599                1.0
600            } else {
601                0.0
602            }
603        })
604        .collect()
605}
606
607/// One Supertrend recurrence step: tighten the bands against the prior bar, then flip the
608/// trend (`+1` up / `−1` down) on a close crossing the relevant band. Returns the new
609/// `(trend, final_upper, final_lower)`. Source: TradingView — Supertrend.
610// One parameter per band/state input the step folds; a struct would only repackage them.
611#[allow(clippy::too_many_arguments)]
612fn supertrend_step(
613    hl2: f64,
614    atr: f64,
615    close: f64,
616    mult: f64,
617    prev_trend: f64,
618    prev_fu: f64,
619    prev_fl: f64,
620    prev_close: f64,
621) -> (f64, f64, f64) {
622    let (bu, bl) = (hl2 + mult * atr, hl2 - mult * atr);
623    let fu = if bu < prev_fu || prev_close > prev_fu { bu } else { prev_fu };
624    let fl = if bl > prev_fl || prev_close < prev_fl { bl } else { prev_fl };
625    let trend = if prev_trend < 0.0 {
626        if close > fu { 1.0 } else { -1.0 }
627    } else if close < fl {
628        -1.0
629    } else {
630        1.0
631    };
632    (trend, fu, fl)
633}
634
635/// The Supertrend output at a bar given its `(trend, final_upper, final_lower)`: the line
636/// (lower band in an up-trend, upper band in a down-trend) or the trend direction.
637fn supertrend_out(trend: f64, fu: f64, fl: f64, want_direction: bool) -> f64 {
638    if want_direction {
639        trend
640    } else if trend > 0.0 {
641        fl
642    } else {
643        fu
644    }
645}
646
647/// Supertrend (TradingView): `hl2 ± mult·ATR` bands, recursively tightened and flipped into a
648/// single trailing line. `want_direction` returns the `+1`/`−1` trend instead of the line.
649/// Source: TradingView — Supertrend.
650pub fn supertrend(
651    high: &[f64],
652    low: &[f64],
653    close: &[f64],
654    period: usize,
655    mult: f64,
656    want_direction: bool,
657) -> Vec<f64> {
658    let len = close.len();
659    let mut out = vec![f64::NAN; len];
660    let atr = super::atr(high, low, close, period);
661    if period >= len {
662        return out; // ATR never seeds
663    }
664    let hl2 = |i: usize| (high[i] + low[i]) / 2.0;
665    // Seed at the first ATR-valid bar: trend starts down (the upper band), per TradingView.
666    let (mut trend, mut fu, mut fl, mut pc) = (
667        -1.0_f64,
668        hl2(period) + mult * atr[period],
669        hl2(period) - mult * atr[period],
670        close[period],
671    );
672    out[period] = supertrend_out(trend, fu, fl, want_direction);
673    for i in (period + 1)..len {
674        let step = supertrend_step(hl2(i), atr[i], close[i], mult, trend, fu, fl, pc);
675        (trend, fu, fl) = step;
676        out[i] = supertrend_out(trend, fu, fl, want_direction);
677        pc = close[i];
678    }
679    out
680}
681
682/// `[trend, final_upper, final_lower, prev_close, atr]` after a full [`supertrend`], or `None`
683/// before the ATR seeds. The shared state for both the line and direction sub-commands.
684pub fn supertrend_final_state(
685    high: &[f64],
686    low: &[f64],
687    close: &[f64],
688    period: usize,
689    mult: f64,
690) -> Option<Vec<f64>> {
691    let len = close.len();
692    // `atr_final_state` already declines (returns None) when `period >= len`, so reaching here
693    // guarantees `period < len` — the seed index is in range.
694    let atr_end = super::atr_final_state(high, low, close, period)?;
695    let atr = super::atr(high, low, close, period);
696    let hl2 = |i: usize| (high[i] + low[i]) / 2.0;
697    let (mut trend, mut fu, mut fl, mut pc) = (
698        -1.0_f64,
699        hl2(period) + mult * atr[period],
700        hl2(period) - mult * atr[period],
701        close[period],
702    );
703    for i in (period + 1)..len {
704        let step = supertrend_step(hl2(i), atr[i], close[i], mult, trend, fu, fl, pc);
705        (trend, fu, fl) = step;
706        pc = close[i];
707    }
708    Some(vec![trend, fu, fl, close[len - 1], atr_end[0]])
709}
710
711/// Resume [`supertrend`] over `[from, n)` by advancing the carried trend / bands with the ATR
712/// resume — bit-identical to a full recompute. `None` at `from == 0` (the ATR resume declines).
713// One parameter per series/state the resume reads; a struct would only repackage them.
714#[allow(clippy::too_many_arguments)]
715pub fn supertrend_resume(
716    high: &[f64],
717    low: &[f64],
718    close: &[f64],
719    period: usize,
720    mult: f64,
721    want_direction: bool,
722    from: usize,
723    state: &[f64],
724) -> Option<(Vec<f64>, Vec<f64>)> {
725    let (mut trend, mut fu, mut fl, mut pc) = (state[0], state[1], state[2], state[3]);
726    let (atr_tail, atr_new) = super::atr_resume(high, low, close, period, from, &state[4..5])?;
727    let hl2 = |i: usize| (high[i] + low[i]) / 2.0;
728    let mut out = Vec::with_capacity(close.len().saturating_sub(from));
729    for (j, i) in (from..close.len()).enumerate() {
730        let step = supertrend_step(hl2(i), atr_tail[j], close[i], mult, trend, fu, fl, pc);
731        (trend, fu, fl) = step;
732        out.push(supertrend_out(trend, fu, fl, want_direction));
733        pc = close[i];
734    }
735    Some((out, vec![trend, fu, fl, pc, atr_new[0]]))
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use crate::indicators::test_support::*;
742
743    /// The cumulative `*_final_state` guards decline on an empty series (no first bar to seed).
744    #[test]
745    fn cumulative_final_state_declines_on_empty() {
746        assert!(wad_final_state(&[], &[], &[]).is_none());
747        assert!(asi_final_state(&[], &[], &[], &[], 3.0).is_none());
748    }
749
750    /// wad / asi resume, fed the carried state of a full compute over the head `[0, from)`,
751    /// reproduces the tail of a full compute over the whole input — bit-for-bit.
752    #[test]
753    fn cumulative_resume_is_bit_identical_to_full() {
754        let (high, low, close) = ohlc(160);
755        let open: Vec<f64> = close.iter().map(|c| c - 0.3).collect();
756        let wad_full = wad(&high, &low, &close);
757        let asi_full = asi(&open, &high, &low, &close, 3.0);
758        for &from in &[1usize, 2, 40, 80, 159] {
759            let st = wad_final_state(&high[..from], &low[..from], &close[..from]).unwrap();
760            assert_bits(&wad_resume(&high, &low, &close, from, &st).0, &wad_full[from..], "wad");
761
762            let st =
763                asi_final_state(&open[..from], &high[..from], &low[..from], &close[..from], 3.0)
764                    .unwrap();
765            assert_bits(
766                &asi_resume(&open, &high, &low, &close, 3.0, from, &st).0,
767                &asi_full[from..],
768                "asi",
769            );
770        }
771    }
772
773    /// Supertrend blanks before its ATR seeds, and its resume reproduces a full recompute
774    /// bit-for-bit — including the recursive band-tightening and trend flips.
775    #[test]
776    fn supertrend_resume_and_guards() {
777        let (high, low, close) = ohlc(160);
778        // Too-short frame: the ATR never seeds → an all-NaN line and no carried state.
779        assert!(supertrend(&high[..5], &low[..5], &close[..5], 10, 3.0, false)
780            .iter()
781            .all(|x| x.is_nan()));
782        assert!(supertrend_final_state(&high[..5], &low[..5], &close[..5], 10, 3.0).is_none());
783
784        let line_full = supertrend(&high, &low, &close, 10, 3.0, false);
785        let dir_full = supertrend(&high, &low, &close, 10, 3.0, true);
786        for &from in &[11usize, 20, 80, 159] {
787            let st = supertrend_final_state(&high[..from], &low[..from], &close[..from], 10, 3.0)
788                .unwrap();
789            let (line, _) =
790                supertrend_resume(&high, &low, &close, 10, 3.0, false, from, &st).unwrap();
791            assert_bits(&line, &line_full[from..], "supertrend");
792            let (dir, _) =
793                supertrend_resume(&high, &low, &close, 10, 3.0, true, from, &st).unwrap();
794            assert_bits(&dir, &dir_full[from..], "supertrend.direction");
795        }
796    }
797}