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,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 = []
|
||||
@@ -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);
|
||||
@@ -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::*;
|
||||
Reference in New Issue
Block a user