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
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "wickra"
description = "Streaming-first technical analysis library: incremental indicators, drop-in TA-Lib replacement, multi-language."
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
[lints]
workspace = true
[dependencies]
wickra-core = { workspace = true }
[features]
default = ["parallel"]
parallel = ["wickra-core/parallel"]
[dev-dependencies]
approx = { workspace = true }
criterion = { workspace = true }
proptest = { workspace = true }
wickra-data = { path = "../wickra-data" }
[[bench]]
name = "indicators"
harness = false
[[example]]
name = "backtest"
path = "../../examples/rust/backtest.rs"
required-features = []
+142
View File
@@ -0,0 +1,142 @@
//! Microbenchmarks for every built-in indicator.
//!
//! Run with:
//! ```text
//! cargo bench -p wickra
//! ```
//!
//! Each benchmark feeds a deterministic synthetic price series through both the
//! streaming (`update` loop) and batch APIs of an indicator. Sizes cover small
//! (1 000), medium (10 000), and large (100 000) workloads.
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use wickra::{
Atr, BatchExt, BollingerBands, Candle, Ema, Indicator, MacdIndicator, Obv, Rsi, Sma,
Stochastic, Wma,
};
/// Deterministic synthetic price series of length `n`.
fn price_series(n: usize) -> Vec<f64> {
(0..n)
.map(|i| {
let t = i as f64;
100.0 + (t * 0.013).sin() * 12.0 + (t * 0.071).cos() * 4.0 + (t * 0.003).sin() * 30.0
})
.collect()
}
/// Synthetic OHLC candle series.
fn candle_series(n: usize) -> Vec<Candle> {
let closes = price_series(n);
closes
.iter()
.enumerate()
.map(|(i, c)| {
let t = i as f64;
let spread = 0.5 + (t * 0.05).sin().abs();
// Benchmark synthetic data: i originates from a usize counter capped at 100_000,
// well within i64::MAX. The wrap-around lint does not apply here.
#[allow(clippy::cast_possible_wrap)]
let ts = i as i64;
Candle::new_unchecked(*c, c + spread, c - spread, *c, 1_000.0, ts)
})
.collect()
}
fn bench_scalar<I, F>(c: &mut Criterion, name: &str, sizes: &[usize], make: F)
where
F: Fn() -> I,
I: Indicator<Input = f64, Output = f64> + BatchExt,
{
let mut group = c.benchmark_group(name);
for &n in sizes {
let series = price_series(n);
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::new("streaming", n), &series, |b, prices| {
b.iter(|| {
let mut ind = make();
for p in prices {
black_box(ind.update(*p));
}
});
});
group.bench_with_input(BenchmarkId::new("batch", n), &series, |b, prices| {
b.iter(|| {
let mut ind = make();
black_box(ind.batch(prices));
});
});
}
group.finish();
}
fn bench_macd(c: &mut Criterion, sizes: &[usize]) {
let mut group = c.benchmark_group("macd");
for &n in sizes {
let series = price_series(n);
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::new("streaming", n), &series, |b, prices| {
b.iter(|| {
let mut ind = MacdIndicator::classic();
for p in prices {
black_box(ind.update(*p));
}
});
});
}
group.finish();
}
fn bench_bollinger(c: &mut Criterion, sizes: &[usize]) {
let mut group = c.benchmark_group("bollinger");
for &n in sizes {
let series = price_series(n);
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::new("streaming", n), &series, |b, prices| {
b.iter(|| {
let mut ind = BollingerBands::classic();
for p in prices {
black_box(ind.update(*p));
}
});
});
}
group.finish();
}
fn bench_candle_input<I, F, O>(c: &mut Criterion, name: &str, sizes: &[usize], make: F)
where
F: Fn() -> I,
I: Indicator<Input = Candle, Output = O>,
{
let mut group = c.benchmark_group(name);
for &n in sizes {
let candles = candle_series(n);
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::new("streaming", n), &candles, |b, candles| {
b.iter(|| {
let mut ind = make();
for c in candles {
black_box(ind.update(*c));
}
});
});
}
group.finish();
}
fn benches(c: &mut Criterion) {
let sizes = [1_000_usize, 10_000, 100_000];
bench_scalar(c, "sma", &sizes, || Sma::new(14).unwrap());
bench_scalar(c, "ema", &sizes, || Ema::new(14).unwrap());
bench_scalar(c, "wma", &sizes, || Wma::new(14).unwrap());
bench_scalar(c, "rsi", &sizes, || Rsi::new(14).unwrap());
bench_macd(c, &sizes);
bench_bollinger(c, &sizes);
bench_candle_input(c, "atr", &sizes, || Atr::new(14).unwrap());
bench_candle_input(c, "stochastic", &sizes, Stochastic::classic);
bench_candle_input(c, "obv", &sizes, Obv::new);
}
criterion_group!(name = wickra_benches; config = Criterion::default(); targets = benches);
criterion_main!(wickra_benches);
+21
View File
@@ -0,0 +1,21 @@
//! Wickra: streaming-first technical analysis.
//!
//! This crate is a thin re-export of [`wickra_core`] so downstream users can depend on
//! a single `wickra` package without thinking about the internal split. Every public
//! item lives in `wickra_core`; only the names re-exported here are part of the stable
//! public API.
//!
//! # Example
//!
//! ```
//! use wickra::{Indicator, Sma};
//!
//! let mut sma = Sma::new(3).unwrap();
//! let prices = [1.0, 2.0, 3.0, 4.0, 5.0];
//! let out: Vec<Option<f64>> = prices.iter().map(|p| sma.update(*p)).collect();
//! assert_eq!(out, vec![None, None, Some(2.0), Some(3.0), Some(4.0)]);
//! ```
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use wickra_core::*;