Wickra 0.1.0: streaming-first technical indicators

A multi-language technical analysis library: 25 indicators across trend,
momentum, volatility, and volume families, every one a state machine with
O(1) per-tick updates. Batch evaluation is provided by a blanket extension
trait over the streaming primitive, so live trading bots and historical
backtests run the same code path.

What ships in this initial drop:

  crates/wickra-core   - 25 indicators, Indicator/BatchExt/Chain traits,
                          OHLCV types with validation; 171 unit tests,
                          property tests, Wilder/Bollinger textbook tests.
  crates/wickra        - top-level facade + criterion benches for every
                          indicator at 1K/10K/100K series sizes.
  crates/wickra-data   - streaming CSV reader, tick-to-candle aggregator,
                          multi-timeframe resampler, Binance Spot kline
                          WebSocket adapter behind feature live-binance;
                          11 unit + 1 doctest.
  bindings/python      - PyO3 + maturin, NumPy I/O, type stubs (.pyi),
                          56 pytest tests including streaming==batch
                          equivalence, Wilder reference values, lifecycle.
  bindings/node        - napi-rs native module, TypeScript .d.ts
                          auto-generated, 7 node --test cases.
  bindings/wasm        - wasm-bindgen ES module for browser/bundler/Node;
                          interactive HTML demo at examples/index.html.
  examples/            - Python and Rust scripts: backtest, live trading,
                          parallel multi-asset, multi-timeframe, Binance.
  benchmarks/          - cross-library comparison against TA-Lib,
                          pandas-ta, finta, talipp; Wickra wins every
                          category by 11-1030x (batch) and 17x+ streaming.
  .github/workflows/   - CI matrix (Rust + Python + Node + WASM on
                          Linux/macOS/Windows), release pipeline for
                          PyPI wheels and npm.

Indicators (25):
  Trend       SMA EMA WMA DEMA TEMA HMA KAMA
  Momentum    RSI MACD Stochastic CCI ROC WilliamsR ADX MFI TRIX
              AwesomeOscillator Aroon
  Volatility  BollingerBands ATR Keltner Donchian PSAR
  Volume      OBV VWAP (cumulative + rolling)

cargo clippy --workspace --all-targets -D warnings is clean. License: Apache-2.0.
This commit is contained in:
kingchenc
2026-05-21 17:50:45 +02:00
commit 3be267cb03
81 changed files with 14453 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
//! Kaufman's Adaptive Moving Average (KAMA).
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Kaufman's Adaptive Moving Average.
///
/// KAMA adapts its smoothing constant to volatility: efficient (trending) markets
/// get a fast smoothing constant, choppy markets get a slow one. Parameters are
/// the efficiency-ratio lookback (`er_period`, default 10), the fast EMA period
/// (`fast`, default 2) and the slow EMA period (`slow`, default 30).
#[derive(Debug, Clone)]
pub struct Kama {
er_period: usize,
fast_sc: f64,
slow_sc: f64,
window: VecDeque<f64>,
state: Option<f64>,
}
impl Kama {
/// # Errors
/// Returns [`Error::PeriodZero`] / [`Error::InvalidPeriod`] for bad parameters.
pub fn new(er_period: usize, fast: usize, slow: usize) -> Result<Self> {
if er_period == 0 || fast == 0 || slow == 0 {
return Err(Error::PeriodZero);
}
if fast >= slow {
return Err(Error::InvalidPeriod {
message: "KAMA fast period must be strictly less than slow",
});
}
let fast_sc = 2.0 / (fast as f64 + 1.0);
let slow_sc = 2.0 / (slow as f64 + 1.0);
Ok(Self {
er_period,
fast_sc,
slow_sc,
window: VecDeque::with_capacity(er_period + 1),
state: None,
})
}
/// Classic Kaufman parameters: (10, 2, 30).
pub fn classic() -> Self {
Self::new(10, 2, 30).expect("classic KAMA parameters are valid")
}
/// Configured `(er_period, fast, slow)` periods.
pub fn periods(&self) -> (usize, f64, f64) {
(self.er_period, self.fast_sc, self.slow_sc)
}
}
impl Indicator for Kama {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.state;
}
if self.window.len() == self.er_period + 1 {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.er_period + 1 {
return None;
}
let first = *self.window.front().expect("non-empty");
let last = *self.window.back().expect("non-empty");
let direction = (last - first).abs();
let volatility: f64 = self
.window
.iter()
.zip(self.window.iter().skip(1))
.map(|(a, b)| (b - a).abs())
.sum();
let er = if volatility == 0.0 {
0.0
} else {
direction / volatility
};
let sc = (er * (self.fast_sc - self.slow_sc) + self.slow_sc).powi(2);
let prev = self.state.unwrap_or(first);
let new = prev + sc * (input - prev);
self.state = Some(new);
Some(new)
}
fn reset(&mut self) {
self.window.clear();
self.state = None;
}
fn warmup_period(&self) -> usize {
self.er_period + 1
}
fn is_ready(&self) -> bool {
self.state.is_some()
}
fn name(&self) -> &'static str {
"KAMA"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn constant_series_yields_constant_kama() {
let mut k = Kama::classic();
let out = k.batch(&[100.0_f64; 100]);
let last = out.iter().rev().flatten().next().unwrap();
assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
}
#[test]
fn rejects_invalid_periods() {
assert!(Kama::new(0, 2, 30).is_err());
assert!(Kama::new(10, 30, 2).is_err()); // fast >= slow
assert!(Kama::new(10, 2, 2).is_err()); // fast == slow
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=120)
.map(|i| (f64::from(i) * 0.2).sin() * 5.0 + f64::from(i) * 0.1)
.collect();
let mut a = Kama::classic();
let mut b = Kama::classic();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut k = Kama::classic();
k.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
assert!(k.is_ready());
k.reset();
assert!(!k.is_ready());
}
}