Skip to main content

ta_indicators/
lib.rs

1//! Warmup-exact batch port of TA-Lib technical indicators for Rust.
2//!
3//! See crate README for module layout (`lib.rs` vs `candles.rs`) and parity policy.
4//!
5//! # Example
6//!
7//! ```
8//! use ta_indicators::{bollinger_bands, macd, rsi};
9//!
10//! let closes = [100.0, 101.0, 102.0, 101.5, 103.0, 104.0, 103.5, 105.0];
11//! let rsi_3 = rsi(&closes, 3);
12//! let macd_out = macd(&closes, 3, 6, 3);
13//! let bands = bollinger_bands(&closes, 3, 2.0);
14//!
15//! assert_eq!(rsi_3.len(), closes.len());
16//! assert_eq!(macd_out.macd.len(), closes.len());
17//! assert_eq!(bands.middle.len(), closes.len());
18//! ```
19//!
20//! Outputs use `Option<f64>` for numeric indicators. Warmup bars are `None`;
21//! emitted bars are matched against committed TA-Lib reference fixtures.
22
23mod candles;
24pub use candles::*;
25use std::collections::VecDeque;
26
27#[derive(Debug, Clone)]
28pub struct Macd {
29    pub macd: Vec<Option<f64>>,
30    pub signal: Vec<Option<f64>>,
31    pub hist: Vec<Option<f64>>,
32}
33
34#[derive(Debug, Clone)]
35pub struct AdxFamily {
36    pub adx: Vec<Option<f64>>,
37    pub adxr: Vec<Option<f64>>,
38    pub plus_di: Vec<Option<f64>>,
39    pub minus_di: Vec<Option<f64>>,
40    pub dx: Vec<Option<f64>>,
41}
42
43#[derive(Debug, Clone)]
44pub struct Aroon {
45    pub up: Vec<Option<f64>>,
46    pub down: Vec<Option<f64>>,
47    pub oscillator: Vec<Option<f64>>,
48}
49
50#[derive(Debug, Clone)]
51pub struct PriceTransforms {
52    pub avgprice: Vec<Option<f64>>,
53    pub medprice: Vec<Option<f64>>,
54    pub typprice: Vec<Option<f64>>,
55    pub wclprice: Vec<Option<f64>>,
56}
57
58#[derive(Debug, Clone)]
59pub struct BollingerBands {
60    pub upper: Vec<Option<f64>>,
61    pub middle: Vec<Option<f64>>,
62    pub lower: Vec<Option<f64>>,
63}
64
65#[derive(Debug, Clone)]
66pub struct DirectionalMovement {
67    pub plus_dm: Vec<Option<f64>>,
68    pub minus_dm: Vec<Option<f64>>,
69}
70
71#[derive(Debug, Clone)]
72pub struct StochRsi {
73    pub k: Vec<Option<f64>>,
74    pub d: Vec<Option<f64>>,
75}
76
77#[derive(Debug, Clone)]
78pub struct LinearRegression {
79    pub line: Vec<Option<f64>>,
80    pub slope: Vec<Option<f64>>,
81    pub angle: Vec<Option<f64>>,
82    pub intercept: Vec<Option<f64>>,
83    pub tsf: Vec<Option<f64>>,
84}
85
86#[derive(Debug, Clone)]
87pub struct PriceContext {
88    pub close_vs_ath_pct: Vec<Option<f64>>,
89    pub close_vs_atl_pct: Vec<Option<f64>>,
90    pub days_since_ath: Vec<Option<f64>>,
91    pub days_since_atl: Vec<Option<f64>>,
92}
93
94#[inline]
95fn finite(value: f64) -> Option<f64> {
96    value.is_finite().then_some(value)
97}
98
99fn option_values(values: &[Option<f64>]) -> Vec<f64> {
100    values
101        .iter()
102        .map(|value| value.unwrap_or(f64::NAN))
103        .collect()
104}
105
106pub fn ema(values: &[f64], period: usize) -> Vec<Option<f64>> {
107    let mut out = vec![None; values.len()];
108    if period == 0 {
109        return out;
110    }
111    let alpha = 2.0 / (period as f64 + 1.0);
112    let mut current = None;
113    let mut warm_sum = 0.0;
114    let mut warm_count = 0usize;
115    for (idx, value) in values.iter().copied().enumerate() {
116        let Some(value) = finite(value) else {
117            continue;
118        };
119        if let Some(prev) = current {
120            let next = alpha * value + (1.0 - alpha) * prev;
121            current = Some(next);
122            out[idx] = Some(next);
123        } else {
124            warm_sum += value;
125            warm_count += 1;
126            if warm_count == period {
127                let next = warm_sum / period as f64;
128                current = Some(next);
129                out[idx] = Some(next);
130            }
131        }
132    }
133    out
134}
135
136/// SMA-seeded EMA whose seed is the average of the `period` raw values ending
137/// at `seed_end`, then the standard EMA recurrence forward from there. This
138/// matches TA-Lib's `TA_INT_EMA` when its output start is forced to `seed_end`.
139fn ema_seeded_at(values: &[f64], period: usize, seed_end: usize) -> Vec<Option<f64>> {
140    let mut out = vec![None; values.len()];
141    if period == 0 || seed_end >= values.len() || seed_end + 1 < period {
142        return out;
143    }
144    let alpha = 2.0 / (period as f64 + 1.0);
145    let seed = values[seed_end + 1 - period..=seed_end].iter().sum::<f64>() / period as f64;
146    out[seed_end] = Some(seed);
147    let mut prev = seed;
148    for (idx, value) in values.iter().enumerate().skip(seed_end + 1) {
149        prev = alpha * value + (1.0 - alpha) * prev;
150        out[idx] = Some(prev);
151    }
152    out
153}
154
155/// EMA over an `Option` series that is contiguously valid from `valid_start`,
156/// SMA-seeded over its first `period` values. Used for the MACD signal line.
157fn ema_seeded_options(
158    series: &[Option<f64>],
159    period: usize,
160    valid_start: usize,
161) -> Vec<Option<f64>> {
162    let mut out = vec![None; series.len()];
163    let seed_end = valid_start + period.saturating_sub(1);
164    if period == 0 || seed_end >= series.len() {
165        return out;
166    }
167    let mut sum = 0.0;
168    for value in series.iter().take(seed_end + 1).skip(valid_start) {
169        match value {
170            Some(value) => sum += value,
171            None => return out,
172        }
173    }
174    let alpha = 2.0 / (period as f64 + 1.0);
175    let seed = sum / period as f64;
176    out[seed_end] = Some(seed);
177    let mut prev = seed;
178    for (idx, value) in series.iter().enumerate().skip(seed_end + 1) {
179        if let Some(value) = value {
180            prev = alpha * value + (1.0 - alpha) * prev;
181            out[idx] = Some(prev);
182        }
183    }
184    out
185}
186
187pub fn macd(values: &[f64], fast: usize, slow: usize, signal: usize) -> Macd {
188    let len = values.len();
189    let mut macd_line = vec![None; len];
190    if fast == 0 || slow == 0 || signal == 0 {
191        return Macd {
192            macd: macd_line.clone(),
193            signal: vec![None; len],
194            hist: vec![None; len],
195        };
196    }
197    // TA-Lib aligns both EMAs' SMA seed to the slow EMA's start (`slow - 1`),
198    // so the fast EMA is seeded over the same window's tail rather than its own
199    // natural warmup. The MACD line is then `fastEMA - slowEMA` from there.
200    let seed_end = fast.max(slow) - 1;
201    let fast_ema = ema_seeded_at(values, fast, seed_end);
202    let slow_ema = ema_seeded_at(values, slow, seed_end);
203    for idx in seed_end..len {
204        if let (Some(f), Some(s)) = (fast_ema[idx], slow_ema[idx]) {
205            macd_line[idx] = Some(f - s);
206        }
207    }
208    let signal_line = ema_seeded_options(&macd_line, signal, seed_end);
209    let hist = macd_line
210        .iter()
211        .zip(signal_line.iter())
212        .map(|(macd, signal)| Some((*macd)? - (*signal)?))
213        .collect();
214    Macd {
215        macd: macd_line,
216        signal: signal_line,
217        hist,
218    }
219}
220
221pub fn macdfix(values: &[f64], signal: usize) -> Macd {
222    macd(values, 12, 26, signal)
223}
224
225/// Simple moving average over a raw series. First value at index `period - 1`.
226fn sma(values: &[f64], period: usize) -> Vec<Option<f64>> {
227    rolling_sum(values, period)
228        .into_iter()
229        .map(|sum| sum.map(|sum| sum / period as f64))
230        .collect()
231}
232
233/// Simple moving average over an `Option` series; a window is only emitted when
234/// all `period` members are present (matches TA-Lib MA-of-MA warmup behavior).
235fn sma_options(series: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
236    let mut out = vec![None; series.len()];
237    if period == 0 {
238        return out;
239    }
240    for idx in period - 1..series.len() {
241        let window = &series[idx + 1 - period..=idx];
242        let Some(sum) = window
243            .iter()
244            .try_fold(0.0, |sum, value| value.map(|value| sum + value))
245        else {
246            continue;
247        };
248        out[idx] = Some(sum / period as f64);
249    }
250    out
251}
252
253/// MACDEXT with all three moving averages set to SMA (TA-Lib `matype = 0`).
254/// fast SMA − slow SMA forms the MACD line; an SMA over the MACD line forms the
255/// signal. Matches `talib.MACDEXT(.., fastmatype=0, slowmatype=0, signalmatype=0)`.
256pub fn macdext_sma(values: &[f64], fast: usize, slow: usize, signal: usize) -> Macd {
257    let len = values.len();
258    if fast == 0 || slow == 0 || signal == 0 {
259        return Macd {
260            macd: vec![None; len],
261            signal: vec![None; len],
262            hist: vec![None; len],
263        };
264    }
265    let fast_ma = sma(values, fast);
266    let slow_ma = sma(values, slow);
267    let macd_line: Vec<Option<f64>> = fast_ma
268        .iter()
269        .zip(slow_ma.iter())
270        .map(|(fast, slow)| Some((*fast)? - (*slow)?))
271        .collect();
272    let signal_line = sma_options(&macd_line, signal);
273    let hist = macd_line
274        .iter()
275        .zip(signal_line.iter())
276        .map(|(macd, signal)| Some((*macd)? - (*signal)?))
277        .collect();
278    Macd {
279        macd: macd_line,
280        signal: signal_line,
281        hist,
282    }
283}
284
285/// Variable-period simple moving average (TA-Lib `MAVP`, `matype = 0`). Each
286/// bar uses `periods[idx]` truncated to an integer and clamped to
287/// `[min_period, max_period]`; the global warmup is `max_period - 1` so every
288/// emitted bar has a full window.
289pub fn mavp(
290    values: &[f64],
291    periods: &[f64],
292    min_period: usize,
293    max_period: usize,
294) -> Vec<Option<f64>> {
295    let len = values.len().min(periods.len());
296    let mut out = vec![None; values.len()];
297    if max_period == 0 || max_period < min_period {
298        return out;
299    }
300    if values[..len].iter().all(|value| value.is_finite()) {
301        let mut sums = vec![0.0; len + 1];
302        for (idx, value) in values.iter().take(len).enumerate() {
303            sums[idx + 1] = sums[idx] + value;
304        }
305        for idx in (max_period - 1)..len {
306            let raw = periods[idx];
307            if !raw.is_finite() || raw < 0.0 {
308                continue;
309            }
310            let period = (raw as usize).clamp(min_period, max_period);
311            if period == 0 {
312                continue;
313            }
314            let start = idx + 1 - period;
315            out[idx] = Some((sums[idx + 1] - sums[start]) / period as f64);
316        }
317        return out;
318    }
319    for idx in (max_period - 1)..len {
320        let raw = periods[idx];
321        if !raw.is_finite() || raw < 0.0 {
322            continue;
323        }
324        let period = (raw as usize).clamp(min_period, max_period);
325        if period == 0 {
326            continue;
327        }
328        let window = &values[idx + 1 - period..=idx];
329        if window.iter().all(|value| value.is_finite()) {
330            let sum: f64 = window.iter().sum();
331            out[idx] = Some(sum / period as f64);
332        }
333    }
334    out
335}
336
337/// Rolling BETA of `real0` against `real1` (TA-Lib `BETA`). Uses `period`
338/// trailing simple returns; `beta = (n·Σxy − Σx·Σy) / (n·Σxx − (Σx)²)` where
339/// `x` are `real0` returns and `y` are `real1` returns. First value at index
340/// `period`.
341pub fn beta(real0: &[f64], real1: &[f64], period: usize) -> Vec<Option<f64>> {
342    let len = real0.len().min(real1.len());
343    let mut out = vec![None; real0.len()];
344    if period == 0 || len <= period {
345        return out;
346    }
347    let n = period as f64;
348    if real0[..len].iter().all(|value| value.is_finite())
349        && real1[..len].iter().all(|value| value.is_finite())
350        && real0[..len - 1].iter().all(|value| *value != 0.0)
351        && real1[..len - 1].iter().all(|value| *value != 0.0)
352    {
353        // Sliding return sums adapted from talib-rs 0.1.2 (BSD-3-Clause);
354        // see THIRD_PARTY_NOTICES.md.
355        let mut sx = 0.0;
356        let mut sy = 0.0;
357        let mut sxx = 0.0;
358        let mut sxy = 0.0;
359        let ret = |values: &[f64], idx: usize| (values[idx] - values[idx - 1]) / values[idx - 1];
360        for idx in 1..=period {
361            let x = ret(real0, idx);
362            let y = ret(real1, idx);
363            sx += x;
364            sy += y;
365            sxx += x * x;
366            sxy += x * y;
367        }
368        let emit = |slot: &mut Option<f64>, sx: f64, sy: f64, sxx: f64, sxy: f64| {
369            let denom = n * sxx - sx * sx;
370            if denom != 0.0 {
371                *slot = finite((n * sxy - sx * sy) / denom);
372            }
373        };
374        emit(&mut out[period], sx, sy, sxx, sxy);
375        for (idx, slot) in out.iter_mut().enumerate().take(len).skip(period + 1) {
376            let old_x = ret(real0, idx - period);
377            let old_y = ret(real1, idx - period);
378            let new_x = ret(real0, idx);
379            let new_y = ret(real1, idx);
380            sx += new_x - old_x;
381            sy += new_y - old_y;
382            sxx += new_x * new_x - old_x * old_x;
383            sxy += new_x * new_y - old_x * old_y;
384            emit(slot, sx, sy, sxx, sxy);
385        }
386        return out;
387    }
388    for (t, slot) in out.iter_mut().enumerate().take(len).skip(period) {
389        let (mut sx, mut sy, mut sxx, mut sxy) = (0.0, 0.0, 0.0, 0.0);
390        let mut ok = true;
391        for j in (t + 1 - period)..=t {
392            let (px, py) = (real0[j - 1], real1[j - 1]);
393            if !px.is_finite()
394                || !py.is_finite()
395                || !real0[j].is_finite()
396                || !real1[j].is_finite()
397                || px == 0.0
398                || py == 0.0
399            {
400                ok = false;
401                break;
402            }
403            let x = (real0[j] - px) / px;
404            let y = (real1[j] - py) / py;
405            sx += x;
406            sy += y;
407            sxx += x * x;
408            sxy += x * y;
409        }
410        if !ok {
411            continue;
412        }
413        let denom = n * sxx - sx * sx;
414        if denom == 0.0 {
415            continue;
416        }
417        *slot = finite((n * sxy - sx * sy) / denom);
418    }
419    out
420}
421
422/// Rolling Pearson correlation of `real0` and `real1` (TA-Lib `CORREL`) over a
423/// trailing window of raw values. First value at index `period - 1`; when the
424/// variance product is non-positive TA-Lib emits `0.0`.
425pub fn correl(real0: &[f64], real1: &[f64], period: usize) -> Vec<Option<f64>> {
426    let len = real0.len().min(real1.len());
427    let mut out = vec![None; real0.len()];
428    if period == 0 {
429        return out;
430    }
431    if period > len {
432        return out;
433    }
434    let n = period as f64;
435    if real0[..len].iter().all(|value| value.is_finite())
436        && real1[..len].iter().all(|value| value.is_finite())
437    {
438        // Sliding-window update adapted from talib-rs 0.1.2 (BSD-3-Clause);
439        // see THIRD_PARTY_NOTICES.md.
440        let mut sx = 0.0;
441        let mut sy = 0.0;
442        let mut sxx = 0.0;
443        let mut syy = 0.0;
444        let mut sxy = 0.0;
445        for idx in 0..period {
446            let x = real0[idx];
447            let y = real1[idx];
448            sx += x;
449            sy += y;
450            sxx += x * x;
451            syy += y * y;
452            sxy += x * y;
453        }
454        let emit = |slot: &mut Option<f64>, sx: f64, sy: f64, sxx: f64, syy: f64, sxy: f64| {
455            let numerator = n * sxy - sx * sy;
456            let denominator = ((n * sxx - sx * sx) * (n * syy - sy * sy)).sqrt();
457            *slot = if denominator > 0.0 {
458                finite(numerator / denominator)
459            } else {
460                Some(0.0)
461            };
462        };
463        emit(&mut out[period - 1], sx, sy, sxx, syy, sxy);
464        for idx in period..len {
465            let old_x = real0[idx - period];
466            let old_y = real1[idx - period];
467            let new_x = real0[idx];
468            let new_y = real1[idx];
469            sx += new_x - old_x;
470            sy += new_y - old_y;
471            sxx += new_x * new_x - old_x * old_x;
472            syy += new_y * new_y - old_y * old_y;
473            sxy += new_x * new_y - old_x * old_y;
474            emit(&mut out[idx], sx, sy, sxx, syy, sxy);
475        }
476        return out;
477    }
478    for (t, slot) in out.iter_mut().enumerate().take(len).skip(period - 1) {
479        let (mut sx, mut sy, mut sxx, mut syy, mut sxy) = (0.0, 0.0, 0.0, 0.0, 0.0);
480        let mut ok = true;
481        for j in (t + 1 - period)..=t {
482            let (x, y) = (real0[j], real1[j]);
483            if !x.is_finite() || !y.is_finite() {
484                ok = false;
485                break;
486            }
487            sx += x;
488            sy += y;
489            sxx += x * x;
490            syy += y * y;
491            sxy += x * y;
492        }
493        if !ok {
494            continue;
495        }
496        let var_product = (sxx - sx * sx / n) * (syy - sy * sy / n);
497        *slot = if var_product > 0.0 {
498            finite((sxy - sx * sy / n) / var_product.sqrt())
499        } else {
500            Some(0.0)
501        };
502    }
503    out
504}
505
506pub fn apo(values: &[f64], fast: usize, slow: usize) -> Vec<Option<f64>> {
507    let fast_ema = ema(values, fast);
508    let slow_ema = ema(values, slow);
509    fast_ema
510        .iter()
511        .zip(slow_ema.iter())
512        .map(|(fast, slow)| Some((*fast)? - (*slow)?))
513        .collect()
514}
515
516pub fn ppo(values: &[f64], fast: usize, slow: usize) -> Vec<Option<f64>> {
517    let fast_ema = ema(values, fast);
518    let slow_ema = ema(values, slow);
519    fast_ema
520        .iter()
521        .zip(slow_ema.iter())
522        .map(|(fast, slow)| {
523            let slow = slow.filter(|slow| slow.abs() > f64::EPSILON)?;
524            Some(((*fast)? / slow - 1.0) * 100.0)
525        })
526        .collect()
527}
528
529pub fn rsi(values: &[f64], period: usize) -> Vec<Option<f64>> {
530    let mut out = vec![None; values.len()];
531    if period == 0 || values.len() <= period {
532        return out;
533    }
534    let mut gains = 0.0;
535    let mut losses = 0.0;
536    for idx in 1..values.len() {
537        let change = values[idx] - values[idx - 1];
538        if !change.is_finite() {
539            continue;
540        }
541        let gain = change.max(0.0);
542        let loss = (-change).max(0.0);
543        if idx <= period {
544            gains += gain;
545            losses += loss;
546            if idx == period {
547                gains /= period as f64;
548                losses /= period as f64;
549                out[idx] = Some(rsi_value(gains, losses));
550            }
551        } else {
552            gains = (gains * (period as f64 - 1.0) + gain) / period as f64;
553            losses = (losses * (period as f64 - 1.0) + loss) / period as f64;
554            out[idx] = Some(rsi_value(gains, losses));
555        }
556    }
557    out
558}
559
560fn rsi_value(avg_gain: f64, avg_loss: f64) -> f64 {
561    if avg_loss <= f64::EPSILON {
562        100.0
563    } else {
564        100.0 - 100.0 / (1.0 + avg_gain / avg_loss)
565    }
566}
567
568pub fn stochrsi(values: &[f64], rsi_period: usize, k_period: usize, d_period: usize) -> StochRsi {
569    let rsi = rsi(values, rsi_period);
570    let mut k = vec![None; values.len()];
571    if k_period > 0 {
572        let mut valid_count = 0usize;
573        let mut lows: VecDeque<(usize, f64)> = VecDeque::new();
574        let mut highs: VecDeque<(usize, f64)> = VecDeque::new();
575        for idx in 0..rsi.len() {
576            if let Some(value) = rsi[idx] {
577                valid_count += 1;
578                while lows.back().is_some_and(|(_, prior)| *prior >= value) {
579                    lows.pop_back();
580                }
581                lows.push_back((idx, value));
582                while highs.back().is_some_and(|(_, prior)| *prior <= value) {
583                    highs.pop_back();
584                }
585                highs.push_back((idx, value));
586            }
587            if idx >= k_period {
588                let expired = idx - k_period;
589                if rsi[expired].is_some() {
590                    valid_count -= 1;
591                }
592                while lows.front().is_some_and(|(low_idx, _)| *low_idx <= expired) {
593                    lows.pop_front();
594                }
595                while highs
596                    .front()
597                    .is_some_and(|(high_idx, _)| *high_idx <= expired)
598                {
599                    highs.pop_front();
600                }
601            }
602            if idx + 1 >= k_period
603                && valid_count == k_period
604                && let (Some((_, low)), Some((_, high)), Some(current)) =
605                    (lows.front(), highs.front(), rsi[idx])
606            {
607                let range = high - low;
608                if range.abs() > f64::EPSILON {
609                    k[idx] = Some((current - low) / range * 100.0);
610                }
611            }
612        }
613    }
614    let d = option_mean(&k, d_period);
615    StochRsi { k, d }
616}
617
618fn option_mean(values: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
619    let mut out = vec![None; values.len()];
620    if period == 0 {
621        return out;
622    }
623    let mut sum = 0.0;
624    let mut valid_count = 0usize;
625    for idx in 0..values.len() {
626        if let Some(value) = values[idx] {
627            sum += value;
628            valid_count += 1;
629        }
630        if idx >= period
631            && let Some(value) = values[idx - period]
632        {
633            sum -= value;
634            valid_count -= 1;
635        }
636        if idx + 1 >= period && valid_count == period {
637            out[idx] = Some(sum / period as f64);
638        }
639    }
640    out
641}
642
643pub fn kama(values: &[f64], period: usize) -> Vec<Option<f64>> {
644    let mut out = vec![None; values.len()];
645    if period == 0 || values.len() <= period {
646        return out;
647    }
648    let fast_sc = 2.0 / (2.0 + 1.0);
649    let slow_sc = 2.0 / (30.0 + 1.0);
650    // TA-Lib seeds the running KAMA with the prior close (`values[period - 1]`)
651    // and applies a full smoothing step on the first emitted bar.
652    if values.iter().all(|value| value.is_finite()) {
653        // Rolling volatility update adapted from talib-rs 0.1.2
654        // (BSD-3-Clause); see THIRD_PARTY_NOTICES.md.
655        let mut current = values[period - 1];
656        let mut volatility = 0.0;
657        for idx in 1..=period {
658            volatility += (values[idx] - values[idx - 1]).abs();
659        }
660        for idx in period..values.len() {
661            if idx > period {
662                volatility += (values[idx] - values[idx - 1]).abs()
663                    - (values[idx - period] - values[idx - period - 1]).abs();
664            }
665            let change = (values[idx] - values[idx - period]).abs();
666            let er = if volatility > f64::EPSILON {
667                change / volatility
668            } else {
669                0.0
670            };
671            let smoothing_base = er * (fast_sc - slow_sc) + slow_sc;
672            let smoothing = smoothing_base * smoothing_base;
673            current += smoothing * (values[idx] - current);
674            out[idx] = Some(current);
675        }
676        return out;
677    }
678    let mut current = finite(values[period - 1]);
679    for idx in period..values.len() {
680        let Some(price) = finite(values[idx]) else {
681            continue;
682        };
683        let Some(prior) = finite(values[idx - period]) else {
684            continue;
685        };
686        let change = (price - prior).abs();
687        let mut volatility = 0.0;
688        let mut valid = true;
689        for offset in idx + 1 - period..=idx {
690            let diff = values[offset] - values[offset - 1];
691            if !diff.is_finite() {
692                valid = false;
693                break;
694            }
695            volatility += diff.abs();
696        }
697        if !valid {
698            continue;
699        }
700        let er = if volatility > f64::EPSILON {
701            change / volatility
702        } else {
703            0.0
704        };
705        let smoothing_base = er * (fast_sc - slow_sc) + slow_sc;
706        let smoothing = smoothing_base * smoothing_base;
707        let next = if let Some(prev) = current {
708            prev + smoothing * (price - prev)
709        } else {
710            price
711        };
712        current = Some(next);
713        out[idx] = Some(next);
714    }
715    out
716}
717
718pub fn bollinger_bands(values: &[f64], period: usize, deviations: f64) -> BollingerBands {
719    let mut upper = vec![None; values.len()];
720    let mut middle = vec![None; values.len()];
721    let mut lower = vec![None; values.len()];
722    if period == 0 {
723        return BollingerBands {
724            upper,
725            middle,
726            lower,
727        };
728    }
729    if period <= values.len() && values.iter().all(|value| value.is_finite()) {
730        // Sliding-window update adapted from talib-rs 0.1.2 (BSD-3-Clause);
731        // see THIRD_PARTY_NOTICES.md.
732        let inv_period = 1.0 / period as f64;
733        let mut sum = 0.0;
734        let mut sum_sq = 0.0;
735        for value in values.iter().take(period) {
736            sum += *value;
737            sum_sq += value * value;
738        }
739        let emit = |idx: usize,
740                    sum: f64,
741                    sum_sq: f64,
742                    middle: &mut [Option<f64>],
743                    upper: &mut [Option<f64>],
744                    lower: &mut [Option<f64>]| {
745            let mean = sum * inv_period;
746            let variance = (sum_sq * inv_period - mean * mean).max(0.0);
747            let std = variance.sqrt();
748            middle[idx] = Some(mean);
749            upper[idx] = Some(mean + deviations * std);
750            lower[idx] = Some(mean - deviations * std);
751        };
752        emit(period - 1, sum, sum_sq, &mut middle, &mut upper, &mut lower);
753        for idx in period..values.len() {
754            let old = values[idx - period];
755            let new = values[idx];
756            sum += new - old;
757            sum_sq += new * new - old * old;
758            emit(idx, sum, sum_sq, &mut middle, &mut upper, &mut lower);
759        }
760        return BollingerBands {
761            upper,
762            middle,
763            lower,
764        };
765    }
766    for idx in period - 1..values.len() {
767        let start = idx + 1 - period;
768        let window = &values[start..=idx];
769        if window.iter().any(|value| !value.is_finite()) {
770            continue;
771        }
772        let mean = window.iter().sum::<f64>() / period as f64;
773        let variance = window
774            .iter()
775            .map(|value| {
776                let delta = value - mean;
777                delta * delta
778            })
779            .sum::<f64>()
780            / period as f64;
781        let std = variance.sqrt();
782        middle[idx] = Some(mean);
783        upper[idx] = Some(mean + deviations * std);
784        lower[idx] = Some(mean - deviations * std);
785    }
786    BollingerBands {
787        upper,
788        middle,
789        lower,
790    }
791}
792
793pub fn bop(opens: &[f64], highs: &[f64], lows: &[f64], closes: &[f64]) -> Vec<Option<f64>> {
794    let len = opens
795        .len()
796        .min(highs.len())
797        .min(lows.len())
798        .min(closes.len());
799    let mut out = Vec::with_capacity(len);
800    for idx in 0..len {
801        let range = highs[idx] - lows[idx];
802        out.push(
803            (range.abs() > f64::EPSILON)
804                .then_some((closes[idx] - opens[idx]) / range)
805                .filter(|value| value.is_finite()),
806        );
807    }
808    out
809}
810
811pub fn cmo(values: &[f64], period: usize) -> Vec<Option<f64>> {
812    let mut out = vec![None; values.len()];
813    if period == 0 || values.len() <= period {
814        return out;
815    }
816    // TA-Lib CMO uses Wilder smoothing of the up/down sums (identical to RSI's
817    // running average), not a simple trailing window.
818    let mut gains = 0.0;
819    let mut losses = 0.0;
820    for idx in 1..values.len() {
821        let change = values[idx] - values[idx - 1];
822        if !change.is_finite() {
823            continue;
824        }
825        let gain = change.max(0.0);
826        let loss = (-change).max(0.0);
827        if idx <= period {
828            gains += gain;
829            losses += loss;
830            if idx == period {
831                gains /= period as f64;
832                losses /= period as f64;
833            }
834        } else {
835            gains = (gains * (period as f64 - 1.0) + gain) / period as f64;
836            losses = (losses * (period as f64 - 1.0) + loss) / period as f64;
837        }
838        if idx >= period {
839            let denom = gains + losses;
840            if denom > f64::EPSILON {
841                out[idx] = Some(100.0 * (gains - losses) / denom);
842            }
843        }
844    }
845    out
846}
847
848pub fn ultosc(
849    highs: &[f64],
850    lows: &[f64],
851    closes: &[f64],
852    short: usize,
853    medium: usize,
854    long: usize,
855) -> Vec<Option<f64>> {
856    let len = highs.len().min(lows.len()).min(closes.len());
857    let mut out = vec![None; len];
858    if short == 0 || medium == 0 || long == 0 || len <= long {
859        return out;
860    }
861    if short <= long
862        && medium <= long
863        && highs[..len].iter().all(|value| value.is_finite())
864        && lows[..len].iter().all(|value| value.is_finite())
865        && closes[..len].iter().all(|value| value.is_finite())
866    {
867        // Rolling BP/TR sums adapted from talib-rs 0.1.2 (BSD-3-Clause);
868        // see THIRD_PARTY_NOTICES.md.
869        let mut buying_pressure = vec![0.0; len];
870        let mut true_range = vec![0.0; len];
871        for idx in 1..len {
872            let prev_close = closes[idx - 1];
873            let true_low = lows[idx].min(prev_close);
874            buying_pressure[idx] = closes[idx] - true_low;
875            true_range[idx] = highs[idx].max(prev_close) - true_low;
876        }
877        let mut short_bp: f64 = buying_pressure[(long + 1 - short)..=long].iter().sum();
878        let mut short_tr: f64 = true_range[(long + 1 - short)..=long].iter().sum();
879        let mut medium_bp: f64 = buying_pressure[(long + 1 - medium)..=long].iter().sum();
880        let mut medium_tr: f64 = true_range[(long + 1 - medium)..=long].iter().sum();
881        let mut long_bp: f64 = buying_pressure[1..=long].iter().sum();
882        let mut long_tr: f64 = true_range[1..=long].iter().sum();
883        let emit = |idx: usize,
884                    short_bp: f64,
885                    short_tr: f64,
886                    medium_bp: f64,
887                    medium_tr: f64,
888                    long_bp: f64,
889                    long_tr: f64,
890                    out: &mut [Option<f64>]| {
891            if short_tr.abs() > f64::EPSILON
892                && medium_tr.abs() > f64::EPSILON
893                && long_tr.abs() > f64::EPSILON
894            {
895                let short_avg = short_bp / short_tr;
896                let medium_avg = medium_bp / medium_tr;
897                let long_avg = long_bp / long_tr;
898                out[idx] = Some(100.0 * (4.0 * short_avg + 2.0 * medium_avg + long_avg) / 7.0);
899            }
900        };
901        emit(
902            long, short_bp, short_tr, medium_bp, medium_tr, long_bp, long_tr, &mut out,
903        );
904        for idx in (long + 1)..len {
905            short_bp += buying_pressure[idx] - buying_pressure[idx - short];
906            short_tr += true_range[idx] - true_range[idx - short];
907            medium_bp += buying_pressure[idx] - buying_pressure[idx - medium];
908            medium_tr += true_range[idx] - true_range[idx - medium];
909            long_bp += buying_pressure[idx] - buying_pressure[idx - long];
910            long_tr += true_range[idx] - true_range[idx - long];
911            emit(
912                idx, short_bp, short_tr, medium_bp, medium_tr, long_bp, long_tr, &mut out,
913            );
914        }
915        return out;
916    }
917    let mut buying_pressure = vec![0.0; len];
918    let mut true_range = vec![0.0; len];
919    for idx in 1..len {
920        let prev_close = closes[idx - 1];
921        buying_pressure[idx] = closes[idx] - lows[idx].min(prev_close);
922        true_range[idx] = highs[idx].max(prev_close) - lows[idx].min(prev_close);
923    }
924    for idx in long..len {
925        let avg = |period: usize| -> Option<f64> {
926            let start = idx + 1 - period;
927            let bp = buying_pressure[start..=idx].iter().sum::<f64>();
928            let tr = true_range[start..=idx].iter().sum::<f64>();
929            (tr.abs() > f64::EPSILON).then_some(bp / tr)
930        };
931        if let (Some(short_avg), Some(medium_avg), Some(long_avg)) =
932            (avg(short), avg(medium), avg(long))
933        {
934            out[idx] = Some(100.0 * (4.0 * short_avg + 2.0 * medium_avg + long_avg) / 7.0);
935        }
936    }
937    out
938}
939
940pub fn dema(values: &[f64], period: usize) -> Vec<Option<f64>> {
941    let ema1 = ema(values, period);
942    let ema2 = ema(&option_values(&ema1), period);
943    ema1.iter()
944        .zip(ema2.iter())
945        .map(|(ema1, ema2)| Some(2.0 * (*ema1)? - (*ema2)?))
946        .collect()
947}
948
949pub fn tema(values: &[f64], period: usize) -> Vec<Option<f64>> {
950    let ema1 = ema(values, period);
951    let ema2 = ema(&option_values(&ema1), period);
952    let ema3 = ema(&option_values(&ema2), period);
953    ema1.iter()
954        .zip(ema2.iter().zip(ema3.iter()))
955        .map(|(ema1, (ema2, ema3))| Some(3.0 * (*ema1)? - 3.0 * (*ema2)? + (*ema3)?))
956        .collect()
957}
958
959pub fn t3(values: &[f64], period: usize, vfactor: f64) -> Vec<Option<f64>> {
960    let ema1 = ema(values, period);
961    let ema2 = ema(&option_values(&ema1), period);
962    let ema3 = ema(&option_values(&ema2), period);
963    let ema4 = ema(&option_values(&ema3), period);
964    let ema5 = ema(&option_values(&ema4), period);
965    let ema6 = ema(&option_values(&ema5), period);
966    let v2 = vfactor * vfactor;
967    let v3 = v2 * vfactor;
968    let c1 = -v3;
969    let c2 = 3.0 * v2 + 3.0 * v3;
970    let c3 = -6.0 * v2 - 3.0 * vfactor - 3.0 * v3;
971    let c4 = 1.0 + 3.0 * vfactor + 3.0 * v2 + v3;
972    ema3.iter()
973        .zip(ema4.iter().zip(ema5.iter().zip(ema6.iter())))
974        .map(|(ema3, (ema4, (ema5, ema6)))| {
975            Some(c1 * (*ema6)? + c2 * (*ema5)? + c3 * (*ema4)? + c4 * (*ema3)?)
976        })
977        .collect()
978}
979
980pub fn trix(values: &[f64], period: usize) -> Vec<Option<f64>> {
981    let ema1 = ema(values, period);
982    let ema2 = ema(&option_values(&ema1), period);
983    let ema3 = ema(&option_values(&ema2), period);
984    let mut out = vec![None; values.len()];
985    for idx in 1..ema3.len() {
986        let prior = ema3[idx - 1].filter(|prior| prior.abs() > f64::EPSILON);
987        if let (Some(current), Some(prior)) = (ema3[idx], prior) {
988            out[idx] = Some((current / prior - 1.0) * 100.0);
989        }
990    }
991    out
992}
993
994pub fn price_transforms(
995    opens: &[f64],
996    highs: &[f64],
997    lows: &[f64],
998    closes: &[f64],
999) -> PriceTransforms {
1000    let len = opens
1001        .len()
1002        .min(highs.len())
1003        .min(lows.len())
1004        .min(closes.len());
1005    let mut avgprice = Vec::with_capacity(len);
1006    let mut medprice = Vec::with_capacity(len);
1007    let mut typprice = Vec::with_capacity(len);
1008    let mut wclprice = Vec::with_capacity(len);
1009    for idx in 0..len {
1010        let open = opens[idx];
1011        let high = highs[idx];
1012        let low = lows[idx];
1013        let close = closes[idx];
1014        avgprice.push(finite((open + high + low + close) * 0.25));
1015        medprice.push(finite((high + low) * 0.5));
1016        typprice.push(finite((high + low + close) / 3.0));
1017        wclprice.push(finite((high + low + 2.0 * close) * 0.25));
1018    }
1019    PriceTransforms {
1020        avgprice,
1021        medprice,
1022        typprice,
1023        wclprice,
1024    }
1025}
1026
1027pub fn rolling_sum(values: &[f64], period: usize) -> Vec<Option<f64>> {
1028    let mut out = vec![None; values.len()];
1029    if period == 0 {
1030        return out;
1031    }
1032    let mut sum = 0.0;
1033    let mut count = 0usize;
1034    for idx in 0..values.len() {
1035        if values[idx].is_finite() {
1036            sum += values[idx];
1037            count += 1;
1038        }
1039        if idx >= period && values[idx - period].is_finite() {
1040            sum -= values[idx - period];
1041            count -= 1;
1042        }
1043        if idx + 1 >= period && count == period {
1044            out[idx] = Some(sum);
1045        }
1046    }
1047    out
1048}
1049
1050pub fn directional_movement(highs: &[f64], lows: &[f64]) -> DirectionalMovement {
1051    let len = highs.len().min(lows.len());
1052    let mut plus_dm = vec![None; len];
1053    let mut minus_dm = vec![None; len];
1054    for idx in 1..len {
1055        let high_diff = highs[idx] - highs[idx - 1];
1056        let low_diff = lows[idx - 1] - lows[idx];
1057        if !high_diff.is_finite() || !low_diff.is_finite() {
1058            continue;
1059        }
1060        plus_dm[idx] = Some(if high_diff > low_diff && high_diff > 0.0 {
1061            high_diff
1062        } else {
1063            0.0
1064        });
1065        minus_dm[idx] = Some(if low_diff > high_diff && low_diff > 0.0 {
1066            low_diff
1067        } else {
1068            0.0
1069        });
1070    }
1071    DirectionalMovement { plus_dm, minus_dm }
1072}
1073
1074pub fn price_context(closes: &[f64]) -> PriceContext {
1075    let mut close_vs_ath_pct = vec![None; closes.len()];
1076    let mut close_vs_atl_pct = vec![None; closes.len()];
1077    let mut days_since_ath = vec![None; closes.len()];
1078    let mut days_since_atl = vec![None; closes.len()];
1079    let mut ath = f64::NEG_INFINITY;
1080    let mut atl = f64::INFINITY;
1081    let mut ath_idx = 0usize;
1082    let mut atl_idx = 0usize;
1083    for (idx, close) in closes.iter().copied().enumerate() {
1084        let Some(close) = finite(close) else {
1085            continue;
1086        };
1087        if close >= ath {
1088            ath = close;
1089            ath_idx = idx;
1090        }
1091        if close <= atl {
1092            atl = close;
1093            atl_idx = idx;
1094        }
1095        if ath.abs() > f64::EPSILON {
1096            close_vs_ath_pct[idx] = Some((close / ath - 1.0) * 100.0);
1097        }
1098        if atl.abs() > f64::EPSILON {
1099            close_vs_atl_pct[idx] = Some((close / atl - 1.0) * 100.0);
1100        }
1101        days_since_ath[idx] = Some((idx - ath_idx) as f64);
1102        days_since_atl[idx] = Some((idx - atl_idx) as f64);
1103    }
1104    PriceContext {
1105        close_vs_ath_pct,
1106        close_vs_atl_pct,
1107        days_since_ath,
1108        days_since_atl,
1109    }
1110}
1111
1112pub fn linear_regression(values: &[f64], period: usize) -> LinearRegression {
1113    let mut line = vec![None; values.len()];
1114    let mut slope_out = vec![None; values.len()];
1115    let mut angle = vec![None; values.len()];
1116    let mut intercept_out = vec![None; values.len()];
1117    let mut tsf = vec![None; values.len()];
1118    if period == 0 {
1119        return LinearRegression {
1120            line,
1121            slope: slope_out,
1122            angle,
1123            intercept: intercept_out,
1124            tsf,
1125        };
1126    }
1127    let x_mean = (period - 1) as f64 * 0.5;
1128    let x_var = (0..period)
1129        .map(|idx| {
1130            let delta = idx as f64 - x_mean;
1131            delta * delta
1132        })
1133        .sum::<f64>();
1134    if x_var <= f64::EPSILON {
1135        return LinearRegression {
1136            line,
1137            slope: slope_out,
1138            angle,
1139            intercept: intercept_out,
1140            tsf,
1141        };
1142    }
1143    if period <= values.len() && values.iter().all(|value| value.is_finite()) {
1144        // Sliding weighted-sum update adapted from talib-rs 0.1.2
1145        // (BSD-3-Clause); see THIRD_PARTY_NOTICES.md.
1146        let n = period as f64;
1147        let sum_x = n * (n - 1.0) * 0.5;
1148        let sum_x2 = n * (n - 1.0) * (2.0 * n - 1.0) / 6.0;
1149        let denom = n * sum_x2 - sum_x * sum_x;
1150        if denom <= f64::EPSILON {
1151            return LinearRegression {
1152                line,
1153                slope: slope_out,
1154                angle,
1155                intercept: intercept_out,
1156                tsf,
1157            };
1158        }
1159
1160        let mut sum_y = 0.0;
1161        let mut weighted_sum = 0.0;
1162        for (offset, value) in values.iter().take(period).enumerate() {
1163            sum_y += *value;
1164            weighted_sum += offset as f64 * *value;
1165        }
1166        let mut emit = |idx: usize, sum_y: f64, weighted_sum: f64| {
1167            let slope = (n * weighted_sum - sum_x * sum_y) / denom;
1168            let intercept = (sum_y - slope * sum_x) / n;
1169            slope_out[idx] = Some(slope);
1170            angle[idx] = Some(slope.atan().to_degrees());
1171            intercept_out[idx] = Some(intercept);
1172            line[idx] = Some(intercept + slope * (period - 1) as f64);
1173            tsf[idx] = Some(intercept + slope * period as f64);
1174        };
1175        emit(period - 1, sum_y, weighted_sum);
1176        for idx in period..values.len() {
1177            let old = values[idx - period];
1178            let new = values[idx];
1179            weighted_sum = weighted_sum - sum_y + old + (n - 1.0) * new;
1180            sum_y += new - old;
1181            emit(idx, sum_y, weighted_sum);
1182        }
1183        return LinearRegression {
1184            line,
1185            slope: slope_out,
1186            angle,
1187            intercept: intercept_out,
1188            tsf,
1189        };
1190    }
1191    for idx in period - 1..values.len() {
1192        let start = idx + 1 - period;
1193        let mut y_sum = 0.0;
1194        let mut valid = true;
1195        for value in &values[start..=idx] {
1196            let Some(value) = finite(*value) else {
1197                valid = false;
1198                break;
1199            };
1200            y_sum += value;
1201        }
1202        if !valid {
1203            continue;
1204        }
1205        let y_mean = y_sum / period as f64;
1206        let mut covariance = 0.0;
1207        for (offset, value) in values[start..=idx].iter().enumerate() {
1208            covariance += (offset as f64 - x_mean) * (*value - y_mean);
1209        }
1210        let slope = covariance / x_var;
1211        let intercept = y_mean - slope * x_mean;
1212        slope_out[idx] = Some(slope);
1213        angle[idx] = Some(slope.atan().to_degrees());
1214        intercept_out[idx] = Some(intercept);
1215        line[idx] = Some(intercept + slope * (period - 1) as f64);
1216        tsf[idx] = Some(intercept + slope * period as f64);
1217    }
1218    LinearRegression {
1219        line,
1220        slope: slope_out,
1221        angle,
1222        intercept: intercept_out,
1223        tsf,
1224    }
1225}
1226
1227pub fn sar(highs: &[f64], lows: &[f64], acceleration: f64, maximum: f64) -> Vec<Option<f64>> {
1228    let len = highs.len().min(lows.len());
1229    let mut out = vec![None; len];
1230    if len < 2 || acceleration <= 0.0 || maximum <= 0.0 {
1231        return out;
1232    }
1233    // TA-Lib picks the initial trend from the first bar's directional movement,
1234    // seeds SAR/EP from bars 0 and 1, emits the seed at index 1 with no step,
1235    // and only folds in the second prior extreme once two real prior bars exist.
1236    let dm_plus = highs[1] - highs[0];
1237    let dm_minus = lows[0] - lows[1];
1238    let mut long = !(dm_minus > 0.0 && dm_minus > dm_plus);
1239    let (mut sar, mut ep) = if long {
1240        (lows[0], highs[1])
1241    } else {
1242        (highs[0], lows[1])
1243    };
1244    let mut af = acceleration;
1245    out[1] = finite(sar);
1246
1247    for idx in 2..len {
1248        sar += af * (ep - sar);
1249        if long {
1250            sar = sar.min(lows[idx - 1]);
1251            if idx >= 3 {
1252                sar = sar.min(lows[idx - 2]);
1253            }
1254            if lows[idx] < sar {
1255                long = false;
1256                sar = ep;
1257                ep = lows[idx];
1258                af = acceleration;
1259            } else if highs[idx] > ep {
1260                ep = highs[idx];
1261                af = (af + acceleration).min(maximum);
1262            }
1263        } else {
1264            sar = sar.max(highs[idx - 1]);
1265            if idx >= 3 {
1266                sar = sar.max(highs[idx - 2]);
1267            }
1268            if highs[idx] > sar {
1269                long = true;
1270                sar = ep;
1271                ep = highs[idx];
1272                af = acceleration;
1273            } else if lows[idx] < ep {
1274                ep = lows[idx];
1275                af = (af + acceleration).min(maximum);
1276            }
1277        }
1278        out[idx] = finite(sar);
1279    }
1280    out
1281}
1282
1283/// Parabolic SAR Extended (TA-Lib `SAREXT`). Mirrors the proven `sar` core but
1284/// adds: explicit start value / initial direction, per-side acceleration
1285/// (init/step/max for long and short), an offset-on-reverse, and TA-Lib's sign
1286/// convention where short bars are emitted as the negated SAR. First value at
1287/// index 1.
1288#[allow(clippy::too_many_arguments)]
1289pub fn sarext(
1290    highs: &[f64],
1291    lows: &[f64],
1292    start_value: f64,
1293    offset_on_reverse: f64,
1294    accel_init_long: f64,
1295    accel_long: f64,
1296    accel_max_long: f64,
1297    accel_init_short: f64,
1298    accel_short: f64,
1299    accel_max_short: f64,
1300) -> Vec<Option<f64>> {
1301    let len = highs.len().min(lows.len());
1302    let mut out = vec![None; len];
1303    if len < 2 {
1304        return out;
1305    }
1306    let mut long = if start_value == 0.0 {
1307        let dm_plus = highs[1] - highs[0];
1308        let dm_minus = lows[0] - lows[1];
1309        !(dm_minus > 0.0 && dm_minus > dm_plus)
1310    } else {
1311        start_value > 0.0
1312    };
1313    let (mut sar, mut ep, mut af) = if long {
1314        let seed = if start_value == 0.0 {
1315            lows[0]
1316        } else {
1317            start_value.abs()
1318        };
1319        (seed, highs[1], accel_init_long)
1320    } else {
1321        let seed = if start_value == 0.0 {
1322            highs[0]
1323        } else {
1324            start_value.abs()
1325        };
1326        (seed, lows[1], accel_init_short)
1327    };
1328    out[1] = finite(if long { sar } else { -sar });
1329
1330    for idx in 2..len {
1331        sar += af * (ep - sar);
1332        if long {
1333            sar = sar.min(lows[idx - 1]);
1334            if idx >= 3 {
1335                sar = sar.min(lows[idx - 2]);
1336            }
1337            if lows[idx] < sar {
1338                long = false;
1339                sar = ep;
1340                if offset_on_reverse != 0.0 {
1341                    sar += sar * offset_on_reverse;
1342                }
1343                ep = lows[idx];
1344                af = accel_init_short;
1345            } else if highs[idx] > ep {
1346                ep = highs[idx];
1347                af = (af + accel_long).min(accel_max_long);
1348            }
1349        } else {
1350            sar = sar.max(highs[idx - 1]);
1351            if idx >= 3 {
1352                sar = sar.max(highs[idx - 2]);
1353            }
1354            if highs[idx] > sar {
1355                long = true;
1356                sar = ep;
1357                if offset_on_reverse != 0.0 {
1358                    sar -= sar * offset_on_reverse;
1359                }
1360                ep = highs[idx];
1361                af = accel_init_long;
1362            } else if lows[idx] < ep {
1363                ep = lows[idx];
1364                af = (af + accel_short).min(accel_max_short);
1365            }
1366        }
1367        out[idx] = finite(if long { sar } else { -sar });
1368    }
1369    out
1370}
1371
1372pub fn ad(highs: &[f64], lows: &[f64], closes: &[f64], volumes: &[f64]) -> Vec<Option<f64>> {
1373    let mut out = vec![None; closes.len()];
1374    let mut current = 0.0;
1375    for idx in 0..closes.len() {
1376        let high = highs[idx];
1377        let low = lows[idx];
1378        let close = closes[idx];
1379        let volume = volumes[idx];
1380        let range = high - low;
1381        if !range.is_finite() || range.abs() <= f64::EPSILON || !volume.is_finite() {
1382            out[idx] = Some(current);
1383            continue;
1384        }
1385        let multiplier = ((close - low) - (high - close)) / range;
1386        if multiplier.is_finite() {
1387            current += multiplier * volume;
1388        }
1389        out[idx] = Some(current);
1390    }
1391    out
1392}
1393
1394pub fn adosc(
1395    highs: &[f64],
1396    lows: &[f64],
1397    closes: &[f64],
1398    volumes: &[f64],
1399    fast: usize,
1400    slow: usize,
1401) -> Vec<Option<f64>> {
1402    let mut ad_line = vec![0.0; closes.len()];
1403    let mut current = 0.0;
1404    for idx in 0..closes.len() {
1405        let high = highs[idx];
1406        let low = lows[idx];
1407        let close = closes[idx];
1408        let volume = volumes[idx];
1409        let range = high - low;
1410        if range.is_finite() && range.abs() > f64::EPSILON && volume.is_finite() {
1411            let multiplier = ((close - low) - (high - close)) / range;
1412            if multiplier.is_finite() {
1413                current += multiplier * volume;
1414            }
1415        }
1416        ad_line[idx] = current;
1417    }
1418    let len = ad_line.len();
1419    let mut out = vec![None; len];
1420    if fast == 0 || slow == 0 || len == 0 {
1421        return out;
1422    }
1423    // TA-Lib seeds both AD-line EMAs with the first AD value, runs the recurrence
1424    // from bar 1, and emits the oscillator from `slow - 1`.
1425    let fast_k = 2.0 / (fast as f64 + 1.0);
1426    let slow_k = 2.0 / (slow as f64 + 1.0);
1427    let mut fast_ema = ad_line[0];
1428    let mut slow_ema = ad_line[0];
1429    let first_out = slow - 1;
1430    for idx in 1..len {
1431        let value = ad_line[idx];
1432        fast_ema = fast_k * value + (1.0 - fast_k) * fast_ema;
1433        slow_ema = slow_k * value + (1.0 - slow_k) * slow_ema;
1434        if idx >= first_out {
1435            out[idx] = Some(fast_ema - slow_ema);
1436        }
1437    }
1438    out
1439}
1440
1441pub fn adx_family(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> AdxFamily {
1442    let len = highs.len().min(lows.len()).min(closes.len());
1443    let mut plus_di = vec![None; len];
1444    let mut minus_di = vec![None; len];
1445    let mut dx = vec![None; len];
1446    let mut adx = vec![None; len];
1447    let mut adxr = vec![None; len];
1448    if period == 0 || len <= period {
1449        return AdxFamily {
1450            adx,
1451            adxr,
1452            plus_di,
1453            minus_di,
1454            dx,
1455        };
1456    }
1457
1458    let mut tr = vec![0.0; len];
1459    let mut plus_dm = vec![0.0; len];
1460    let mut minus_dm = vec![0.0; len];
1461    for idx in 1..len {
1462        let high_diff = highs[idx] - highs[idx - 1];
1463        let low_diff = lows[idx - 1] - lows[idx];
1464        plus_dm[idx] = if high_diff > low_diff && high_diff > 0.0 {
1465            high_diff
1466        } else {
1467            0.0
1468        };
1469        minus_dm[idx] = if low_diff > high_diff && low_diff > 0.0 {
1470            low_diff
1471        } else {
1472            0.0
1473        };
1474        tr[idx] = (highs[idx] - lows[idx])
1475            .max((highs[idx] - closes[idx - 1]).abs())
1476            .max((lows[idx] - closes[idx - 1]).abs());
1477    }
1478
1479    // TA-Lib seeds the Wilder sums over the first `period - 1` deltas (indices
1480    // 1..=period-1), then applies the first smoothing step at `idx == period`,
1481    // which folds in the period-th delta. Using the Wilder "sum" form here is
1482    // exact for the DI ratios (the period factor cancels).
1483    let mut smooth_tr = tr[1..period].iter().sum::<f64>();
1484    let mut smooth_plus = plus_dm[1..period].iter().sum::<f64>();
1485    let mut smooth_minus = minus_dm[1..period].iter().sum::<f64>();
1486    for idx in period..len {
1487        smooth_tr = smooth_tr - smooth_tr / period as f64 + tr[idx];
1488        smooth_plus = smooth_plus - smooth_plus / period as f64 + plus_dm[idx];
1489        smooth_minus = smooth_minus - smooth_minus / period as f64 + minus_dm[idx];
1490        if smooth_tr > f64::EPSILON {
1491            let plus = 100.0 * smooth_plus / smooth_tr;
1492            let minus = 100.0 * smooth_minus / smooth_tr;
1493            plus_di[idx] = Some(plus);
1494            minus_di[idx] = Some(minus);
1495            let sum = plus + minus;
1496            dx[idx] = if sum > f64::EPSILON {
1497                Some(100.0 * (plus - minus).abs() / sum)
1498            } else {
1499                Some(0.0)
1500            };
1501        }
1502    }
1503
1504    let mut warm_dx = Vec::with_capacity(period);
1505    let mut smooth_adx = None;
1506    for idx in period..len {
1507        let Some(current_dx) = dx[idx] else {
1508            continue;
1509        };
1510        if let Some(prev) = smooth_adx {
1511            let next = (prev * (period as f64 - 1.0) + current_dx) / period as f64;
1512            smooth_adx = Some(next);
1513            adx[idx] = Some(next);
1514        } else {
1515            warm_dx.push(current_dx);
1516            if warm_dx.len() == period {
1517                let next = warm_dx.iter().sum::<f64>() / period as f64;
1518                smooth_adx = Some(next);
1519                adx[idx] = Some(next);
1520            }
1521        }
1522    }
1523
1524    // TA-Lib ADXR averages the current ADX with the ADX from `period - 1` bars
1525    // back (not `period`).
1526    let adxr_lag = period - 1;
1527    for idx in adxr_lag..len {
1528        if let (Some(current), Some(prior)) = (adx[idx], adx[idx - adxr_lag]) {
1529            adxr[idx] = Some((current + prior) * 0.5);
1530        }
1531    }
1532
1533    AdxFamily {
1534        adx,
1535        adxr,
1536        plus_di,
1537        minus_di,
1538        dx,
1539    }
1540}
1541
1542pub fn aroon(highs: &[f64], lows: &[f64], period: usize) -> Aroon {
1543    let len = highs.len().min(lows.len());
1544    let mut up = vec![None; len];
1545    let mut down = vec![None; len];
1546    let mut oscillator = vec![None; len];
1547    if period == 0 {
1548        return Aroon {
1549            up,
1550            down,
1551            oscillator,
1552        };
1553    }
1554    if period < len
1555        && highs[..len].iter().all(|value| value.is_finite())
1556        && lows[..len].iter().all(|value| value.is_finite())
1557    {
1558        // Rolling extremum index update adapted from talib-rs 0.1.2
1559        // (BSD-3-Clause); see THIRD_PARTY_NOTICES.md.
1560        let scale = 100.0 / period as f64;
1561        let window = period + 1;
1562        let mut high_value = highs[0];
1563        let mut high_idx = 0usize;
1564        let mut low_value = lows[0];
1565        let mut low_idx = 0usize;
1566        for idx in 1..window {
1567            if highs[idx] >= high_value {
1568                high_value = highs[idx];
1569                high_idx = idx;
1570            }
1571            if lows[idx] <= low_value {
1572                low_value = lows[idx];
1573                low_idx = idx;
1574            }
1575        }
1576        let emit = |idx: usize,
1577                    high_idx: usize,
1578                    low_idx: usize,
1579                    up: &mut [Option<f64>],
1580                    down: &mut [Option<f64>],
1581                    oscillator: &mut [Option<f64>]| {
1582            let up_value = (period - (idx - high_idx)) as f64 * scale;
1583            let down_value = (period - (idx - low_idx)) as f64 * scale;
1584            up[idx] = Some(up_value);
1585            down[idx] = Some(down_value);
1586            oscillator[idx] = Some(up_value - down_value);
1587        };
1588        emit(
1589            period,
1590            high_idx,
1591            low_idx,
1592            &mut up,
1593            &mut down,
1594            &mut oscillator,
1595        );
1596
1597        let mut trailing_idx = 1usize;
1598        for idx in (period + 1)..len {
1599            if high_idx < trailing_idx {
1600                high_idx = trailing_idx;
1601                high_value = highs[trailing_idx];
1602                for (offset, value) in highs[trailing_idx + 1..=idx].iter().enumerate() {
1603                    if *value >= high_value {
1604                        high_value = *value;
1605                        high_idx = trailing_idx + 1 + offset;
1606                    }
1607                }
1608            } else if highs[idx] >= high_value {
1609                high_value = highs[idx];
1610                high_idx = idx;
1611            }
1612
1613            if low_idx < trailing_idx {
1614                low_idx = trailing_idx;
1615                low_value = lows[trailing_idx];
1616                for (offset, value) in lows[trailing_idx + 1..=idx].iter().enumerate() {
1617                    if *value <= low_value {
1618                        low_value = *value;
1619                        low_idx = trailing_idx + 1 + offset;
1620                    }
1621                }
1622            } else if lows[idx] <= low_value {
1623                low_value = lows[idx];
1624                low_idx = idx;
1625            }
1626
1627            emit(idx, high_idx, low_idx, &mut up, &mut down, &mut oscillator);
1628            trailing_idx += 1;
1629        }
1630        return Aroon {
1631            up,
1632            down,
1633            oscillator,
1634        };
1635    }
1636    // TA-Lib scans a trailing window of `period + 1` bars (the current bar plus
1637    // the prior `period`), emitting the first value at index `period`.
1638    for idx in period..len {
1639        let start = idx - period;
1640        let mut high_value = f64::NEG_INFINITY;
1641        let mut low_value = f64::INFINITY;
1642        let mut high_idx = start;
1643        let mut low_idx = start;
1644        let mut valid = true;
1645        for offset in start..=idx {
1646            let high = highs[offset];
1647            let low = lows[offset];
1648            if !high.is_finite() || !low.is_finite() {
1649                valid = false;
1650                break;
1651            }
1652            if high >= high_value {
1653                high_value = high;
1654                high_idx = offset;
1655            }
1656            if low <= low_value {
1657                low_value = low;
1658                low_idx = offset;
1659            }
1660        }
1661        if valid {
1662            let scale = 100.0 / period as f64;
1663            let up_value = (period - (idx - high_idx)) as f64 * scale;
1664            let down_value = (period - (idx - low_idx)) as f64 * scale;
1665            up[idx] = Some(up_value);
1666            down[idx] = Some(down_value);
1667            oscillator[idx] = Some(up_value - down_value);
1668        }
1669    }
1670    Aroon {
1671        up,
1672        down,
1673        oscillator,
1674    }
1675}
1676
1677// ===========================================================================
1678// Hilbert Transform family (John Ehlers): HT_DCPERIOD, HT_PHASOR, MAMA/FAMA,
1679// HT_DCPHASE, HT_SINE, HT_TRENDLINE, HT_TRENDMODE.
1680//
1681// Faithful port of TA-Lib's shared Hilbert core. Every 32-lookback function
1682// (DCPERIOD, PHASOR, MAMA/FAMA) shares one identical warmup trajectory, and
1683// every 63-lookback function (DCPHASE, SINE, TRENDLINE, TRENDMODE) shares
1684// another. Because the cores are bit-for-bit identical across functions in a
1685// group, we run two unified passes and slice each function's output from them
1686// (this yields exactly the same values TA-Lib produces per-function).
1687// ===========================================================================
1688
1689const HT_A: f64 = 0.0962;
1690const HT_B: f64 = 0.5769;
1691
1692#[inline]
1693fn ht_rad2deg() -> f64 {
1694    // TA-Lib: rad2Deg = 45.0 / atan(1) = 180/PI.
1695    45.0 / (1.0_f64).atan()
1696}
1697
1698/// Period-4 weighted moving average smoother (TA-Lib DO_PRICE_WMA macro state).
1699struct HtWma {
1700    sub: f64,
1701    sum: f64,
1702    trailing_value: f64,
1703    trailing_idx: usize,
1704}
1705
1706impl HtWma {
1707    #[inline]
1708    fn next(&mut self, new_price: f64, close: &[f64]) -> f64 {
1709        self.sub += new_price;
1710        self.sub -= self.trailing_value;
1711        self.sum += new_price * 4.0;
1712        self.trailing_value = close[self.trailing_idx];
1713        self.trailing_idx += 1;
1714        let smoothed = self.sum * 0.1;
1715        self.sum -= self.sub;
1716        smoothed
1717    }
1718}
1719
1720/// One Hilbert transform channel with odd/even circular buffers
1721/// (TA-Lib HILBERT_VARIABLES / DO_HILBERT_TRANSFORM macros).
1722#[derive(Default)]
1723struct HtChannel {
1724    odd: [f64; 3],
1725    even: [f64; 3],
1726    prev_odd: f64,
1727    prev_even: f64,
1728    prev_in_odd: f64,
1729    prev_in_even: f64,
1730}
1731
1732impl HtChannel {
1733    #[inline]
1734    fn transform(&mut self, input: f64, adjusted_prev_period: f64, idx: usize, even: bool) -> f64 {
1735        let temp = HT_A * input;
1736        let mut value;
1737        if even {
1738            value = -self.even[idx];
1739            self.even[idx] = temp;
1740            value += temp;
1741            value -= self.prev_even;
1742            self.prev_even = HT_B * self.prev_in_even;
1743            value += self.prev_even;
1744            self.prev_in_even = input;
1745        } else {
1746            value = -self.odd[idx];
1747            self.odd[idx] = temp;
1748            value += temp;
1749            value -= self.prev_odd;
1750            self.prev_odd = HT_B * self.prev_in_odd;
1751            value += self.prev_odd;
1752            self.prev_in_odd = input;
1753        }
1754        value * adjusted_prev_period
1755    }
1756}
1757
1758struct Ht32 {
1759    dcperiod: Vec<Option<f64>>,
1760    inphase: Vec<Option<f64>>,
1761    quadrature: Vec<Option<f64>>,
1762    mama: Vec<Option<f64>>,
1763    fama: Vec<Option<f64>>,
1764}
1765
1766/// Unified 32-lookback Hilbert pass. `fast_limit`/`slow_limit` only affect the
1767/// MAMA/FAMA outputs; DCPERIOD and PHASOR are independent of them.
1768fn ht32(close: &[f64], fast_limit: f64, slow_limit: f64) -> Ht32 {
1769    let n = close.len();
1770    let lookback = 32usize;
1771    let mut dcperiod = vec![None; n];
1772    let mut inphase = vec![None; n];
1773    let mut quadrature = vec![None; n];
1774    let mut mama_out = vec![None; n];
1775    let mut fama_out = vec![None; n];
1776    if n <= lookback {
1777        return Ht32 {
1778            dcperiod,
1779            inphase,
1780            quadrature,
1781            mama: mama_out,
1782            fama: fama_out,
1783        };
1784    }
1785    let rad2deg = ht_rad2deg();
1786    let start_idx = lookback;
1787    let end_idx = n - 1;
1788
1789    let mut today = 0usize; // trailing_wma_idx = start_idx - lookback = 0
1790    let mut t = close[today];
1791    today += 1;
1792    let mut sub = t;
1793    let mut sum = t;
1794    t = close[today];
1795    today += 1;
1796    sub += t;
1797    sum += t * 2.0;
1798    t = close[today];
1799    today += 1;
1800    sub += t;
1801    sum += t * 3.0;
1802    let mut wma = HtWma {
1803        sub,
1804        sum,
1805        trailing_value: 0.0,
1806        trailing_idx: 0,
1807    };
1808    for _ in 0..9 {
1809        let p = close[today];
1810        today += 1;
1811        wma.next(p, close);
1812    }
1813
1814    let mut hilbert_idx = 0usize;
1815    let mut detrender = HtChannel::default();
1816    let mut q1c = HtChannel::default();
1817    let mut jic = HtChannel::default();
1818    let mut jqc = HtChannel::default();
1819    let mut period = 0.0;
1820    let mut smooth_period = 0.0;
1821    let mut prev_i2 = 0.0;
1822    let mut prev_q2 = 0.0;
1823    let mut re = 0.0;
1824    let mut im = 0.0;
1825    let mut i1_odd_prev3 = 0.0;
1826    let mut i1_odd_prev2 = 0.0;
1827    let mut i1_even_prev3 = 0.0;
1828    let mut i1_even_prev2 = 0.0;
1829    let mut mama = 0.0;
1830    let mut fama = 0.0;
1831    let mut prev_phase = 0.0;
1832
1833    while today <= end_idx {
1834        let adjusted_prev_period = 0.075 * period + 0.54;
1835        let today_value = close[today];
1836        let smoothed = wma.next(today_value, close);
1837
1838        let q1v;
1839        let i1;
1840        let q2;
1841        let i2;
1842        if today % 2 == 0 {
1843            let det = detrender.transform(smoothed, adjusted_prev_period, hilbert_idx, true);
1844            let q1n = q1c.transform(det, adjusted_prev_period, hilbert_idx, true);
1845            let jin = jic.transform(i1_even_prev3, adjusted_prev_period, hilbert_idx, true);
1846            let jqn = jqc.transform(q1n, adjusted_prev_period, hilbert_idx, true);
1847            hilbert_idx += 1;
1848            if hilbert_idx == 3 {
1849                hilbert_idx = 0;
1850            }
1851            q2 = 0.2 * (q1n + jin) + 0.8 * prev_q2;
1852            i2 = 0.2 * (i1_even_prev3 - jqn) + 0.8 * prev_i2;
1853            i1_odd_prev3 = i1_odd_prev2;
1854            i1_odd_prev2 = det;
1855            q1v = q1n;
1856            i1 = i1_even_prev3;
1857        } else {
1858            let det = detrender.transform(smoothed, adjusted_prev_period, hilbert_idx, false);
1859            let q1n = q1c.transform(det, adjusted_prev_period, hilbert_idx, false);
1860            let jin = jic.transform(i1_odd_prev3, adjusted_prev_period, hilbert_idx, false);
1861            let jqn = jqc.transform(q1n, adjusted_prev_period, hilbert_idx, false);
1862            q2 = 0.2 * (q1n + jin) + 0.8 * prev_q2;
1863            i2 = 0.2 * (i1_odd_prev3 - jqn) + 0.8 * prev_i2;
1864            i1_even_prev3 = i1_even_prev2;
1865            i1_even_prev2 = det;
1866            q1v = q1n;
1867            i1 = i1_odd_prev3;
1868        }
1869
1870        // MAMA / FAMA adaptive smoothing.
1871        let alpha_phase = if i1 != 0.0 {
1872            (q1v / i1).atan() * rad2deg
1873        } else {
1874            0.0
1875        };
1876        let mut delta = prev_phase - alpha_phase;
1877        prev_phase = alpha_phase;
1878        if delta < 1.0 {
1879            delta = 1.0;
1880        }
1881        let alpha = if delta > 1.0 {
1882            (fast_limit / delta).max(slow_limit)
1883        } else {
1884            fast_limit
1885        };
1886        mama = alpha * today_value + (1.0 - alpha) * mama;
1887        let alpha_half = alpha * 0.5;
1888        fama = alpha_half * mama + (1.0 - alpha_half) * fama;
1889
1890        // Dominant-cycle period update (shared by all 32-lookback functions).
1891        re = 0.2 * ((i2 * prev_i2) + (q2 * prev_q2)) + 0.8 * re;
1892        im = 0.2 * ((i2 * prev_q2) - (q2 * prev_i2)) + 0.8 * im;
1893        prev_q2 = q2;
1894        prev_i2 = i2;
1895        let prev_period = period;
1896        if im != 0.0 && re != 0.0 {
1897            period = 360.0 / ((im / re).atan() * rad2deg);
1898        }
1899        let hi = 1.5 * prev_period;
1900        if period > hi {
1901            period = hi;
1902        }
1903        let lo = 0.67 * prev_period;
1904        if period < lo {
1905            period = lo;
1906        }
1907        period = period.clamp(6.0, 50.0);
1908        period = 0.2 * period + 0.8 * prev_period;
1909        smooth_period = 0.33 * period + 0.67 * smooth_period;
1910
1911        if today >= start_idx {
1912            dcperiod[today] = Some(smooth_period);
1913            inphase[today] = Some(i1);
1914            quadrature[today] = Some(q1v);
1915            mama_out[today] = Some(mama);
1916            fama_out[today] = Some(fama);
1917        }
1918        today += 1;
1919    }
1920
1921    Ht32 {
1922        dcperiod,
1923        inphase,
1924        quadrature,
1925        mama: mama_out,
1926        fama: fama_out,
1927    }
1928}
1929
1930struct Ht63 {
1931    dcphase: Vec<Option<f64>>,
1932    sine: Vec<Option<f64>>,
1933    leadsine: Vec<Option<f64>>,
1934    trendline: Vec<Option<f64>>,
1935    trendmode: Vec<Option<f64>>,
1936}
1937
1938/// Unified 63-lookback Hilbert pass (DCPHASE, SINE, TRENDLINE, TRENDMODE).
1939fn ht63(close: &[f64]) -> Ht63 {
1940    const SMOOTH_PRICE_SIZE: usize = 50;
1941    let n = close.len();
1942    let lookback = 63usize;
1943    let mut dcphase = vec![None; n];
1944    let mut sine = vec![None; n];
1945    let mut leadsine = vec![None; n];
1946    let mut trendline = vec![None; n];
1947    // TA-Lib zero-fills the TREND_MODE warmup region (integer 0/1, no nulls).
1948    let mut trendmode = vec![Some(0.0); n];
1949    if n <= lookback {
1950        return Ht63 {
1951            dcphase,
1952            sine,
1953            leadsine,
1954            trendline,
1955            trendmode,
1956        };
1957    }
1958    let rad2deg = ht_rad2deg();
1959    let deg2rad = 1.0 / rad2deg;
1960    let const_deg2rad_by360 = (1.0_f64).atan() * 8.0; // = 2*PI
1961    let start_idx = lookback;
1962    let end_idx = n - 1;
1963
1964    let mut today = 0usize;
1965    let mut t = close[today];
1966    today += 1;
1967    let mut sub = t;
1968    let mut sum = t;
1969    t = close[today];
1970    today += 1;
1971    sub += t;
1972    sum += t * 2.0;
1973    t = close[today];
1974    today += 1;
1975    sub += t;
1976    sum += t * 3.0;
1977    let mut wma = HtWma {
1978        sub,
1979        sum,
1980        trailing_value: 0.0,
1981        trailing_idx: 0,
1982    };
1983    for _ in 0..34 {
1984        let p = close[today];
1985        today += 1;
1986        wma.next(p, close);
1987    }
1988
1989    let mut hilbert_idx = 0usize;
1990    let mut detrender = HtChannel::default();
1991    let mut q1c = HtChannel::default();
1992    let mut jic = HtChannel::default();
1993    let mut jqc = HtChannel::default();
1994    let mut period = 0.0;
1995    let mut smooth_period = 0.0;
1996    let mut prev_i2 = 0.0;
1997    let mut prev_q2 = 0.0;
1998    let mut re = 0.0;
1999    let mut im = 0.0;
2000    let mut i1_odd_prev3 = 0.0;
2001    let mut i1_odd_prev2 = 0.0;
2002    let mut i1_even_prev3 = 0.0;
2003    let mut i1_even_prev2 = 0.0;
2004
2005    let mut smooth_price = [0.0f64; SMOOTH_PRICE_SIZE];
2006    let mut smooth_price_idx = 0usize;
2007    let mut dc_phase = 0.0;
2008    let mut sine_val = 0.0;
2009    let mut lead_sine_val = 0.0;
2010    let mut i_trend1 = 0.0;
2011    let mut i_trend2 = 0.0;
2012    let mut i_trend3 = 0.0;
2013    let mut days_in_trend = 0i32;
2014
2015    while today <= end_idx {
2016        let adjusted_prev_period = 0.075 * period + 0.54;
2017        let today_value = close[today];
2018        let smoothed = wma.next(today_value, close);
2019        smooth_price[smooth_price_idx] = smoothed;
2020
2021        let q2;
2022        let i2;
2023        if today % 2 == 0 {
2024            let det = detrender.transform(smoothed, adjusted_prev_period, hilbert_idx, true);
2025            let q1n = q1c.transform(det, adjusted_prev_period, hilbert_idx, true);
2026            let jin = jic.transform(i1_even_prev3, adjusted_prev_period, hilbert_idx, true);
2027            let jqn = jqc.transform(q1n, adjusted_prev_period, hilbert_idx, true);
2028            hilbert_idx += 1;
2029            if hilbert_idx == 3 {
2030                hilbert_idx = 0;
2031            }
2032            q2 = 0.2 * (q1n + jin) + 0.8 * prev_q2;
2033            i2 = 0.2 * (i1_even_prev3 - jqn) + 0.8 * prev_i2;
2034            i1_odd_prev3 = i1_odd_prev2;
2035            i1_odd_prev2 = det;
2036        } else {
2037            let det = detrender.transform(smoothed, adjusted_prev_period, hilbert_idx, false);
2038            let q1n = q1c.transform(det, adjusted_prev_period, hilbert_idx, false);
2039            let jin = jic.transform(i1_odd_prev3, adjusted_prev_period, hilbert_idx, false);
2040            let jqn = jqc.transform(q1n, adjusted_prev_period, hilbert_idx, false);
2041            q2 = 0.2 * (q1n + jin) + 0.8 * prev_q2;
2042            i2 = 0.2 * (i1_odd_prev3 - jqn) + 0.8 * prev_i2;
2043            i1_even_prev3 = i1_even_prev2;
2044            i1_even_prev2 = det;
2045        }
2046
2047        re = 0.2 * ((i2 * prev_i2) + (q2 * prev_q2)) + 0.8 * re;
2048        im = 0.2 * ((i2 * prev_q2) - (q2 * prev_i2)) + 0.8 * im;
2049        prev_q2 = q2;
2050        prev_i2 = i2;
2051        let prev_period = period;
2052        if im != 0.0 && re != 0.0 {
2053            period = 360.0 / ((im / re).atan() * rad2deg);
2054        }
2055        let hi = 1.5 * prev_period;
2056        if period > hi {
2057            period = hi;
2058        }
2059        let lo = 0.67 * prev_period;
2060        if period < lo {
2061            period = lo;
2062        }
2063        period = period.clamp(6.0, 50.0);
2064        period = 0.2 * period + 0.8 * prev_period;
2065        smooth_period = 0.33 * period + 0.67 * smooth_period;
2066
2067        let prev_dc_phase = dc_phase;
2068
2069        // Dominant cycle phase from the smoothed-price circular buffer.
2070        let dc_period_int = (smooth_period + 0.5) as i32;
2071        let mut real_part = 0.0;
2072        let mut imag_part = 0.0;
2073        let mut idx = smooth_price_idx;
2074        for i in 0..dc_period_int {
2075            let angle = (i as f64 * const_deg2rad_by360) / dc_period_int as f64;
2076            let value = smooth_price[idx];
2077            real_part += angle.sin() * value;
2078            imag_part += angle.cos() * value;
2079            if idx == 0 {
2080                idx = SMOOTH_PRICE_SIZE - 1;
2081            } else {
2082                idx -= 1;
2083            }
2084        }
2085        let abs_imag = imag_part.abs();
2086        if abs_imag > 0.0 {
2087            dc_phase = (real_part / imag_part).atan() * rad2deg;
2088        } else if abs_imag <= 0.01 {
2089            if real_part < 0.0 {
2090                dc_phase -= 90.0;
2091            } else if real_part > 0.0 {
2092                dc_phase += 90.0;
2093            }
2094        }
2095        dc_phase += 90.0;
2096        dc_phase += 360.0 / smooth_period;
2097        if imag_part < 0.0 {
2098            dc_phase += 180.0;
2099        }
2100        if dc_phase > 315.0 {
2101            dc_phase -= 360.0;
2102        }
2103
2104        let prev_sine = sine_val;
2105        let prev_lead_sine = lead_sine_val;
2106        sine_val = (dc_phase * deg2rad).sin();
2107        lead_sine_val = ((dc_phase + 45.0) * deg2rad).sin();
2108
2109        // Trendline: average of raw close over the dominant-cycle period,
2110        // smoothed by the iTrend WMA.
2111        let dc_period_int2 = (smooth_period + 0.5) as i32;
2112        let mut sum_close = 0.0;
2113        let mut k = today as i64;
2114        for _ in 0..dc_period_int2 {
2115            if k < 0 {
2116                break;
2117            }
2118            sum_close += close[k as usize];
2119            k -= 1;
2120        }
2121        if dc_period_int2 > 0 {
2122            sum_close /= dc_period_int2 as f64;
2123        }
2124        let trendline_val = (4.0 * sum_close + 3.0 * i_trend1 + 2.0 * i_trend2 + i_trend3) / 10.0;
2125        i_trend3 = i_trend2;
2126        i_trend2 = i_trend1;
2127        i_trend1 = sum_close;
2128
2129        // Trend mode (assume trend, then disqualify).
2130        let mut trend = 1i32;
2131        if (sine_val > lead_sine_val && prev_sine <= prev_lead_sine)
2132            || (sine_val < lead_sine_val && prev_sine >= prev_lead_sine)
2133        {
2134            days_in_trend = 0;
2135            trend = 0;
2136        }
2137        days_in_trend += 1;
2138        if (days_in_trend as f64) < 0.5 * smooth_period {
2139            trend = 0;
2140        }
2141        let dphase_delta = dc_phase - prev_dc_phase;
2142        if smooth_period != 0.0
2143            && dphase_delta > 0.67 * 360.0 / smooth_period
2144            && dphase_delta < 1.5 * 360.0 / smooth_period
2145        {
2146            trend = 0;
2147        }
2148        let cur_smooth = smooth_price[smooth_price_idx];
2149        if trendline_val != 0.0 && ((cur_smooth - trendline_val) / trendline_val).abs() >= 0.015 {
2150            trend = 1;
2151        }
2152
2153        if today >= start_idx {
2154            dcphase[today] = Some(dc_phase);
2155            sine[today] = Some(sine_val);
2156            leadsine[today] = Some(lead_sine_val);
2157            trendline[today] = Some(trendline_val);
2158            trendmode[today] = Some(trend as f64);
2159        }
2160
2161        smooth_price_idx += 1;
2162        if smooth_price_idx == SMOOTH_PRICE_SIZE {
2163            smooth_price_idx = 0;
2164        }
2165        today += 1;
2166    }
2167
2168    Ht63 {
2169        dcphase,
2170        sine,
2171        leadsine,
2172        trendline,
2173        trendmode,
2174    }
2175}
2176
2177/// HT_DCPERIOD — dominant cycle period.
2178pub fn ht_dcperiod(close: &[f64]) -> Vec<Option<f64>> {
2179    ht32(close, 0.5, 0.05).dcperiod
2180}
2181
2182/// HT_PHASOR — (in-phase, quadrature) components.
2183pub fn ht_phasor(close: &[f64]) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
2184    let out = ht32(close, 0.5, 0.05);
2185    (out.inphase, out.quadrature)
2186}
2187
2188/// MAMA — (MAMA, FAMA) adaptive moving averages.
2189pub fn mama(
2190    close: &[f64],
2191    fast_limit: f64,
2192    slow_limit: f64,
2193) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
2194    let out = ht32(close, fast_limit, slow_limit);
2195    (out.mama, out.fama)
2196}
2197
2198/// HT_DCPHASE — dominant cycle phase.
2199pub fn ht_dcphase(close: &[f64]) -> Vec<Option<f64>> {
2200    ht63(close).dcphase
2201}
2202
2203/// HT_SINE — (sine, lead sine) wave.
2204pub fn ht_sine(close: &[f64]) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
2205    let out = ht63(close);
2206    (out.sine, out.leadsine)
2207}
2208
2209/// HT_TRENDLINE — instantaneous trendline.
2210pub fn ht_trendline(close: &[f64]) -> Vec<Option<f64>> {
2211    ht63(close).trendline
2212}
2213
2214/// HT_TRENDMODE — trend vs cycle mode (1 = trend, 0 = cycle; warmup zero-filled).
2215pub fn ht_trendmode(close: &[f64]) -> Vec<Option<f64>> {
2216    ht63(close).trendmode
2217}
2218
2219#[cfg(test)]
2220mod tests {
2221    use super::*;
2222
2223    fn assert_close(actual: Option<f64>, expected: f64) {
2224        let actual = actual.expect("expected Some value");
2225        assert!(
2226            (actual - expected).abs() < 1e-9,
2227            "actual {actual} expected {expected}"
2228        );
2229    }
2230
2231    #[test]
2232    fn macd_hist_is_difference_between_macd_and_signal() {
2233        let values = (1..40).map(|value| value as f64).collect::<Vec<_>>();
2234        let out = macd(&values, 3, 6, 3);
2235        let idx = out.hist.iter().position(Option::is_some).unwrap();
2236        assert_close(
2237            out.hist[idx],
2238            out.macd[idx].unwrap() - out.signal[idx].unwrap(),
2239        );
2240        let fixed = macdfix(&values, 9);
2241        assert!(fixed.hist.iter().any(Option::is_some));
2242    }
2243
2244    #[test]
2245    fn dema_and_tema_warm_after_nested_emas() {
2246        let values = (1..30).map(|value| value as f64).collect::<Vec<_>>();
2247        assert!(dema(&values, 5).iter().any(Option::is_some));
2248        assert!(tema(&values, 5).iter().any(Option::is_some));
2249        assert!(t3(&values, 5, 0.7).iter().any(Option::is_some));
2250        assert!(trix(&values, 5).iter().any(Option::is_some));
2251        assert!(kama(&values, 5).iter().any(Option::is_some));
2252    }
2253
2254    #[test]
2255    fn bollinger_bands_match_population_std_window() {
2256        let values = [1.0, 2.0, 3.0];
2257        let out = bollinger_bands(&values, 3, 2.0);
2258        let std = (2.0_f64 / 3.0).sqrt();
2259        assert_close(out.middle[2], 2.0);
2260        assert_close(out.upper[2], 2.0 + 2.0 * std);
2261        assert_close(out.lower[2], 2.0 - 2.0 * std);
2262    }
2263
2264    #[test]
2265    fn price_transform_formulas_match_talib_names() {
2266        let out = price_transforms(&[1.0], &[4.0], &[2.0], &[3.0]);
2267        assert_close(out.avgprice[0], 2.5);
2268        assert_close(out.medprice[0], 3.0);
2269        assert_close(out.typprice[0], 3.0);
2270        assert_close(out.wclprice[0], 3.0);
2271    }
2272
2273    #[test]
2274    fn bop_cmo_and_ultosc_emit_bounded_momentum_values() {
2275        let opens = [9.0, 10.0, 11.0, 10.0, 9.0, 10.0, 11.0, 12.0];
2276        let highs = [10.0, 11.0, 12.0, 11.0, 10.0, 11.0, 12.0, 13.0];
2277        let lows = [8.0, 9.0, 10.0, 9.0, 8.0, 9.0, 10.0, 11.0];
2278        let closes = [10.0, 11.0, 10.0, 9.0, 10.0, 11.0, 12.0, 13.0];
2279        assert_close(bop(&opens, &highs, &lows, &closes)[0], 0.5);
2280        assert_close(cmo(&closes, 3)[3], -100.0 / 3.0);
2281        assert!(
2282            ultosc(&highs, &lows, &closes, 2, 3, 4)[4]
2283                .is_some_and(|value| (0.0..=100.0).contains(&value))
2284        );
2285    }
2286
2287    #[test]
2288    fn rsi_and_stochrsi_warm_then_emit_bounded_values() {
2289        let values = [
2290            10.0, 11.0, 12.0, 11.0, 13.0, 14.0, 13.0, 15.0, 16.0, 15.0, 17.0, 18.0, 19.0, 18.0,
2291            20.0, 21.0,
2292        ];
2293        let rsi_out = rsi(&values, 5);
2294        assert_eq!(rsi_out[4], None);
2295        assert!(
2296            rsi_out
2297                .iter()
2298                .flatten()
2299                .all(|value| (0.0..=100.0).contains(value))
2300        );
2301
2302        let stoch = stochrsi(&values, 5, 5, 3);
2303        assert!(stoch.k.iter().any(Option::is_some));
2304        assert!(stoch.d.iter().any(Option::is_some));
2305        assert!(
2306            stoch
2307                .k
2308                .iter()
2309                .flatten()
2310                .all(|value| (0.0..=100.0).contains(value))
2311        );
2312        assert!(
2313            stoch
2314                .d
2315                .iter()
2316                .flatten()
2317                .all(|value| (0.0..=100.0).contains(value))
2318        );
2319    }
2320
2321    #[test]
2322    fn linear_regression_family_matches_simple_line() {
2323        let values = [2.0, 4.0, 6.0, 8.0, 10.0];
2324        let out = linear_regression(&values, 3);
2325        assert_close(out.intercept[2], 2.0);
2326        assert_close(out.slope[2], 2.0);
2327        assert_close(out.line[2], 6.0);
2328        assert_close(out.tsf[2], 8.0);
2329        assert_close(out.angle[2], 2.0_f64.atan().to_degrees());
2330    }
2331
2332    #[test]
2333    fn rolling_sum_and_sar_have_explicit_warmup() {
2334        let values = [1.0, 2.0, 3.0, 4.0];
2335        assert_eq!(rolling_sum(&values, 3)[1], None);
2336        assert_close(rolling_sum(&values, 3)[2], 6.0);
2337        let highs = [10.0, 11.0, 12.0, 13.0];
2338        let lows = [9.0, 10.0, 11.0, 12.0];
2339        let out = sar(&highs, &lows, 0.02, 0.2);
2340        assert_eq!(out[0], None);
2341        assert!(out.iter().skip(1).all(Option::is_some));
2342    }
2343
2344    #[test]
2345    fn directional_movement_and_price_context_are_point_in_time() {
2346        let highs = [10.0, 12.0, 11.0, 14.0];
2347        let lows = [9.0, 10.0, 8.0, 11.0];
2348        let dm = directional_movement(&highs, &lows);
2349        assert_eq!(dm.plus_dm[0], None);
2350        assert_close(dm.plus_dm[1], 2.0);
2351        assert_close(dm.minus_dm[2], 2.0);
2352
2353        let closes = [10.0, 12.0, 9.0, 15.0, 14.0];
2354        let context = price_context(&closes);
2355        assert_close(context.close_vs_ath_pct[2], -25.0);
2356        assert_close(context.close_vs_atl_pct[3], 66.66666666666666);
2357        assert_close(context.days_since_ath[4], 1.0);
2358        assert_close(context.days_since_atl[4], 2.0);
2359    }
2360
2361    #[test]
2362    fn aroon_detects_recent_high_and_old_low() {
2363        // TA-Lib scans a `period + 1` bar window and emits the first value at
2364        // index `period`, so a period-4 Aroon needs 5 bars.
2365        let highs = [1.0, 2.0, 3.0, 4.0, 5.0];
2366        let lows = [1.0, 2.0, 3.0, 4.0, 5.0];
2367        let out = aroon(&highs, &lows, 4);
2368        assert_close(out.up[4], 100.0);
2369        assert_close(out.down[4], 0.0);
2370        assert_close(out.oscillator[4], 100.0);
2371    }
2372
2373    #[test]
2374    fn ad_line_accumulates_close_location_volume() {
2375        let high = [10.0, 10.0];
2376        let low = [0.0, 0.0];
2377        let close = [10.0, 0.0];
2378        let volume = [2.0, 3.0];
2379        let out = ad(&high, &low, &close, &volume);
2380        assert_close(out[0], 2.0);
2381        assert_close(out[1], -1.0);
2382    }
2383
2384    #[test]
2385    fn adx_family_exposes_di_dx_and_adx() {
2386        let highs = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0];
2387        let lows = [9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0];
2388        let closes = [9.5, 10.5, 11.5, 12.5, 13.5, 14.5, 15.5, 16.5, 17.5, 18.5];
2389        let out = adx_family(&highs, &lows, &closes, 3);
2390        assert!(out.plus_di.iter().any(Option::is_some));
2391        assert!(out.dx.iter().any(Option::is_some));
2392        assert!(out.adx.iter().any(Option::is_some));
2393        assert!(out.adxr.iter().any(Option::is_some));
2394    }
2395}