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:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user