2026-03-23 23:34:28 +05:30
|
|
|
|
/*!
|
|
|
|
|
|
# ferro-ta WASM bindings
|
|
|
|
|
|
|
|
|
|
|
|
WebAssembly bindings for the ferro-ta technical analysis library.
|
|
|
|
|
|
|
|
|
|
|
|
All functions accept `Float64Array` inputs and return `Float64Array` (or a
|
|
|
|
|
|
`js_sys::Array` of `Float64Array` for multi-output indicators such as `BBANDS`
|
|
|
|
|
|
and `MACD`).
|
|
|
|
|
|
|
|
|
|
|
|
## Overlap Studies
|
|
|
|
|
|
- [`sma`] — Simple Moving Average
|
|
|
|
|
|
- [`ema`] — Exponential Moving Average
|
2026-03-24 14:28:51 +05:30
|
|
|
|
- [`wma`] — Weighted Moving Average
|
2026-03-23 23:34:28 +05:30
|
|
|
|
- [`bbands`] — Bollinger Bands (returns `[upper, middle, lower]`)
|
|
|
|
|
|
|
|
|
|
|
|
## Momentum Indicators
|
|
|
|
|
|
- [`rsi`] — Relative Strength Index (Wilder smoothing)
|
|
|
|
|
|
- [`macd`] — Moving Average Convergence/Divergence (returns `[macd, signal, hist]`)
|
|
|
|
|
|
- [`mom`] — Momentum (close[i] - close[i-period])
|
|
|
|
|
|
- [`stochf`] — Fast Stochastic (returns `[fastk, fastd]`)
|
2026-03-24 14:28:51 +05:30
|
|
|
|
- [`adx`] — Average Directional Movement Index
|
2026-03-23 23:34:28 +05:30
|
|
|
|
|
|
|
|
|
|
## Volatility Indicators
|
|
|
|
|
|
- [`atr`] — Average True Range (Wilder smoothing)
|
|
|
|
|
|
|
|
|
|
|
|
## Volume Indicators
|
|
|
|
|
|
- [`obv`] — On-Balance Volume
|
2026-03-24 14:28:51 +05:30
|
|
|
|
- [`mfi`] — Money Flow Index
|
2026-03-23 23:34:28 +05:30
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
use js_sys::{Array, Float64Array};
|
|
|
|
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Helpers
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Copy a `Float64Array` into a `Vec<f64>`.
|
|
|
|
|
|
fn to_vec(arr: &Float64Array) -> Vec<f64> {
|
|
|
|
|
|
let n = arr.length() as usize;
|
|
|
|
|
|
let mut v = vec![0.0f64; n];
|
|
|
|
|
|
arr.copy_to(&mut v);
|
|
|
|
|
|
v
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Create a `Float64Array` from a `Vec<f64>`.
|
|
|
|
|
|
fn from_vec(v: Vec<f64>) -> Float64Array {
|
|
|
|
|
|
// Safety: Float64Array::view requires the backing Vec to stay alive for the
|
|
|
|
|
|
// duration of the copy. We immediately copy via `Float64Array::from` so
|
|
|
|
|
|
// there is no aliasing.
|
|
|
|
|
|
let arr = Float64Array::new_with_length(v.len() as u32);
|
|
|
|
|
|
arr.copy_from(&v);
|
|
|
|
|
|
arr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// SMA — Simple Moving Average
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Simple Moving Average.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `timeperiod` – look-back window (default 30, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array` with the first `timeperiod - 1` values set to `NaN`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn sma(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
|
|
|
|
|
let prices = to_vec(close);
|
|
|
|
|
|
let n = prices.len();
|
|
|
|
|
|
let mut result = vec![f64::NAN; n];
|
|
|
|
|
|
|
|
|
|
|
|
if timeperiod == 0 || n < timeperiod {
|
|
|
|
|
|
return from_vec(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Seed: sum of first window
|
|
|
|
|
|
let mut window_sum: f64 = prices[..timeperiod].iter().sum();
|
|
|
|
|
|
result[timeperiod - 1] = window_sum / timeperiod as f64;
|
|
|
|
|
|
|
|
|
|
|
|
for i in timeperiod..n {
|
|
|
|
|
|
window_sum += prices[i] - prices[i - timeperiod];
|
|
|
|
|
|
result[i] = window_sum / timeperiod as f64;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
from_vec(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// EMA — Exponential Moving Average
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Exponential Moving Average (SMA-seeded).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `timeperiod` – look-back period (default 30, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array` with the first `timeperiod - 1` values set to `NaN`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn ema(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
|
|
|
|
|
let prices = to_vec(close);
|
|
|
|
|
|
let n = prices.len();
|
|
|
|
|
|
let mut result = vec![f64::NAN; n];
|
|
|
|
|
|
|
|
|
|
|
|
if timeperiod == 0 || n < timeperiod {
|
|
|
|
|
|
return from_vec(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let k = 2.0 / (timeperiod as f64 + 1.0);
|
|
|
|
|
|
|
|
|
|
|
|
// Seed with SMA of first window
|
|
|
|
|
|
let seed: f64 = prices[..timeperiod].iter().sum::<f64>() / timeperiod as f64;
|
|
|
|
|
|
result[timeperiod - 1] = seed;
|
|
|
|
|
|
let mut prev = seed;
|
|
|
|
|
|
|
|
|
|
|
|
for i in timeperiod..n {
|
|
|
|
|
|
let val = prices[i] * k + prev * (1.0 - k);
|
|
|
|
|
|
result[i] = val;
|
|
|
|
|
|
prev = val;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
from_vec(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// BBANDS — Bollinger Bands
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Bollinger Bands (SMA ± k × rolling standard deviation).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `timeperiod` – look-back window (default 5, minimum 1).
|
|
|
|
|
|
/// - `nbdevup` – multiplier for the upper band (default 2.0).
|
|
|
|
|
|
/// - `nbdevdn` – multiplier for the lower band (default 2.0).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// A `js_sys::Array` containing three `Float64Array` elements:
|
|
|
|
|
|
/// `[upperband, middleband, lowerband]`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn bbands(
|
|
|
|
|
|
close: &Float64Array,
|
|
|
|
|
|
timeperiod: usize,
|
|
|
|
|
|
nbdevup: f64,
|
|
|
|
|
|
nbdevdn: f64,
|
|
|
|
|
|
) -> Array {
|
|
|
|
|
|
let prices = to_vec(close);
|
|
|
|
|
|
let n = prices.len();
|
|
|
|
|
|
let mut upper = vec![f64::NAN; n];
|
|
|
|
|
|
let mut middle = vec![f64::NAN; n];
|
|
|
|
|
|
let mut lower = vec![f64::NAN; n];
|
|
|
|
|
|
|
|
|
|
|
|
if timeperiod == 0 || n < timeperiod {
|
|
|
|
|
|
let out = Array::new();
|
|
|
|
|
|
out.push(&from_vec(upper));
|
|
|
|
|
|
out.push(&from_vec(middle));
|
|
|
|
|
|
out.push(&from_vec(lower));
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for i in (timeperiod - 1)..n {
|
|
|
|
|
|
let window = &prices[(i + 1 - timeperiod)..=i];
|
|
|
|
|
|
let mean = window.iter().sum::<f64>() / timeperiod as f64;
|
|
|
|
|
|
let variance = window.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / timeperiod as f64;
|
|
|
|
|
|
let stddev = variance.sqrt();
|
|
|
|
|
|
middle[i] = mean;
|
|
|
|
|
|
upper[i] = mean + nbdevup * stddev;
|
|
|
|
|
|
lower[i] = mean - nbdevdn * stddev;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let out = Array::new();
|
|
|
|
|
|
out.push(&from_vec(upper));
|
|
|
|
|
|
out.push(&from_vec(middle));
|
|
|
|
|
|
out.push(&from_vec(lower));
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// RSI — Relative Strength Index (Wilder smoothing, TA-Lib compatible)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Relative Strength Index (Wilder smoothing).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `timeperiod` – look-back period (default 14, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array` — values in `[0, 100]`; first `timeperiod` values are `NaN`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn rsi(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
|
|
|
|
|
let prices = to_vec(close);
|
|
|
|
|
|
let n = prices.len();
|
|
|
|
|
|
let mut result = vec![f64::NAN; n];
|
|
|
|
|
|
|
|
|
|
|
|
if timeperiod == 0 || n <= timeperiod {
|
|
|
|
|
|
return from_vec(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Compute gains and losses
|
|
|
|
|
|
let diffs: Vec<f64> = prices.windows(2).map(|w| w[1] - w[0]).collect();
|
|
|
|
|
|
|
|
|
|
|
|
// Seed average gain / loss over first `timeperiod` bars
|
|
|
|
|
|
let mut avg_gain: f64 = diffs[..timeperiod]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|&d| if d > 0.0 { d } else { 0.0 })
|
|
|
|
|
|
.sum::<f64>()
|
|
|
|
|
|
/ timeperiod as f64;
|
|
|
|
|
|
let mut avg_loss: f64 = diffs[..timeperiod]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|&d| if d < 0.0 { -d } else { 0.0 })
|
|
|
|
|
|
.sum::<f64>()
|
|
|
|
|
|
/ timeperiod as f64;
|
|
|
|
|
|
|
|
|
|
|
|
// First RSI value at index `timeperiod`
|
|
|
|
|
|
let rs = if avg_loss == 0.0 { f64::INFINITY } else { avg_gain / avg_loss };
|
|
|
|
|
|
result[timeperiod] = 100.0 - 100.0 / (1.0 + rs);
|
|
|
|
|
|
|
|
|
|
|
|
// Wilder smoothing for remaining values
|
|
|
|
|
|
for i in (timeperiod + 1)..n {
|
|
|
|
|
|
let diff = diffs[i - 1];
|
|
|
|
|
|
let gain = if diff > 0.0 { diff } else { 0.0 };
|
|
|
|
|
|
let loss = if diff < 0.0 { -diff } else { 0.0 };
|
|
|
|
|
|
avg_gain = (avg_gain * (timeperiod as f64 - 1.0) + gain) / timeperiod as f64;
|
|
|
|
|
|
avg_loss = (avg_loss * (timeperiod as f64 - 1.0) + loss) / timeperiod as f64;
|
|
|
|
|
|
let rs = if avg_loss == 0.0 { f64::INFINITY } else { avg_gain / avg_loss };
|
|
|
|
|
|
result[i] = 100.0 - 100.0 / (1.0 + rs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
from_vec(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ATR — Average True Range (Wilder smoothing)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Average True Range (Wilder smoothing, TA-Lib compatible).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `high` – `Float64Array` of high prices.
|
|
|
|
|
|
/// - `low` – `Float64Array` of low prices.
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `timeperiod` – look-back period (default 14, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array`; first `timeperiod` values are `NaN`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn atr(
|
|
|
|
|
|
high: &Float64Array,
|
|
|
|
|
|
low: &Float64Array,
|
|
|
|
|
|
close: &Float64Array,
|
|
|
|
|
|
timeperiod: usize,
|
|
|
|
|
|
) -> Float64Array {
|
|
|
|
|
|
let h = to_vec(high);
|
|
|
|
|
|
let l = to_vec(low);
|
|
|
|
|
|
let c = to_vec(close);
|
|
|
|
|
|
let n = h.len();
|
|
|
|
|
|
let mut result = vec![f64::NAN; n];
|
|
|
|
|
|
|
|
|
|
|
|
if timeperiod == 0 || n <= timeperiod {
|
|
|
|
|
|
return from_vec(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
if l.len() != n || c.len() != n {
|
|
|
|
|
|
return from_vec(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// True Range for each bar
|
|
|
|
|
|
let mut tr = vec![0.0f64; n];
|
|
|
|
|
|
tr[0] = h[0] - l[0]; // first bar: no previous close
|
|
|
|
|
|
for i in 1..n {
|
|
|
|
|
|
let hl = h[i] - l[i];
|
|
|
|
|
|
let hpc = (h[i] - c[i - 1]).abs();
|
|
|
|
|
|
let lpc = (l[i] - c[i - 1]).abs();
|
|
|
|
|
|
tr[i] = hl.max(hpc).max(lpc);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Seed: SMA of first `timeperiod` true ranges
|
|
|
|
|
|
let seed: f64 = tr[1..=timeperiod].iter().sum::<f64>() / timeperiod as f64;
|
|
|
|
|
|
result[timeperiod] = seed;
|
|
|
|
|
|
let mut prev = seed;
|
|
|
|
|
|
|
|
|
|
|
|
for i in (timeperiod + 1)..n {
|
|
|
|
|
|
let val = (prev * (timeperiod as f64 - 1.0) + tr[i]) / timeperiod as f64;
|
|
|
|
|
|
result[i] = val;
|
|
|
|
|
|
prev = val;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
from_vec(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// OBV — On-Balance Volume
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// On-Balance Volume.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `volume` – `Float64Array` of volume values.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array` — cumulative OBV.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn obv(close: &Float64Array, volume: &Float64Array) -> Float64Array {
|
|
|
|
|
|
let c = to_vec(close);
|
|
|
|
|
|
let v = to_vec(volume);
|
|
|
|
|
|
let n = c.len();
|
|
|
|
|
|
let mut result = vec![0.0f64; n];
|
|
|
|
|
|
|
|
|
|
|
|
if n == 0 || v.len() != n {
|
|
|
|
|
|
return from_vec(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result[0] = v[0];
|
|
|
|
|
|
for i in 1..n {
|
|
|
|
|
|
if c[i] > c[i - 1] {
|
|
|
|
|
|
result[i] = result[i - 1] + v[i];
|
|
|
|
|
|
} else if c[i] < c[i - 1] {
|
|
|
|
|
|
result[i] = result[i - 1] - v[i];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result[i] = result[i - 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
from_vec(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 14:28:51 +05:30
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// WMA — Weighted Moving Average
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Weighted Moving Average.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `timeperiod` – look-back window (default 30, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array` with the first `timeperiod - 1` values set to `NaN`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn wma(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
|
|
|
|
|
let prices = to_vec(close);
|
|
|
|
|
|
from_vec(ferro_ta_core::overlap::wma(&prices, timeperiod))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 23:34:28 +05:30
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// MOM — Momentum
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Momentum — difference between current close and close *timeperiod* bars ago.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `timeperiod` – look-back window (default 10, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array`; first `timeperiod` values are `NaN`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn mom(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
|
|
|
|
|
let prices = to_vec(close);
|
|
|
|
|
|
let n = prices.len();
|
|
|
|
|
|
let mut result = vec![f64::NAN; n];
|
|
|
|
|
|
|
|
|
|
|
|
if timeperiod == 0 || n <= timeperiod {
|
|
|
|
|
|
return from_vec(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for i in timeperiod..n {
|
|
|
|
|
|
result[i] = prices[i] - prices[i - timeperiod];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
from_vec(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// STOCHF — Fast Stochastic Oscillator
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Fast Stochastic Oscillator.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `high` – `Float64Array` of high prices.
|
|
|
|
|
|
/// - `low` – `Float64Array` of low prices.
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `fastk_period` – fast-%K look-back window (default 5, minimum 1).
|
|
|
|
|
|
/// - `fastd_period` – fast-%D SMA smoothing period (default 3, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// A `js_sys::Array` containing two `Float64Array` elements: `[fastk, fastd]`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn stochf(
|
|
|
|
|
|
high: &Float64Array,
|
|
|
|
|
|
low: &Float64Array,
|
|
|
|
|
|
close: &Float64Array,
|
|
|
|
|
|
fastk_period: usize,
|
|
|
|
|
|
fastd_period: usize,
|
|
|
|
|
|
) -> Array {
|
|
|
|
|
|
let h = to_vec(high);
|
|
|
|
|
|
let l = to_vec(low);
|
|
|
|
|
|
let c = to_vec(close);
|
|
|
|
|
|
let n = c.len();
|
|
|
|
|
|
|
|
|
|
|
|
let nan_out = || {
|
|
|
|
|
|
let out = Array::new();
|
|
|
|
|
|
out.push(&from_vec(vec![f64::NAN; n]));
|
|
|
|
|
|
out.push(&from_vec(vec![f64::NAN; n]));
|
|
|
|
|
|
out
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if fastk_period == 0 || fastd_period == 0 || n < fastk_period {
|
|
|
|
|
|
return nan_out();
|
|
|
|
|
|
}
|
|
|
|
|
|
if h.len() != n || l.len() != n {
|
|
|
|
|
|
return nan_out();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fast %K: (close - lowest_low) / (highest_high - lowest_low) * 100
|
|
|
|
|
|
let mut fastk = vec![f64::NAN; n];
|
|
|
|
|
|
for i in (fastk_period - 1)..n {
|
|
|
|
|
|
let low_min = l[(i + 1 - fastk_period)..=i]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.fold(f64::INFINITY, f64::min);
|
|
|
|
|
|
let high_max = h[(i + 1 - fastk_period)..=i]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.fold(f64::NEG_INFINITY, f64::max);
|
|
|
|
|
|
let range = high_max - low_min;
|
|
|
|
|
|
fastk[i] = if range > 0.0 {
|
|
|
|
|
|
(c[i] - low_min) / range * 100.0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
50.0 // all bars at same price — neutral
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fast %D: SMA(fastd_period) of fast %K
|
|
|
|
|
|
let mut fastd = vec![f64::NAN; n];
|
|
|
|
|
|
let k_start = fastk_period - 1;
|
|
|
|
|
|
if n >= k_start + fastd_period {
|
|
|
|
|
|
for i in (k_start + fastd_period - 1)..n {
|
|
|
|
|
|
let window = &fastk[(i + 1 - fastd_period)..=i];
|
|
|
|
|
|
if window.iter().all(|x| x.is_finite()) {
|
|
|
|
|
|
fastd[i] = window.iter().sum::<f64>() / fastd_period as f64;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let out = Array::new();
|
|
|
|
|
|
out.push(&from_vec(fastk));
|
|
|
|
|
|
out.push(&from_vec(fastd));
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 14:28:51 +05:30
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ADX — Average Directional Movement Index
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Average Directional Movement Index (Wilder smoothing).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `high` – `Float64Array` of high prices.
|
|
|
|
|
|
/// - `low` – `Float64Array` of low prices.
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `timeperiod` – look-back period (default 14, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array`; warm-up values are `NaN`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn adx(
|
|
|
|
|
|
high: &Float64Array,
|
|
|
|
|
|
low: &Float64Array,
|
|
|
|
|
|
close: &Float64Array,
|
|
|
|
|
|
timeperiod: usize,
|
|
|
|
|
|
) -> Float64Array {
|
|
|
|
|
|
let h = to_vec(high);
|
|
|
|
|
|
let l = to_vec(low);
|
|
|
|
|
|
let c = to_vec(close);
|
|
|
|
|
|
if h.len() != l.len() || h.len() != c.len() {
|
|
|
|
|
|
return from_vec(vec![f64::NAN; c.len()]);
|
|
|
|
|
|
}
|
|
|
|
|
|
from_vec(ferro_ta_core::momentum::adx(&h, &l, &c, timeperiod))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// MFI — Money Flow Index
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Money Flow Index.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `high` – `Float64Array` of high prices.
|
|
|
|
|
|
/// - `low` – `Float64Array` of low prices.
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `volume` – `Float64Array` of volume values.
|
|
|
|
|
|
/// - `timeperiod` – look-back period (default 14, minimum 1).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// `Float64Array`; warm-up values are `NaN`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn mfi(
|
|
|
|
|
|
high: &Float64Array,
|
|
|
|
|
|
low: &Float64Array,
|
|
|
|
|
|
close: &Float64Array,
|
|
|
|
|
|
volume: &Float64Array,
|
|
|
|
|
|
timeperiod: usize,
|
|
|
|
|
|
) -> Float64Array {
|
|
|
|
|
|
let h = to_vec(high);
|
|
|
|
|
|
let l = to_vec(low);
|
|
|
|
|
|
let c = to_vec(close);
|
|
|
|
|
|
let v = to_vec(volume);
|
|
|
|
|
|
let n = c.len();
|
|
|
|
|
|
if h.len() != n || l.len() != n || v.len() != n {
|
|
|
|
|
|
return from_vec(vec![f64::NAN; n]);
|
|
|
|
|
|
}
|
|
|
|
|
|
from_vec(ferro_ta_core::volume::mfi(&h, &l, &c, &v, timeperiod))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 23:34:28 +05:30
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// MACD — Moving Average Convergence/Divergence
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Moving Average Convergence/Divergence.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
/// - `close` – `Float64Array` of close prices.
|
|
|
|
|
|
/// - `fastperiod` – fast EMA period (default 12).
|
|
|
|
|
|
/// - `slowperiod` – slow EMA period (default 26).
|
|
|
|
|
|
/// - `signalperiod` – signal EMA period (default 9).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
/// A `js_sys::Array` containing three `Float64Array` elements:
|
|
|
|
|
|
/// `[macd_line, signal_line, histogram]`.
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
|
pub fn macd(
|
|
|
|
|
|
close: &Float64Array,
|
|
|
|
|
|
fastperiod: usize,
|
|
|
|
|
|
slowperiod: usize,
|
|
|
|
|
|
signalperiod: usize,
|
|
|
|
|
|
) -> Array {
|
|
|
|
|
|
let prices = to_vec(close);
|
|
|
|
|
|
let n = prices.len();
|
|
|
|
|
|
let nan_result = || {
|
|
|
|
|
|
let out = Array::new();
|
|
|
|
|
|
out.push(&from_vec(vec![f64::NAN; n]));
|
|
|
|
|
|
out.push(&from_vec(vec![f64::NAN; n]));
|
|
|
|
|
|
out.push(&from_vec(vec![f64::NAN; n]));
|
|
|
|
|
|
out
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if fastperiod == 0 || slowperiod == 0 || signalperiod == 0 || fastperiod >= slowperiod {
|
|
|
|
|
|
return nan_result();
|
|
|
|
|
|
}
|
|
|
|
|
|
if n < slowperiod {
|
|
|
|
|
|
return nan_result();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helper: SMA-seeded EMA
|
|
|
|
|
|
let ema_vec = |data: &[f64], period: usize| -> Vec<f64> {
|
|
|
|
|
|
let len = data.len();
|
|
|
|
|
|
let mut result = vec![f64::NAN; len];
|
|
|
|
|
|
if period == 0 || len < period {
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
let k = 2.0 / (period as f64 + 1.0);
|
|
|
|
|
|
let seed: f64 = data[..period].iter().sum::<f64>() / period as f64;
|
|
|
|
|
|
result[period - 1] = seed;
|
|
|
|
|
|
for i in period..len {
|
|
|
|
|
|
result[i] = data[i] * k + result[i - 1] * (1.0 - k);
|
|
|
|
|
|
}
|
|
|
|
|
|
result
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let fast_ema = ema_vec(&prices, fastperiod);
|
|
|
|
|
|
let slow_ema = ema_vec(&prices, slowperiod);
|
|
|
|
|
|
|
|
|
|
|
|
// MACD line = fast EMA − slow EMA (valid from index slowperiod - 1)
|
|
|
|
|
|
let mut macd_line = vec![f64::NAN; n];
|
|
|
|
|
|
for i in (slowperiod - 1)..n {
|
|
|
|
|
|
if fast_ema[i].is_finite() && slow_ema[i].is_finite() {
|
|
|
|
|
|
macd_line[i] = fast_ema[i] - slow_ema[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Signal line = EMA(signalperiod) of macd_line, seeded at index slowperiod - 1
|
|
|
|
|
|
let macd_start = slowperiod - 1;
|
|
|
|
|
|
let mut signal_line = vec![f64::NAN; n];
|
|
|
|
|
|
let signal_seed_end = macd_start + signalperiod;
|
|
|
|
|
|
if signal_seed_end > n {
|
|
|
|
|
|
let out = Array::new();
|
|
|
|
|
|
out.push(&from_vec(macd_line.clone()));
|
|
|
|
|
|
out.push(&from_vec(signal_line));
|
|
|
|
|
|
out.push(&from_vec(vec![f64::NAN; n]));
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Seed: SMA of first signalperiod MACD values
|
|
|
|
|
|
let seed: f64 = macd_line[macd_start..signal_seed_end]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.sum::<f64>()
|
|
|
|
|
|
/ signalperiod as f64;
|
|
|
|
|
|
signal_line[signal_seed_end - 1] = seed;
|
|
|
|
|
|
let k = 2.0 / (signalperiod as f64 + 1.0);
|
|
|
|
|
|
for i in signal_seed_end..n {
|
|
|
|
|
|
if macd_line[i].is_finite() {
|
|
|
|
|
|
signal_line[i] = macd_line[i] * k + signal_line[i - 1] * (1.0 - k);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Histogram = MACD − signal
|
|
|
|
|
|
let mut histogram = vec![f64::NAN; n];
|
|
|
|
|
|
for i in (signal_seed_end - 1)..n {
|
|
|
|
|
|
if macd_line[i].is_finite() && signal_line[i].is_finite() {
|
|
|
|
|
|
histogram[i] = macd_line[i] - signal_line[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let out = Array::new();
|
|
|
|
|
|
out.push(&from_vec(macd_line));
|
|
|
|
|
|
out.push(&from_vec(signal_line));
|
|
|
|
|
|
out.push(&from_vec(histogram));
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// WASM tests (run with `wasm-pack test --node`)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
use wasm_bindgen_test::wasm_bindgen_test;
|
|
|
|
|
|
|
|
|
|
|
|
fn make_arr(v: &[f64]) -> Float64Array {
|
|
|
|
|
|
let arr = Float64Array::new_with_length(v.len() as u32);
|
|
|
|
|
|
arr.copy_from(v);
|
|
|
|
|
|
arr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn get_finite(arr: &Float64Array) -> Vec<f64> {
|
|
|
|
|
|
let mut v = vec![0.0f64; arr.length() as usize];
|
|
|
|
|
|
arr.copy_to(&mut v);
|
|
|
|
|
|
v.into_iter().filter(|x| x.is_finite()).collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// SMA tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_sma_output_length() {
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
|
|
|
|
let out = sma(&close, 3);
|
|
|
|
|
|
assert_eq!(out.length(), 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_sma_known_value() {
|
|
|
|
|
|
// SMA(3) of [1,2,3,4,5]: first valid at index 2 = (1+2+3)/3 = 2.0
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
|
|
|
|
let out = sma(&close, 3);
|
|
|
|
|
|
let vals: Vec<f64> = {
|
|
|
|
|
|
let mut v = vec![0.0f64; 5];
|
|
|
|
|
|
out.copy_to(&mut v);
|
|
|
|
|
|
v
|
|
|
|
|
|
};
|
|
|
|
|
|
assert!(vals[0].is_nan());
|
|
|
|
|
|
assert!(vals[1].is_nan());
|
|
|
|
|
|
assert!((vals[2] - 2.0).abs() < 1e-10);
|
|
|
|
|
|
assert!((vals[3] - 3.0).abs() < 1e-10);
|
|
|
|
|
|
assert!((vals[4] - 4.0).abs() < 1e-10);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// EMA tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_ema_output_length() {
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
|
|
|
|
let out = ema(&close, 3);
|
|
|
|
|
|
assert_eq!(out.length(), 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_ema_seed_equals_sma() {
|
|
|
|
|
|
// Seed of EMA(3) at index 2 should equal SMA(3) = 2.0
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
|
|
|
|
let out = ema(&close, 3);
|
|
|
|
|
|
let mut vals = vec![0.0f64; 5];
|
|
|
|
|
|
out.copy_to(&mut vals);
|
|
|
|
|
|
assert!((vals[2] - 2.0).abs() < 1e-10);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// BBANDS tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_bbands_returns_three_arrays() {
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
|
|
|
|
let out = bbands(&close, 3, 2.0, 2.0);
|
|
|
|
|
|
assert_eq!(out.length(), 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_bbands_middle_equals_sma() {
|
|
|
|
|
|
let data = [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10];
|
|
|
|
|
|
let close = make_arr(&data);
|
|
|
|
|
|
let bands = bbands(&close, 3, 2.0, 2.0);
|
|
|
|
|
|
|
|
|
|
|
|
// Middle band should equal SMA(3)
|
|
|
|
|
|
let middle = Float64Array::from(bands.get(1));
|
|
|
|
|
|
let sma_out = sma(&close, 3);
|
|
|
|
|
|
|
|
|
|
|
|
let mut m = vec![0.0f64; 7];
|
|
|
|
|
|
middle.copy_to(&mut m);
|
|
|
|
|
|
let mut s = vec![0.0f64; 7];
|
|
|
|
|
|
sma_out.copy_to(&mut s);
|
|
|
|
|
|
|
|
|
|
|
|
for i in 2..7 {
|
|
|
|
|
|
assert!((m[i] - s[i]).abs() < 1e-10, "middle[{i}] != sma[{i}]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_bbands_upper_greater_than_lower() {
|
|
|
|
|
|
let data = [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10];
|
|
|
|
|
|
let close = make_arr(&data);
|
|
|
|
|
|
let bands = bbands(&close, 3, 2.0, 2.0);
|
|
|
|
|
|
let upper = Float64Array::from(bands.get(0));
|
|
|
|
|
|
let lower = Float64Array::from(bands.get(2));
|
|
|
|
|
|
let mut u = vec![0.0f64; 7];
|
|
|
|
|
|
let mut l = vec![0.0f64; 7];
|
|
|
|
|
|
upper.copy_to(&mut u);
|
|
|
|
|
|
lower.copy_to(&mut l);
|
|
|
|
|
|
for i in 2..7 {
|
|
|
|
|
|
assert!(u[i] >= l[i], "upper[{i}] < lower[{i}]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// RSI tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_rsi_output_length() {
|
|
|
|
|
|
let close = make_arr(&[
|
|
|
|
|
|
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15,
|
|
|
|
|
|
43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
|
|
|
|
|
|
]);
|
|
|
|
|
|
let out = rsi(&close, 14);
|
|
|
|
|
|
assert_eq!(out.length(), 15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_rsi_range_0_to_100() {
|
|
|
|
|
|
let close = make_arr(&[
|
|
|
|
|
|
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15,
|
|
|
|
|
|
43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
|
|
|
|
|
|
]);
|
|
|
|
|
|
let out = rsi(&close, 5);
|
|
|
|
|
|
let finite = get_finite(&out);
|
|
|
|
|
|
for v in finite {
|
|
|
|
|
|
assert!(v >= 0.0 && v <= 100.0, "RSI out of range: {v}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// ATR tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_atr_output_length() {
|
|
|
|
|
|
let high = make_arr(&[45.0, 46.0, 47.0, 46.0, 45.0, 44.0, 45.0]);
|
|
|
|
|
|
let low = make_arr(&[43.0, 44.0, 45.0, 44.0, 43.0, 42.0, 43.0]);
|
|
|
|
|
|
let close = make_arr(&[44.0, 45.0, 46.0, 45.0, 44.0, 43.0, 44.0]);
|
|
|
|
|
|
let out = atr(&high, &low, &close, 3);
|
|
|
|
|
|
assert_eq!(out.length(), 7);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_atr_all_positive() {
|
|
|
|
|
|
let high = make_arr(&[45.0, 46.0, 47.0, 46.0, 45.0, 44.0, 45.0]);
|
|
|
|
|
|
let low = make_arr(&[43.0, 44.0, 45.0, 44.0, 43.0, 42.0, 43.0]);
|
|
|
|
|
|
let close = make_arr(&[44.0, 45.0, 46.0, 45.0, 44.0, 43.0, 44.0]);
|
|
|
|
|
|
let out = atr(&high, &low, &close, 3);
|
|
|
|
|
|
let finite = get_finite(&out);
|
|
|
|
|
|
assert!(!finite.is_empty());
|
|
|
|
|
|
for v in finite {
|
|
|
|
|
|
assert!(v > 0.0, "ATR should be positive, got {v}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// OBV tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_obv_output_length() {
|
|
|
|
|
|
let close = make_arr(&[10.0, 11.0, 10.0, 12.0, 11.0]);
|
|
|
|
|
|
let volume = make_arr(&[100.0, 200.0, 150.0, 300.0, 250.0]);
|
|
|
|
|
|
let out = obv(&close, &volume);
|
|
|
|
|
|
assert_eq!(out.length(), 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_obv_known_values() {
|
|
|
|
|
|
// close: 10 → 11 (up, +200) → 10 (dn, -150) → 12 (up, +300) → 11 (dn, -250)
|
|
|
|
|
|
// OBV: 100, 300, 150, 450, 200
|
|
|
|
|
|
let close = make_arr(&[10.0, 11.0, 10.0, 12.0, 11.0]);
|
|
|
|
|
|
let volume = make_arr(&[100.0, 200.0, 150.0, 300.0, 250.0]);
|
|
|
|
|
|
let out = obv(&close, &volume);
|
|
|
|
|
|
let mut vals = vec![0.0f64; 5];
|
|
|
|
|
|
out.copy_to(&mut vals);
|
|
|
|
|
|
assert!((vals[0] - 100.0).abs() < 1e-10);
|
|
|
|
|
|
assert!((vals[1] - 300.0).abs() < 1e-10);
|
|
|
|
|
|
assert!((vals[2] - 150.0).abs() < 1e-10);
|
|
|
|
|
|
assert!((vals[3] - 450.0).abs() < 1e-10);
|
|
|
|
|
|
assert!((vals[4] - 200.0).abs() < 1e-10);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// MACD tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_macd_returns_three_arrays() {
|
|
|
|
|
|
let data = [
|
|
|
|
|
|
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15,
|
|
|
|
|
|
43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
|
|
|
|
|
|
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.15,
|
|
|
|
|
|
43.61, 44.33, 44.83, 45.10, 45.15, 43.61, 44.33,
|
|
|
|
|
|
];
|
|
|
|
|
|
let close = make_arr(&data);
|
|
|
|
|
|
let out = macd(&close, 3, 5, 2);
|
|
|
|
|
|
assert_eq!(out.length(), 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_macd_output_length() {
|
|
|
|
|
|
let data: Vec<f64> = (1..=30).map(|x| x as f64 * 1.0).collect();
|
|
|
|
|
|
let close = make_arr(&data);
|
|
|
|
|
|
let out = macd(&close, 3, 5, 2);
|
|
|
|
|
|
let macd_line = Float64Array::from(out.get(0));
|
|
|
|
|
|
assert_eq!(macd_line.length(), 30);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_macd_finite_values_after_warmup() {
|
|
|
|
|
|
// With fastperiod=3, slowperiod=5, signalperiod=2:
|
|
|
|
|
|
// MACD line valid from index 4; signal from index 5.
|
|
|
|
|
|
let data: Vec<f64> = (1..=20).map(|x| x as f64).collect();
|
|
|
|
|
|
let close = make_arr(&data);
|
|
|
|
|
|
let out = macd(&close, 3, 5, 2);
|
|
|
|
|
|
let signal = Float64Array::from(out.get(1));
|
|
|
|
|
|
let finite = get_finite(&signal);
|
|
|
|
|
|
assert!(!finite.is_empty(), "signal should have finite values");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_macd_histogram_is_macd_minus_signal() {
|
|
|
|
|
|
let data: Vec<f64> = (1..=20).map(|x| x as f64).collect();
|
|
|
|
|
|
let close = make_arr(&data);
|
|
|
|
|
|
let out = macd(&close, 3, 5, 2);
|
|
|
|
|
|
let macd_arr = Float64Array::from(out.get(0));
|
|
|
|
|
|
let sig_arr = Float64Array::from(out.get(1));
|
|
|
|
|
|
let hist_arr = Float64Array::from(out.get(2));
|
|
|
|
|
|
|
|
|
|
|
|
let n = macd_arr.length() as usize;
|
|
|
|
|
|
let mut m = vec![0.0f64; n];
|
|
|
|
|
|
let mut s = vec![0.0f64; n];
|
|
|
|
|
|
let mut h = vec![0.0f64; n];
|
|
|
|
|
|
macd_arr.copy_to(&mut m);
|
|
|
|
|
|
sig_arr.copy_to(&mut s);
|
|
|
|
|
|
hist_arr.copy_to(&mut h);
|
|
|
|
|
|
|
|
|
|
|
|
for i in 0..n {
|
|
|
|
|
|
if m[i].is_finite() && s[i].is_finite() {
|
|
|
|
|
|
assert!((h[i] - (m[i] - s[i])).abs() < 1e-10,
|
|
|
|
|
|
"histogram[{i}] != macd[{i}] - signal[{i}]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// MOM tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_mom_output_length() {
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
|
|
|
|
|
|
let out = mom(&close, 3);
|
|
|
|
|
|
assert_eq!(out.length(), 7);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_mom_known_values() {
|
|
|
|
|
|
// MOM(2) of [1,2,3,4,5]: NaN, NaN, 2.0, 2.0, 2.0
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
|
|
|
|
let out = mom(&close, 2);
|
|
|
|
|
|
let mut vals = vec![0.0f64; 5];
|
|
|
|
|
|
out.copy_to(&mut vals);
|
|
|
|
|
|
assert!(vals[0].is_nan());
|
|
|
|
|
|
assert!(vals[1].is_nan());
|
|
|
|
|
|
assert!((vals[2] - 2.0).abs() < 1e-10, "MOM[2] should be 2.0");
|
|
|
|
|
|
assert!((vals[3] - 2.0).abs() < 1e-10, "MOM[3] should be 2.0");
|
|
|
|
|
|
assert!((vals[4] - 2.0).abs() < 1e-10, "MOM[4] should be 2.0");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// STOCHF tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_stochf_returns_two_arrays() {
|
|
|
|
|
|
let h = make_arr(&[10.0, 11.0, 12.0, 11.0, 10.0, 12.0, 13.0]);
|
|
|
|
|
|
let l = make_arr(&[8.0, 9.0, 10.0, 9.0, 8.0, 10.0, 11.0]);
|
|
|
|
|
|
let c = make_arr(&[9.0, 10.0, 11.0, 10.0, 9.0, 11.0, 12.0]);
|
|
|
|
|
|
let out = stochf(&h, &l, &c, 3, 2);
|
|
|
|
|
|
assert_eq!(out.length(), 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_stochf_output_length() {
|
|
|
|
|
|
let h = make_arr(&[10.0, 11.0, 12.0, 11.0, 10.0, 12.0, 13.0]);
|
|
|
|
|
|
let l = make_arr(&[8.0, 9.0, 10.0, 9.0, 8.0, 10.0, 11.0]);
|
|
|
|
|
|
let c = make_arr(&[9.0, 10.0, 11.0, 10.0, 9.0, 11.0, 12.0]);
|
|
|
|
|
|
let out = stochf(&h, &l, &c, 3, 2);
|
|
|
|
|
|
let fastk = Float64Array::from(out.get(0));
|
|
|
|
|
|
assert_eq!(fastk.length(), 7);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_stochf_fastk_in_0_to_100() {
|
|
|
|
|
|
let h = make_arr(&[10.0, 11.0, 12.0, 11.0, 10.0, 12.0, 13.0]);
|
|
|
|
|
|
let l = make_arr(&[8.0, 9.0, 10.0, 9.0, 8.0, 10.0, 11.0]);
|
|
|
|
|
|
let c = make_arr(&[9.0, 10.0, 11.0, 10.0, 9.0, 11.0, 12.0]);
|
|
|
|
|
|
let out = stochf(&h, &l, &c, 3, 2);
|
|
|
|
|
|
let fastk = Float64Array::from(out.get(0));
|
|
|
|
|
|
let finite = get_finite(&fastk);
|
|
|
|
|
|
assert!(!finite.is_empty(), "fastk should have finite values");
|
|
|
|
|
|
for v in finite {
|
|
|
|
|
|
assert!(v >= 0.0 && v <= 100.0, "fastk value {v} out of [0, 100]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-24 14:28:51 +05:30
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// WMA tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_wma_output_length() {
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
|
|
|
|
let out = wma(&close, 3);
|
|
|
|
|
|
assert_eq!(out.length(), 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_wma_known_value() {
|
|
|
|
|
|
// WMA(3) at index 2 = (1*1 + 2*2 + 3*3) / 6 = 14/6
|
|
|
|
|
|
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
|
|
|
|
|
let out = wma(&close, 3);
|
|
|
|
|
|
let mut vals = vec![0.0f64; 5];
|
|
|
|
|
|
out.copy_to(&mut vals);
|
|
|
|
|
|
assert!(vals[0].is_nan());
|
|
|
|
|
|
assert!(vals[1].is_nan());
|
|
|
|
|
|
assert!((vals[2] - (14.0 / 6.0)).abs() < 1e-10);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// ADX tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_adx_output_length() {
|
|
|
|
|
|
let h = make_arr(&[10.0, 11.0, 12.0, 13.0, 13.5, 14.0, 14.5, 15.0]);
|
|
|
|
|
|
let l = make_arr(&[9.0, 9.5, 10.5, 11.5, 12.0, 12.5, 13.0, 13.5]);
|
|
|
|
|
|
let c = make_arr(&[9.5, 10.5, 11.5, 12.0, 13.0, 13.5, 14.0, 14.5]);
|
|
|
|
|
|
let out = adx(&h, &l, &c, 3);
|
|
|
|
|
|
assert_eq!(out.length(), 8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_adx_values_in_range() {
|
|
|
|
|
|
let h = make_arr(&[10.0, 11.0, 12.0, 13.0, 13.5, 14.0, 14.5, 15.0]);
|
|
|
|
|
|
let l = make_arr(&[9.0, 9.5, 10.5, 11.5, 12.0, 12.5, 13.0, 13.5]);
|
|
|
|
|
|
let c = make_arr(&[9.5, 10.5, 11.5, 12.0, 13.0, 13.5, 14.0, 14.5]);
|
|
|
|
|
|
let out = adx(&h, &l, &c, 3);
|
|
|
|
|
|
for v in get_finite(&out) {
|
|
|
|
|
|
assert!((0.0..=100.0).contains(&v), "ADX out of range: {v}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// MFI tests
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_mfi_output_length() {
|
|
|
|
|
|
let h = make_arr(&[10.0, 11.0, 12.0, 11.5, 12.5, 13.0, 13.5]);
|
|
|
|
|
|
let l = make_arr(&[9.0, 9.5, 10.5, 10.0, 11.0, 11.5, 12.0]);
|
|
|
|
|
|
let c = make_arr(&[9.5, 10.5, 11.5, 11.0, 12.0, 12.5, 13.0]);
|
|
|
|
|
|
let v = make_arr(&[100.0, 110.0, 120.0, 130.0, 125.0, 140.0, 150.0]);
|
|
|
|
|
|
let out = mfi(&h, &l, &c, &v, 3);
|
|
|
|
|
|
assert_eq!(out.length(), 7);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
|
|
fn test_mfi_values_in_range() {
|
|
|
|
|
|
let h = make_arr(&[10.0, 11.0, 12.0, 11.5, 12.5, 13.0, 13.5]);
|
|
|
|
|
|
let l = make_arr(&[9.0, 9.5, 10.5, 10.0, 11.0, 11.5, 12.0]);
|
|
|
|
|
|
let c = make_arr(&[9.5, 10.5, 11.5, 11.0, 12.0, 12.5, 13.0]);
|
|
|
|
|
|
let v = make_arr(&[100.0, 110.0, 120.0, 130.0, 125.0, 140.0, 150.0]);
|
|
|
|
|
|
let out = mfi(&h, &l, &c, &v, 3);
|
|
|
|
|
|
for val in get_finite(&out) {
|
|
|
|
|
|
assert!((0.0..=100.0).contains(&val), "MFI out of range: {val}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-23 23:34:28 +05:30
|
|
|
|
}
|