feat: init the repo
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "ferro_ta_core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Pure Rust core indicator library — no PyO3, no numpy dependency"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/pratikbhadane24/ferro-ta"
|
||||
homepage = "https://github.com/pratikbhadane24/ferro-ta#readme"
|
||||
documentation = "https://github.com/pratikbhadane24/ferro-ta#readme"
|
||||
keywords = ["technical-analysis", "trading", "indicators", "finance", "ta-lib"]
|
||||
categories = ["finance", "mathematics"]
|
||||
|
||||
[lib]
|
||||
name = "ferro_ta_core"
|
||||
crate-type = ["lib"]
|
||||
|
||||
[dependencies]
|
||||
wide = { version = "1.1.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
|
||||
[[bench]]
|
||||
name = "indicators"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
wide = ["dep:wide"]
|
||||
simd = ["wide"]
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Criterion benchmarks for ferro_ta_core — pure Rust indicator throughput.
|
||||
//!
|
||||
//! Run from repo root: cargo bench -p ferro_ta_core
|
||||
//! Or: cd crates/ferro_ta_core && cargo bench
|
||||
//!
|
||||
//! Input sizes: 1k, 10k, 100k, and 1M bars for key indicators.
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ferro_ta_core::{momentum, overlap, volatility};
|
||||
|
||||
fn synthetic_close(n: usize) -> Vec<f64> {
|
||||
let mut v = Vec::with_capacity(n);
|
||||
let mut price = 100.0_f64;
|
||||
for i in 0..n {
|
||||
price += ((i as f64 * 0.1).sin()) * 0.5;
|
||||
v.push(price);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn synthetic_high_low_close(n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
|
||||
let close = synthetic_close(n);
|
||||
let high: Vec<f64> = close.iter().map(|&c| c + 0.5).collect();
|
||||
let low: Vec<f64> = close.iter().map(|&c| c - 0.5).collect();
|
||||
(high, low, close)
|
||||
}
|
||||
|
||||
fn bench_sma(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("SMA");
|
||||
for size in [1_000_usize, 10_000, 100_000, 1_000_000] {
|
||||
let close = synthetic_close(size);
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &close, |b, close| {
|
||||
b.iter(|| overlap::sma(black_box(close), 14))
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_ema(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("EMA");
|
||||
for size in [1_000_usize, 10_000, 100_000, 1_000_000] {
|
||||
let close = synthetic_close(size);
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &close, |b, close| {
|
||||
b.iter(|| overlap::ema(black_box(close), 14))
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_rsi(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("RSI");
|
||||
for size in [1_000_usize, 10_000, 100_000, 1_000_000] {
|
||||
let close = synthetic_close(size);
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &close, |b, close| {
|
||||
b.iter(|| momentum::rsi(black_box(close), 14))
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_atr(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("ATR");
|
||||
for size in [1_000_usize, 10_000, 100_000, 1_000_000] {
|
||||
let (high, low, close) = synthetic_high_low_close(size);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(size),
|
||||
&(high.clone(), low.clone(), close),
|
||||
|b, (high, low, close)| {
|
||||
b.iter(|| volatility::atr(black_box(high), black_box(low), black_box(close), 14))
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_bbands(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("BBANDS");
|
||||
for size in [1_000_usize, 10_000, 100_000, 1_000_000] {
|
||||
let close = synthetic_close(size);
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &close, |b, close| {
|
||||
b.iter(|| overlap::bbands(black_box(close), 20, 2.0, 2.0))
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_sma,
|
||||
bench_ema,
|
||||
bench_rsi,
|
||||
bench_atr,
|
||||
bench_bbands
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,34 @@
|
||||
/*!
|
||||
ferro_ta_core — Pure Rust indicator library.
|
||||
|
||||
This crate contains all indicator implementations as pure functions operating
|
||||
on `&[f64]` slices and returning `Vec<f64>`. It has **no dependency on PyO3
|
||||
or numpy** so it can be used from any Rust project, or compiled to WASM /
|
||||
Node.js via napi-rs without dragging in Python bindings.
|
||||
|
||||
The Python wheel (`ferro_ta` PyPI package) is built from a thin binding crate
|
||||
that calls into this core and converts NumPy arrays to/from Rust slices.
|
||||
|
||||
# Two-layer architecture
|
||||
|
||||
The root crate (`ferro_ta`) contains PyO3 `#[pyfunction]` wrappers that convert
|
||||
numpy arrays to `&[f64]` and delegate to this core crate.
|
||||
|
||||
# Usage (Rust)
|
||||
|
||||
```rust
|
||||
use ferro_ta_core::overlap;
|
||||
|
||||
let close = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
|
||||
let sma = overlap::sma(&close, 3);
|
||||
assert!(sma[0].is_nan());
|
||||
assert!((sma[2] - 2.0).abs() < 1e-10);
|
||||
```
|
||||
*/
|
||||
|
||||
pub mod math;
|
||||
pub mod momentum;
|
||||
pub mod overlap;
|
||||
pub mod statistic;
|
||||
pub mod volatility;
|
||||
pub mod volume;
|
||||
@@ -0,0 +1,133 @@
|
||||
//! Math utilities.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Rolling sum over `timeperiod` bars.
|
||||
pub fn sum(real: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = real.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if timeperiod < 1 || n < timeperiod {
|
||||
return result;
|
||||
}
|
||||
let mut win: f64 = real[..timeperiod].iter().sum();
|
||||
result[timeperiod - 1] = win;
|
||||
for i in timeperiod..n {
|
||||
win += real[i] - real[i - timeperiod];
|
||||
result[i] = win;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Rolling maximum over `timeperiod` bars — O(n) via monotonic deque.
|
||||
pub fn max(real: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
sliding_max(real, timeperiod)
|
||||
}
|
||||
|
||||
/// Rolling minimum over `timeperiod` bars — O(n) via monotonic deque.
|
||||
pub fn min(real: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
sliding_min(real, timeperiod)
|
||||
}
|
||||
|
||||
/// Sliding maximum over `timeperiod` bars — O(n) via monotonic deque.
|
||||
///
|
||||
/// Equivalent to `max` but uses a monotonic deque for O(n) total time.
|
||||
/// Leading `timeperiod - 1` values are NaN.
|
||||
pub fn sliding_max(real: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = real.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if timeperiod < 1 || n < timeperiod {
|
||||
return result;
|
||||
}
|
||||
let mut dq: VecDeque<usize> = VecDeque::new();
|
||||
for i in 0..n {
|
||||
// Remove indices outside the window
|
||||
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
|
||||
dq.pop_front();
|
||||
}
|
||||
// Maintain decreasing deque
|
||||
while dq.back().map(|&j| real[j] <= real[i]).unwrap_or(false) {
|
||||
dq.pop_back();
|
||||
}
|
||||
dq.push_back(i);
|
||||
if i + 1 >= timeperiod {
|
||||
result[i] = real[*dq.front().unwrap()];
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Sliding minimum over `timeperiod` bars — O(n) via monotonic deque.
|
||||
///
|
||||
/// Equivalent to `min` but uses a monotonic deque for O(n) total time.
|
||||
/// Leading `timeperiod - 1` values are NaN.
|
||||
pub fn sliding_min(real: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = real.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if timeperiod < 1 || n < timeperiod {
|
||||
return result;
|
||||
}
|
||||
let mut dq: VecDeque<usize> = VecDeque::new();
|
||||
for i in 0..n {
|
||||
// Remove indices outside the window
|
||||
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
|
||||
dq.pop_front();
|
||||
}
|
||||
// Maintain increasing deque
|
||||
while dq.back().map(|&j| real[j] >= real[i]).unwrap_or(false) {
|
||||
dq.pop_back();
|
||||
}
|
||||
dq.push_back(i);
|
||||
if i + 1 >= timeperiod {
|
||||
result[i] = real[*dq.front().unwrap()];
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sum_basic() {
|
||||
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let r = sum(&v, 3);
|
||||
assert!(r[0].is_nan());
|
||||
assert!((r[2] - 6.0).abs() < 1e-10);
|
||||
assert!((r[4] - 12.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_basic() {
|
||||
let v = vec![3.0, 1.0, 4.0, 1.0, 5.0];
|
||||
let r = max(&v, 3);
|
||||
assert!((r[2] - 4.0).abs() < 1e-10);
|
||||
assert!((r[4] - 5.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sliding_max_matches_naive() {
|
||||
let v = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
|
||||
let naive = max(&v, 3);
|
||||
let fast = sliding_max(&v, 3);
|
||||
for i in 0..v.len() {
|
||||
assert_eq!(naive[i].is_nan(), fast[i].is_nan());
|
||||
if !naive[i].is_nan() {
|
||||
assert!((naive[i] - fast[i]).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sliding_min_matches_naive() {
|
||||
let v = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
|
||||
let naive = min(&v, 3);
|
||||
let fast = sliding_min(&v, 3);
|
||||
for i in 0..v.len() {
|
||||
assert_eq!(naive[i].is_nan(), fast[i].is_nan());
|
||||
if !naive[i].is_nan() {
|
||||
assert!((naive[i] - fast[i]).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
//! Momentum indicators.
|
||||
|
||||
use crate::math::{sliding_max, sliding_min};
|
||||
|
||||
/// Relative Strength Index — TA-Lib compatible Wilder smoothing.
|
||||
///
|
||||
/// Seeds avg_gain/avg_loss with SMA of first `timeperiod` changes.
|
||||
/// Uses branchless gain/loss split: `gain = diff.max(0.0)`, `loss = (-diff).max(0.0)`.
|
||||
pub fn rsi(close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = close.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if n <= timeperiod || timeperiod < 1 {
|
||||
return result;
|
||||
}
|
||||
let mut avg_gain = 0.0_f64;
|
||||
let mut avg_loss = 0.0_f64;
|
||||
for i in 1..=timeperiod {
|
||||
let diff = close[i] - close[i - 1];
|
||||
let abs_diff = diff.abs();
|
||||
avg_gain += (diff + abs_diff) * 0.5;
|
||||
avg_loss += (abs_diff - diff) * 0.5;
|
||||
}
|
||||
avg_gain /= timeperiod as f64;
|
||||
avg_loss /= timeperiod as f64;
|
||||
let p = timeperiod as f64;
|
||||
let rs = if avg_loss == 0.0 {
|
||||
f64::MAX
|
||||
} else {
|
||||
avg_gain / avg_loss
|
||||
};
|
||||
result[timeperiod] = 100.0 - 100.0 / (1.0 + rs);
|
||||
for i in (timeperiod + 1)..n {
|
||||
let diff = close[i] - close[i - 1];
|
||||
let abs_diff = diff.abs();
|
||||
let gain = (diff + abs_diff) * 0.5;
|
||||
let loss = (abs_diff - diff) * 0.5;
|
||||
avg_gain = (avg_gain * (p - 1.0) + gain) / p;
|
||||
avg_loss = (avg_loss * (p - 1.0) + loss) / p;
|
||||
let rs = if avg_loss == 0.0 {
|
||||
f64::MAX
|
||||
} else {
|
||||
avg_gain / avg_loss
|
||||
};
|
||||
result[i] = 100.0 - 100.0 / (1.0 + rs);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Momentum — `close[i] - close[i - timeperiod]`.
|
||||
pub fn mom(close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = close.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if timeperiod < 1 {
|
||||
return result;
|
||||
}
|
||||
for i in timeperiod..n {
|
||||
result[i] = close[i] - close[i - timeperiod];
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Stochastic Oscillator — TA-Lib compatible.
|
||||
///
|
||||
/// Returns `(slowk, slowd)`.
|
||||
/// - Fast %K[i] = 100 * (close[i] - min(low, fastk_period)) / (max(high, fastk_period) - min(low, fastk_period))
|
||||
/// - Slow %K = SMA(fast %K, slowk_period)
|
||||
/// - Slow %D = SMA(slow %K, slowd_period)
|
||||
///
|
||||
/// Uses O(n) sliding max/min via monotonic deques.
|
||||
pub fn stoch(
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
fastk_period: usize,
|
||||
slowk_period: usize,
|
||||
slowd_period: usize,
|
||||
) -> (Vec<f64>, Vec<f64>) {
|
||||
let n = high.len();
|
||||
let nan_pair = || (vec![f64::NAN; n], vec![f64::NAN; n]);
|
||||
if n == 0 || fastk_period < 1 || slowk_period < 1 || slowd_period < 1 {
|
||||
return nan_pair();
|
||||
}
|
||||
if n < fastk_period {
|
||||
return nan_pair();
|
||||
}
|
||||
|
||||
let max_h = sliding_max(high, fastk_period);
|
||||
let min_l = sliding_min(low, fastk_period);
|
||||
|
||||
let mut slowk = vec![f64::NAN; n];
|
||||
let mut slowd = vec![f64::NAN; n];
|
||||
|
||||
// Fast %K is valid from index fastk_period-1 onward.
|
||||
let fastk_start = fastk_period - 1;
|
||||
let mut fastk_valid = vec![0.0; n - fastk_start];
|
||||
for i in fastk_start..n {
|
||||
let range = max_h[i] - min_l[i];
|
||||
fastk_valid[i - fastk_start] = if range != 0.0 {
|
||||
100.0 * (close[i] - min_l[i]) / range
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
|
||||
// Slow %K = SMA(fastk_valid, slowk_period); write directly into `slowk` offset by `fastk_start`.
|
||||
crate::overlap::sma_into(&fastk_valid, slowk_period, &mut slowk, fastk_start);
|
||||
|
||||
// Slow %D = SMA(slowk, slowd_period).
|
||||
// The valid part of slowk starts at `fastk_start + slowk_period - 1`.
|
||||
let slowk_valid_start = fastk_start + slowk_period - 1;
|
||||
let slowd_valid_start = slowk_valid_start + slowd_period - 1;
|
||||
|
||||
if slowk_valid_start < n {
|
||||
let slowk_valid_slice = &slowk[slowk_valid_start..];
|
||||
crate::overlap::sma_into(
|
||||
slowk_valid_slice,
|
||||
slowd_period,
|
||||
&mut slowd,
|
||||
slowk_valid_start,
|
||||
);
|
||||
}
|
||||
|
||||
// TA-Lib pads BOTH slowk and slowd with NaNs up to the point where both are valid.
|
||||
if slowd_valid_start < n {
|
||||
for v in slowk.iter_mut().take(slowd_valid_start) {
|
||||
*v = f64::NAN;
|
||||
}
|
||||
} else {
|
||||
for v in slowk.iter_mut().take(n) {
|
||||
*v = f64::NAN;
|
||||
}
|
||||
}
|
||||
|
||||
(slowk, slowd)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ADX family
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Return type for ADX inner (pdm_s, mdm_s, plus_di, minus_di, dx, adx).
|
||||
type AdxInnerOutput = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>);
|
||||
|
||||
/// Fused inner function for ADX-family indicators.
|
||||
/// Returns a tuple of (pdm_s, mdm_s, plus_di, minus_di, dx, adx).
|
||||
fn adx_inner(high: &[f64], low: &[f64], close: &[f64], period: usize) -> AdxInnerOutput {
|
||||
let n = high.len();
|
||||
let mut b_pdm = vec![f64::NAN; n];
|
||||
let mut b_mdm = vec![f64::NAN; n];
|
||||
let mut b_pdi = vec![f64::NAN; n];
|
||||
let mut b_mdi = vec![f64::NAN; n];
|
||||
let mut b_dx = vec![f64::NAN; n];
|
||||
let mut b_adx = vec![f64::NAN; n];
|
||||
|
||||
if n < period || period < 1 || n < 2 {
|
||||
return (b_pdm, b_mdm, b_pdi, b_mdi, b_dx, b_adx);
|
||||
}
|
||||
|
||||
let m = n - 1;
|
||||
let mut tr = vec![0.0_f64; m];
|
||||
let mut pdm = vec![0.0_f64; m];
|
||||
let mut mdm = vec![0.0_f64; m];
|
||||
|
||||
for i in 0..m {
|
||||
let j = i + 1;
|
||||
let h_diff = high[j] - high[i];
|
||||
let l_diff = low[i] - low[j];
|
||||
let hl = high[j] - low[j];
|
||||
let hpc = (high[j] - close[i]).abs();
|
||||
let lpc = (low[j] - close[i]).abs();
|
||||
tr[i] = hl.max(hpc).max(lpc);
|
||||
pdm[i] = if h_diff > l_diff && h_diff > 0.0 {
|
||||
h_diff
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
mdm[i] = if l_diff > h_diff && l_diff > 0.0 {
|
||||
l_diff
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
|
||||
if m < period {
|
||||
return (b_pdm, b_mdm, b_pdi, b_mdi, b_dx, b_adx);
|
||||
}
|
||||
|
||||
let mut tr_s = tr[..period].iter().sum::<f64>();
|
||||
let mut pdm_s = pdm[..period].iter().sum::<f64>();
|
||||
let mut mdm_s = mdm[..period].iter().sum::<f64>();
|
||||
|
||||
// Initial seeded values at index `period`
|
||||
b_pdm[period] = pdm_s;
|
||||
b_mdm[period] = mdm_s;
|
||||
if tr_s != 0.0 {
|
||||
b_pdi[period] = 100.0 * pdm_s / tr_s;
|
||||
b_mdi[period] = 100.0 * mdm_s / tr_s;
|
||||
let s = b_pdi[period] + b_mdi[period];
|
||||
b_dx[period] = if s != 0.0 {
|
||||
100.0 * (b_pdi[period] - b_mdi[period]).abs() / s
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
|
||||
let decay = (period - 1) as f64 / period as f64;
|
||||
for i in period..m {
|
||||
tr_s = tr_s * decay + tr[i];
|
||||
pdm_s = pdm_s * decay + pdm[i];
|
||||
mdm_s = mdm_s * decay + mdm[i];
|
||||
|
||||
b_pdm[i + 1] = pdm_s;
|
||||
b_mdm[i + 1] = mdm_s;
|
||||
if tr_s != 0.0 {
|
||||
b_pdi[i + 1] = 100.0 * pdm_s / tr_s;
|
||||
b_mdi[i + 1] = 100.0 * mdm_s / tr_s;
|
||||
let s = b_pdi[i + 1] + b_mdi[i + 1];
|
||||
b_dx[i + 1] = if s != 0.0 {
|
||||
100.0 * (b_pdi[i + 1] - b_mdi[i + 1]).abs() / s
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Wilder smooth DX to get ADX
|
||||
let adx_start = period + period - 1;
|
||||
if n > adx_start {
|
||||
let mut dx_sum = 0.0;
|
||||
let mut valid_dx = true;
|
||||
for v in b_dx.iter().skip(period).take(period) {
|
||||
if v.is_nan() {
|
||||
valid_dx = false;
|
||||
break;
|
||||
}
|
||||
dx_sum += v;
|
||||
}
|
||||
if valid_dx {
|
||||
let mut adx_s = dx_sum / period as f64;
|
||||
b_adx[adx_start] = adx_s;
|
||||
let alpha = 1.0 / period as f64;
|
||||
for i in adx_start + 1..n {
|
||||
adx_s = adx_s + alpha * (b_dx[i] - adx_s);
|
||||
b_adx[i] = adx_s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(b_pdm, b_mdm, b_pdi, b_mdi, b_dx, b_adx)
|
||||
}
|
||||
|
||||
/// Plus Directional Movement (Wilder smoothed). Output length = n (bar 0 is NaN).
|
||||
pub fn plus_dm(high: &[f64], low: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = high.len();
|
||||
let closes = vec![0.0_f64; n];
|
||||
let (pdm, _, _, _, _, _) = adx_inner(high, low, &closes, timeperiod);
|
||||
pdm
|
||||
}
|
||||
|
||||
/// Minus Directional Movement (Wilder smoothed). Output length = n (bar 0 is NaN).
|
||||
pub fn minus_dm(high: &[f64], low: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = high.len();
|
||||
let closes = vec![0.0_f64; n];
|
||||
let (_, mdm, _, _, _, _) = adx_inner(high, low, &closes, timeperiod);
|
||||
mdm
|
||||
}
|
||||
|
||||
/// Plus Directional Indicator (Wilder smoothed). Output length = n.
|
||||
pub fn plus_di(high: &[f64], low: &[f64], close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let (_, _, pdi, _, _, _) = adx_inner(high, low, close, timeperiod);
|
||||
pdi
|
||||
}
|
||||
|
||||
/// Minus Directional Indicator (Wilder smoothed). Output length = n.
|
||||
pub fn minus_di(high: &[f64], low: &[f64], close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let (_, _, _, mdi, _, _) = adx_inner(high, low, close, timeperiod);
|
||||
mdi
|
||||
}
|
||||
|
||||
/// Directional Movement Index: 100 * |+DI − −DI| / (+DI + −DI).
|
||||
pub fn dx(high: &[f64], low: &[f64], close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let (_, _, _, _, dx_vals, _) = adx_inner(high, low, close, timeperiod);
|
||||
dx_vals
|
||||
}
|
||||
|
||||
/// Average Directional Movement Index (Wilder smoothing of DX).
|
||||
pub fn adx(high: &[f64], low: &[f64], close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let (_, _, _, _, _, adx_vals) = adx_inner(high, low, close, timeperiod);
|
||||
adx_vals
|
||||
}
|
||||
|
||||
/// ADX Rating: (ADX[i] + ADX[i − timeperiod]) / 2.
|
||||
pub fn adxr(high: &[f64], low: &[f64], close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = high.len();
|
||||
let adx_vals = adx(high, low, close, timeperiod);
|
||||
let mut result = vec![f64::NAN; n];
|
||||
for i in timeperiod..n {
|
||||
if !adx_vals[i].is_nan() && !adx_vals[i - timeperiod].is_nan() {
|
||||
result[i] = (adx_vals[i] + adx_vals[i - timeperiod]) / 2.0;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rsi_range() {
|
||||
let prices: Vec<f64> = (1..=50).map(|i| i as f64).collect();
|
||||
let result = rsi(&prices, 14);
|
||||
for v in result.iter().filter(|v| !v.is_nan()) {
|
||||
assert!(*v >= 0.0 && *v <= 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mom_basic() {
|
||||
let prices = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let result = mom(&prices, 2);
|
||||
assert!(result[0].is_nan());
|
||||
assert!(result[1].is_nan());
|
||||
assert!((result[2] - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stoch_basic() {
|
||||
let high = vec![10.0, 11.0, 12.0, 11.5, 13.0, 12.5, 14.0, 13.5];
|
||||
let low = vec![9.0, 10.0, 11.0, 10.5, 12.0, 11.5, 13.0, 12.5];
|
||||
let close = vec![9.5, 10.5, 11.5, 11.0, 12.5, 12.0, 13.5, 13.0];
|
||||
let (slowk, slowd) = stoch(&high, &low, &close, 3, 3, 3);
|
||||
// Check that valid values are in [0, 100]
|
||||
for v in slowk.iter().filter(|v| !v.is_nan()) {
|
||||
assert!(*v >= 0.0 && *v <= 100.0, "slowk out of range: {v}");
|
||||
}
|
||||
for v in slowd.iter().filter(|v| !v.is_nan()) {
|
||||
assert!(*v >= 0.0 && *v <= 100.0, "slowd out of range: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adx_nonnegative() {
|
||||
let h: Vec<f64> = (1..=50).map(|i| i as f64 + 1.0).collect();
|
||||
let l: Vec<f64> = (1..=50).map(|i| i as f64).collect();
|
||||
let c: Vec<f64> = (1..=50).map(|i| i as f64 + 0.5).collect();
|
||||
let result = adx(&h, &l, &c, 14);
|
||||
for v in result.iter().filter(|v| !v.is_nan()) {
|
||||
assert!(*v >= 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
//! Overlap studies — moving averages and trend indicators.
|
||||
//!
|
||||
//! All functions return a `Vec<f64>` of the same length as the input.
|
||||
//! Leading values are `f64::NAN` for the warm-up period.
|
||||
|
||||
/// Simple Moving Average over `timeperiod` bars.
|
||||
///
|
||||
/// # Edge Cases
|
||||
/// Returns all-NaN when `timeperiod < 1` or `close.len() < timeperiod`.
|
||||
pub fn sma(close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = close.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
sma_into(close, timeperiod, &mut result, 0);
|
||||
result
|
||||
}
|
||||
|
||||
/// Simple Moving Average written directly into `dest` starting at `dest_offset`.
|
||||
/// Leaves values before `dest_offset + timeperiod - 1` untouched (e.g. they can be NaN).
|
||||
pub fn sma_into(src: &[f64], timeperiod: usize, dest: &mut [f64], dest_offset: usize) {
|
||||
let n = src.len();
|
||||
if timeperiod < 1 || n < timeperiod {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "simd")]
|
||||
let window_sum_init = {
|
||||
use wide::f64x4;
|
||||
let p_data = &src[..timeperiod];
|
||||
let mut sum = f64x4::splat(0.0);
|
||||
let mut chunks = p_data.chunks_exact(4);
|
||||
for chunk in &mut chunks {
|
||||
sum += f64x4::new([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
}
|
||||
let arr = sum.to_array();
|
||||
let mut total = arr[0] + arr[1] + arr[2] + arr[3];
|
||||
for &v in chunks.remainder() {
|
||||
total += v;
|
||||
}
|
||||
total
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
let window_sum_init: f64 = src[..timeperiod].iter().sum();
|
||||
|
||||
let mut window_sum = window_sum_init;
|
||||
let tp_f64 = timeperiod as f64;
|
||||
dest[dest_offset + timeperiod - 1] = window_sum / tp_f64;
|
||||
|
||||
let mut i = timeperiod;
|
||||
while i + 1 < n {
|
||||
let old0 = src[i - timeperiod];
|
||||
let new0 = src[i];
|
||||
window_sum += new0 - old0;
|
||||
dest[dest_offset + i] = window_sum / tp_f64;
|
||||
|
||||
let old1 = src[i + 1 - timeperiod];
|
||||
let new1 = src[i + 1];
|
||||
window_sum += new1 - old1;
|
||||
dest[dest_offset + i + 1] = window_sum / tp_f64;
|
||||
|
||||
i += 2;
|
||||
}
|
||||
if i < n {
|
||||
window_sum += src[i] - src[i - timeperiod];
|
||||
dest[dest_offset + i] = window_sum / tp_f64;
|
||||
}
|
||||
}
|
||||
|
||||
/// Exponential Moving Average — seeded with SMA of first `timeperiod` bars.
|
||||
pub fn ema(close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = close.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if timeperiod < 1 || n < timeperiod {
|
||||
return result;
|
||||
}
|
||||
let k = 2.0 / (timeperiod as f64 + 1.0);
|
||||
let seed: f64 = close[..timeperiod].iter().sum::<f64>() / timeperiod as f64;
|
||||
result[timeperiod - 1] = seed;
|
||||
for i in timeperiod..n {
|
||||
result[i] = (result[i - 1] * (1.0 - k)).mul_add(1.0, close[i] * k);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Weighted Moving Average — O(n) incremental algorithm using running weighted sum.
|
||||
///
|
||||
/// Recurrence: `T[i] = T[i-1] + n*close[i] - S[i-1]`
|
||||
/// where `S[i]` is the rolling sum over `timeperiod` bars.
|
||||
pub fn wma(close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = close.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if timeperiod < 1 || n < timeperiod {
|
||||
return result;
|
||||
}
|
||||
let denom: f64 = (timeperiod * (timeperiod + 1) / 2) as f64;
|
||||
let p = timeperiod as f64;
|
||||
|
||||
// Seed: compute T and S for the first window.
|
||||
#[cfg(feature = "simd")]
|
||||
let (mut t, mut s) = {
|
||||
use wide::f64x4;
|
||||
let p_data = &close[..timeperiod];
|
||||
let mut t_simd = f64x4::splat(0.0);
|
||||
let mut s_simd = f64x4::splat(0.0);
|
||||
let mut chunks = p_data.chunks_exact(4);
|
||||
let mut idx = 1.0;
|
||||
let step = f64x4::new([0.0, 1.0, 2.0, 3.0]);
|
||||
|
||||
for chunk in &mut chunks {
|
||||
let vals = f64x4::new([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
let mults = f64x4::splat(idx) + step;
|
||||
t_simd += vals * mults;
|
||||
s_simd += vals;
|
||||
idx += 4.0;
|
||||
}
|
||||
let t_arr = t_simd.to_array();
|
||||
let s_arr = s_simd.to_array();
|
||||
let mut t = t_arr[0] + t_arr[1] + t_arr[2] + t_arr[3];
|
||||
let mut s = s_arr[0] + s_arr[1] + s_arr[2] + s_arr[3];
|
||||
for &v in chunks.remainder() {
|
||||
t += v * idx;
|
||||
s += v;
|
||||
idx += 1.0;
|
||||
}
|
||||
(t, s)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
let (mut t, mut s) = {
|
||||
let t_val: f64 = close[..timeperiod]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(k, &v)| v * (k + 1) as f64)
|
||||
.sum();
|
||||
let s_val: f64 = close[..timeperiod].iter().sum();
|
||||
(t_val, s_val)
|
||||
};
|
||||
|
||||
result[timeperiod - 1] = t / denom;
|
||||
|
||||
let mut i = timeperiod;
|
||||
while i + 1 < n {
|
||||
t += p * close[i] - s;
|
||||
s += close[i] - close[i - timeperiod];
|
||||
result[i] = t / denom;
|
||||
|
||||
t += p * close[i + 1] - s;
|
||||
s += close[i + 1] - close[i + 1 - timeperiod];
|
||||
result[i + 1] = t / denom;
|
||||
|
||||
i += 2;
|
||||
}
|
||||
if i < n {
|
||||
t += p * close[i] - s;
|
||||
result[i] = t / denom;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Bollinger Bands — returns `(upper, middle, lower)`.
|
||||
///
|
||||
/// Middle is SMA; bands are `± nbdev * stddev`.
|
||||
/// Uses O(n) sliding `sum` and `sum_sq` windows for mean and variance.
|
||||
pub fn bbands(
|
||||
close: &[f64],
|
||||
timeperiod: usize,
|
||||
nbdevup: f64,
|
||||
nbdevdn: f64,
|
||||
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
|
||||
let n = close.len();
|
||||
let nan = vec![f64::NAN; n];
|
||||
if timeperiod < 1 || n < timeperiod {
|
||||
return (nan.clone(), nan.clone(), nan);
|
||||
}
|
||||
let mut upper = vec![f64::NAN; n];
|
||||
let mut middle = vec![f64::NAN; n];
|
||||
let mut lower = vec![f64::NAN; n];
|
||||
let p = timeperiod as f64;
|
||||
|
||||
// Seed sliding sums for the first window.
|
||||
#[cfg(feature = "simd")]
|
||||
let (mut sum, mut sum_sq) = {
|
||||
use wide::f64x4;
|
||||
let p_data = &close[..timeperiod];
|
||||
let mut sum_simd = f64x4::splat(0.0);
|
||||
let mut sq_simd = f64x4::splat(0.0);
|
||||
let mut chunks = p_data.chunks_exact(4);
|
||||
for chunk in &mut chunks {
|
||||
let vals = f64x4::new([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
sum_simd += vals;
|
||||
sq_simd += vals * vals;
|
||||
}
|
||||
let s_arr = sum_simd.to_array();
|
||||
let sq_arr = sq_simd.to_array();
|
||||
let mut sum = s_arr[0] + s_arr[1] + s_arr[2] + s_arr[3];
|
||||
let mut sum_sq = sq_arr[0] + sq_arr[1] + sq_arr[2] + sq_arr[3];
|
||||
for &v in chunks.remainder() {
|
||||
sum += v;
|
||||
sum_sq += v * v;
|
||||
}
|
||||
(sum, sum_sq)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
let (mut sum, mut sum_sq) = {
|
||||
let s: f64 = close[..timeperiod].iter().sum();
|
||||
let sq: f64 = close[..timeperiod].iter().map(|&x| x * x).sum();
|
||||
(s, sq)
|
||||
};
|
||||
|
||||
let mean = sum / p;
|
||||
let var = (sum_sq / p - mean * mean).max(0.0);
|
||||
let std = var.sqrt();
|
||||
middle[timeperiod - 1] = mean;
|
||||
upper[timeperiod - 1] = mean + nbdevup * std;
|
||||
lower[timeperiod - 1] = mean - nbdevdn * std;
|
||||
|
||||
let mut i = timeperiod;
|
||||
while i + 1 < n {
|
||||
let old0 = close[i - timeperiod];
|
||||
sum += close[i] - old0;
|
||||
sum_sq += close[i] * close[i] - old0 * old0;
|
||||
let mean = sum / p;
|
||||
let var = (sum_sq / p - mean * mean).max(0.0);
|
||||
let std = var.sqrt();
|
||||
middle[i] = mean;
|
||||
upper[i] = mean + nbdevup * std;
|
||||
lower[i] = mean - nbdevdn * std;
|
||||
|
||||
let old1 = close[i + 1 - timeperiod];
|
||||
sum += close[i + 1] - old1;
|
||||
sum_sq += close[i + 1] * close[i + 1] - old1 * old1;
|
||||
let mean1 = sum / p;
|
||||
let var1 = (sum_sq / p - mean1 * mean1).max(0.0);
|
||||
let std1 = var1.sqrt();
|
||||
middle[i + 1] = mean1;
|
||||
upper[i + 1] = mean1 + nbdevup * std1;
|
||||
lower[i + 1] = mean1 - nbdevdn * std1;
|
||||
|
||||
i += 2;
|
||||
}
|
||||
if i < n {
|
||||
let old = close[i - timeperiod];
|
||||
sum += close[i] - old;
|
||||
sum_sq += close[i] * close[i] - old * old;
|
||||
let mean = sum / p;
|
||||
let var = (sum_sq / p - mean * mean).max(0.0);
|
||||
let std = var.sqrt();
|
||||
middle[i] = mean;
|
||||
upper[i] = mean + nbdevup * std;
|
||||
lower[i] = mean - nbdevdn * std;
|
||||
}
|
||||
(upper, middle, lower)
|
||||
}
|
||||
|
||||
/// MACD — EMA(fastperiod) minus EMA(slowperiod), signal = EMA(macd, signalperiod).
|
||||
///
|
||||
/// Returns `(macd_line, signal_line, histogram)`, each of length `n`.
|
||||
/// Leading values are `NaN` during warmup.
|
||||
/// `fastperiod` must be less than `slowperiod`.
|
||||
///
|
||||
/// Fast and slow EMAs are computed in a **single combined loop** to minimise
|
||||
/// memory round-trips, then the signal EMA is computed in a second pass.
|
||||
pub fn macd(
|
||||
close: &[f64],
|
||||
fastperiod: usize,
|
||||
slowperiod: usize,
|
||||
signalperiod: usize,
|
||||
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
|
||||
let n = close.len();
|
||||
let nan_vec = || vec![f64::NAN; n];
|
||||
if fastperiod < 1 || slowperiod < 1 || signalperiod < 1 || fastperiod >= slowperiod {
|
||||
return (nan_vec(), nan_vec(), nan_vec());
|
||||
}
|
||||
if n < slowperiod {
|
||||
return (nan_vec(), nan_vec(), nan_vec());
|
||||
}
|
||||
|
||||
let kf = 2.0 / (fastperiod as f64 + 1.0);
|
||||
let ks = 2.0 / (slowperiod as f64 + 1.0);
|
||||
|
||||
// Seed fast EMA from SMA of first fastperiod bars.
|
||||
let mut fast_val: f64 = close[..fastperiod].iter().sum::<f64>() / fastperiod as f64;
|
||||
// Seed slow EMA from SMA of first slowperiod bars.
|
||||
let mut slow_val: f64 = close[..slowperiod].iter().sum::<f64>() / slowperiod as f64;
|
||||
|
||||
let mut macd_line = nan_vec();
|
||||
|
||||
// From fastperiod-1 to slowperiod-2: advance fast EMA only.
|
||||
for &price in close.iter().take(slowperiod - 1).skip(fastperiod) {
|
||||
fast_val = price * kf + fast_val * (1.0 - kf);
|
||||
}
|
||||
|
||||
// From fastperiod to slowperiod-1: advance fastEMA and compute initial MACD at slowperiod-1
|
||||
// Actually, fast_val currently holds the value for `slowperiod - 2` after `take(slowperiod - 1)`
|
||||
// So we apply it for `slowperiod - 1`.
|
||||
fast_val = close[slowperiod - 1] * kf + fast_val * (1.0 - kf);
|
||||
macd_line[slowperiod - 1] = fast_val - slow_val;
|
||||
for i in slowperiod..n {
|
||||
fast_val = close[i] * kf + fast_val * (1.0 - kf);
|
||||
slow_val = close[i] * ks + slow_val * (1.0 - ks);
|
||||
macd_line[i] = fast_val - slow_val;
|
||||
}
|
||||
|
||||
// Signal line: EMA of macd_line, seeded from the first valid macd value.
|
||||
// The signal line starts producing values after slowperiod - 1 + signalperiod - 1 bars.
|
||||
let sig_start = slowperiod - 1 + signalperiod - 1;
|
||||
let mut signal_line = nan_vec();
|
||||
let mut histogram = nan_vec();
|
||||
|
||||
if sig_start >= n {
|
||||
// If we can't compute signal, TA-Lib clears MACD!
|
||||
for v in macd_line.iter_mut().take(n) {
|
||||
*v = f64::NAN;
|
||||
}
|
||||
return (macd_line, signal_line, histogram);
|
||||
}
|
||||
|
||||
let ksig = 2.0 / (signalperiod as f64 + 1.0);
|
||||
// Seed signal EMA with SMA of the first signalperiod macd values.
|
||||
let sig_seed: f64 = macd_line[(slowperiod - 1)..(slowperiod - 1 + signalperiod)]
|
||||
.iter()
|
||||
.sum::<f64>()
|
||||
/ signalperiod as f64;
|
||||
signal_line[sig_start] = sig_seed;
|
||||
histogram[sig_start] = macd_line[sig_start] - signal_line[sig_start];
|
||||
|
||||
for i in (sig_start + 1)..n {
|
||||
signal_line[i] = macd_line[i] * ksig + signal_line[i - 1] * (1.0 - ksig);
|
||||
}
|
||||
for i in (sig_start + 1)..n {
|
||||
histogram[i] = macd_line[i] - signal_line[i];
|
||||
}
|
||||
|
||||
// TA-Lib pads the MACD line itself with NaNs up to `sig_start`!
|
||||
for v in macd_line.iter_mut().take(sig_start) {
|
||||
*v = f64::NAN;
|
||||
}
|
||||
|
||||
(macd_line, signal_line, histogram)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sma_basic() {
|
||||
let prices = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let result = sma(&prices, 3);
|
||||
assert!(result[0].is_nan());
|
||||
assert!(result[1].is_nan());
|
||||
assert!((result[2] - 2.0).abs() < 1e-10);
|
||||
assert!((result[3] - 3.0).abs() < 1e-10);
|
||||
assert!((result[4] - 4.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ema_basic() {
|
||||
let prices = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let result = ema(&prices, 3);
|
||||
assert!(result[0].is_nan());
|
||||
assert!(result[1].is_nan());
|
||||
assert!((result[2] - 2.0).abs() < 1e-10); // seed = SMA(3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wma_basic() {
|
||||
let prices = vec![1.0, 2.0, 3.0];
|
||||
let result = wma(&prices, 3);
|
||||
assert!(result[0].is_nan());
|
||||
assert!(result[1].is_nan());
|
||||
// weights: 1, 2, 3; denom 6 => (1*1 + 2*2 + 3*3)/6 = 14/6
|
||||
assert!((result[2] - 14.0 / 6.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bbands_basic() {
|
||||
let prices = vec![2.0, 2.0, 2.0, 2.0, 2.0];
|
||||
let (upper, middle, lower) = bbands(&prices, 3, 2.0, 2.0);
|
||||
assert!((middle[2] - 2.0).abs() < 1e-10);
|
||||
assert!((upper[2] - 2.0).abs() < 1e-10); // std = 0
|
||||
assert!((lower[2] - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macd_basic() {
|
||||
// 40 bars of linearly increasing prices — MACD line should converge
|
||||
let prices: Vec<f64> = (1..=40).map(|i| i as f64).collect();
|
||||
let (macd_line, signal_line, histogram) = macd(&prices, 3, 5, 2);
|
||||
// TA-Lib pads MACD line with NaN up to sig_start = slowperiod-1 + signalperiod-1 = 5
|
||||
for i in 0..5 {
|
||||
assert!(macd_line[i].is_nan(), "expected NaN at {i}");
|
||||
}
|
||||
// First valid macd bar is at index 5 (sig_start)
|
||||
assert!(!macd_line[5].is_nan());
|
||||
// First valid signal bar is at index 5
|
||||
assert!(!signal_line[5].is_nan());
|
||||
// histogram = macd - signal
|
||||
assert!((histogram[5] - (macd_line[5] - signal_line[5])).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macd_invalid_params() {
|
||||
let prices = vec![1.0; 50];
|
||||
// fastperiod >= slowperiod should return all-NaN
|
||||
let (m, s, h) = macd(&prices, 5, 3, 9);
|
||||
assert!(m.iter().all(|v| v.is_nan()));
|
||||
assert!(s.iter().all(|v| v.is_nan()));
|
||||
assert!(h.iter().all(|v| v.is_nan()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Statistic functions.
|
||||
|
||||
/// Standard deviation — population (`ddof = 0`).
|
||||
pub fn stddev(real: &[f64], timeperiod: usize, nbdev: f64) -> Vec<f64> {
|
||||
let n = real.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if timeperiod < 1 || n < timeperiod {
|
||||
return result;
|
||||
}
|
||||
for i in (timeperiod - 1)..n {
|
||||
let window = &real[i + 1 - timeperiod..=i];
|
||||
let mean: f64 = window.iter().sum::<f64>() / timeperiod as f64;
|
||||
let var: f64 = window.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / timeperiod as f64;
|
||||
result[i] = var.sqrt() * nbdev;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn stddev_constant() {
|
||||
let prices = vec![5.0; 5];
|
||||
let result = stddev(&prices, 3, 1.0);
|
||||
for v in result.iter().filter(|v| !v.is_nan()) {
|
||||
assert!(v.abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Volatility indicators.
|
||||
|
||||
/// Average True Range — Wilder smoothed (TA-Lib compatible).
|
||||
///
|
||||
/// Seeds ATR with SMA of TR[1..=timeperiod] (bar 0 is skipped, matching TA-Lib).
|
||||
/// First valid output is at index `timeperiod`; indices 0..timeperiod are NaN.
|
||||
/// TR is computed on-the-fly (no separate tr Vec allocation).
|
||||
pub fn atr(high: &[f64], low: &[f64], close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = high.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if n <= timeperiod || timeperiod < 1 {
|
||||
return result;
|
||||
}
|
||||
// Seed: SMA of TR[1..=timeperiod] (TA-Lib skips TR[0]).
|
||||
// Compute TR on-the-fly to avoid a separate Vec allocation.
|
||||
let mut seed = 0.0_f64;
|
||||
for i in 1..=timeperiod {
|
||||
let hl = high[i] - low[i];
|
||||
let hpc = (high[i] - close[i - 1]).abs();
|
||||
let lpc = (low[i] - close[i - 1]).abs();
|
||||
seed += hl.max(hpc).max(lpc);
|
||||
}
|
||||
seed /= timeperiod as f64;
|
||||
result[timeperiod] = seed;
|
||||
let p = timeperiod as f64;
|
||||
for i in (timeperiod + 1)..n {
|
||||
let hl = high[i] - low[i];
|
||||
let hpc = (high[i] - close[i - 1]).abs();
|
||||
let lpc = (low[i] - close[i - 1]).abs();
|
||||
let tr = hl.max(hpc).max(lpc);
|
||||
result[i] = (result[i - 1] * (p - 1.0) + tr) / p;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// True Range — max(H-L, |H-Cprev|, |L-Cprev|).
|
||||
pub fn trange(high: &[f64], low: &[f64], close: &[f64]) -> Vec<f64> {
|
||||
let n = high.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if n == 0 {
|
||||
return result;
|
||||
}
|
||||
result[0] = high[0] - low[0];
|
||||
for i in 1..n {
|
||||
let hl = high[i] - low[i];
|
||||
let hpc = (high[i] - close[i - 1]).abs();
|
||||
let lpc = (low[i] - close[i - 1]).abs();
|
||||
result[i] = hl.max(hpc).max(lpc);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn atr_nonnegative() {
|
||||
let h = vec![2.0, 3.0, 4.0, 5.0, 6.0];
|
||||
let l = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let c = vec![1.5, 2.5, 3.5, 4.5, 5.5];
|
||||
let result = atr(&h, &l, &c, 3);
|
||||
for v in result.iter().filter(|v| !v.is_nan()) {
|
||||
assert!(*v >= 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
//! Volume indicators.
|
||||
|
||||
/// On-Balance Volume.
|
||||
pub fn obv(close: &[f64], volume: &[f64]) -> Vec<f64> {
|
||||
let n = close.len();
|
||||
let mut result = vec![0.0_f64; n];
|
||||
if n == 0 {
|
||||
return result;
|
||||
}
|
||||
result[0] = volume[0];
|
||||
for i in 1..n {
|
||||
result[i] = result[i - 1]
|
||||
+ if close[i] > close[i - 1] {
|
||||
volume[i]
|
||||
} else if close[i] < close[i - 1] {
|
||||
-volume[i]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Money Flow Index — O(n) sliding-window implementation without per-bar allocation.
|
||||
///
|
||||
/// MFI = 100 - 100 / (1 + positive_flow / negative_flow) over `timeperiod` bars.
|
||||
/// typical_price = (high + low + close) / 3; raw_money_flow = typical_price * volume.
|
||||
/// Leading `timeperiod` values are NaN.
|
||||
pub fn mfi(
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
volume: &[f64],
|
||||
timeperiod: usize,
|
||||
) -> Vec<f64> {
|
||||
let n = high.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if timeperiod < 1 || n <= timeperiod {
|
||||
return result;
|
||||
}
|
||||
|
||||
let mut pos_flow = vec![0.0_f64; n];
|
||||
let mut neg_flow = vec![0.0_f64; n];
|
||||
let mut tp_prev = (high[0] + low[0] + close[0]) / 3.0;
|
||||
|
||||
for i in 1..n {
|
||||
let tp_cur = (high[i] + low[i] + close[i]) / 3.0;
|
||||
let rmf = tp_cur * volume[i];
|
||||
if tp_cur > tp_prev {
|
||||
pos_flow[i] = rmf;
|
||||
} else if tp_cur < tp_prev {
|
||||
neg_flow[i] = rmf;
|
||||
}
|
||||
tp_prev = tp_cur;
|
||||
}
|
||||
|
||||
// Sliding window sum over timeperiod bars (indices i+1-timeperiod ..= i).
|
||||
// First valid window: indices 1..=timeperiod.
|
||||
let mut pos_sum: f64 = pos_flow[1..=timeperiod].iter().sum();
|
||||
let mut neg_sum: f64 = neg_flow[1..=timeperiod].iter().sum();
|
||||
let mfr = if neg_sum == 0.0 {
|
||||
f64::MAX
|
||||
} else {
|
||||
pos_sum / neg_sum
|
||||
};
|
||||
result[timeperiod] = 100.0 - 100.0 / (1.0 + mfr);
|
||||
|
||||
for i in (timeperiod + 1)..n {
|
||||
pos_sum += pos_flow[i] - pos_flow[i - timeperiod];
|
||||
neg_sum += neg_flow[i] - neg_flow[i - timeperiod];
|
||||
let mfr = if neg_sum == 0.0 {
|
||||
f64::MAX
|
||||
} else {
|
||||
pos_sum / neg_sum
|
||||
};
|
||||
result[i] = 100.0 - 100.0 / (1.0 + mfr);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn obv_up_trend() {
|
||||
let c = vec![1.0, 2.0, 3.0];
|
||||
let v = vec![100.0, 200.0, 300.0];
|
||||
let result = obv(&c, &v);
|
||||
assert!((result[0] - 100.0).abs() < 1e-10);
|
||||
assert!((result[1] - 300.0).abs() < 1e-10);
|
||||
assert!((result[2] - 600.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mfi_range() {
|
||||
let n = 50;
|
||||
let high: Vec<f64> = (1..=n).map(|i| i as f64 + 0.5).collect();
|
||||
let low: Vec<f64> = (1..=n).map(|i| i as f64 - 0.5).collect();
|
||||
let close: Vec<f64> = (1..=n).map(|i| i as f64).collect();
|
||||
let volume: Vec<f64> = vec![1_000_000.0; n];
|
||||
let result = mfi(&high, &low, &close, &volume, 14);
|
||||
for v in result.iter().filter(|v| !v.is_nan()) {
|
||||
assert!(*v >= 0.0 && *v <= 100.0, "MFI out of range: {v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user