* test(pmo): cover periods/value accessors, name, zero-prev fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/pmo.rs
(file at 90.56%):
- const accessors periods (76-78), value (81-83) — never queried
- line 103 (`0.0` in the prev == 0.0 ROC fallback) — every existing
test used prices > 0, so the divide-by-zero guard never fired
- Indicator-impl name body (130-132) — never queried
Add accessors_and_metadata covering periods/value/name. Add
zero_previous_price_treats_roc_as_flat seeding prev_price = 0 then
pushing a non-zero price — the ROC must take the flat-momentum
fallback (0.0) and the doubly-smoothed PMO emits exactly 0.0
rather than NaN.
pmo.rs is now at 106/106 lines, no behavioural change.
* test(ppo): cover periods/value accessors, name, zero-slow-EMA fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/ppo.rs
(file at 90.29%):
- const accessors periods (71-73), value (76-78) — never queried
- line 96 (`0.0` in the s == 0.0 PPO fallback) — every existing test
used prices ≈ 100, so the slow EMA was never 0 and the
divide-by-zero guard never fired
- Indicator-impl name body (122-124) — never queried
Add accessors_and_metadata covering periods/value/name. Add
zero_slow_ema_yields_zero_ppo feeding a stream of zeros — both EMAs
converge to 0.0 and the indicator must emit exactly 0.0 (flat
momentum) rather than NaN.
ppo.rs is now at 103/103 lines, no behavioural change.
* test(roc): cover period accessor, warmup/name, zero-prev fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/roc.rs
(file at 87.80%):
- const accessor period (47-49) — never queried
- line 70 (`0.0` in the prev == 0.0 ROC fallback) — every test used
prices ≥ 1.0, so the divide-by-zero guard never fired
- Indicator-impl warmup_period (83-85), name (91-93) — never queried
Add accessors_and_metadata covering period == 5, warmup_period == 6
(= period + 1), name == "ROC". Add zero_previous_price_yields_zero_roc
feeding a leading zero followed by `period` more values so the front
of the window is exactly 0.0; the next emission must be the
flat-momentum fallback 0.0 (not NaN).
roc.rs is now at 82/82 lines, no behavioural change.
* test(ulcer_index): cover period/value accessors, name, zero-max fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/ulcer_index.rs
(file at 93.86%):
- const accessors period (77-80), value (82-85) — never queried
- line 123 (`0.0` in the max_price == 0.0 drawdown fallback) — every
test used prices > 0, so the trailing-max divisor was always positive
- Indicator-impl name body (162-164) — never queried
Add accessors_and_metadata covering period/value/name. Add
zero_max_price_yields_zero_drawdown feeding a stream of zeros — the
trailing max is exactly 0.0 and the drawdown computation would
otherwise hit 0/0 NaN; the indicator must emit exactly 0.0
(drawdown is 0% by convention).
ulcer_index.rs is now at 163/163 lines, no behavioural change.
* test(williams_r): cover period accessor, warmup/name, zero-range branch
Codecov flagged 10 lines in crates/wickra-core/src/indicators/williams_r.rs
(file at 89.79%):
- const accessor period (49-51) — never queried
- line 78 (`Some(-50.0)` in the range == 0.0 fallback) — every test
used H != L candles, so the lookback range was always positive
- Indicator-impl warmup_period (87-89), name (95-97) — never queried
Add accessors_and_metadata covering period == 14, warmup_period == 14,
name == "WilliamsR". Add zero_range_yields_minus_fifty feeding flat
candles (H == L == close) — the lookback hi/lo coincide and the
divide-by-zero guard fires, returning the neutral mid-range value
-50.0.
williams_r.rs is now at 98/98 lines, no behavioural change.
Wickra
Streaming-first technical indicators. Install with pip install wickra — no system dependencies.
Wickra is a multi-language technical-analysis library with a Rust core and bindings for Python, Node.js, and WebAssembly. Every indicator is a state machine that updates in O(1) per new data point, so live trading bots and historical backtests share the exact same implementation.
import numpy as np
import wickra as ta
# Batch: classic TA-Lib-style usage
prices = np.linspace(100, 200, 1000)
rsi = ta.RSI(14)
values = rsi.batch(prices) # numpy array, NaN during warmup
# Streaming: same indicator, fed tick by tick
rsi = ta.RSI(14)
for price in live_feed:
value = rsi.update(price) # O(1) — no recomputation over history
if value is not None and value > 70:
print("overbought")
Why Wickra exists
The Python TA ecosystem has plenty of libraries — TA-Lib, pandas-ta, finta, talipp, tulipy — and every one of them shares the same blind spot:
| Library | Install pain | Streaming | Multi-language | Active |
|---|---|---|---|---|
| TA-Lib (Python) | yes (C deps) | no | no | barely |
| pandas-ta | clean | no | no | slow |
| finta | clean | no | no | stale |
| ta-lib-python | yes (C deps) | no | no | barely |
| talipp | clean | yes | no | yes |
| Tulip Indicators | yes (C deps) | no | partial | stale |
| ooples (C#) | clean | no | C# only | yes |
| Wickra | clean | yes | Python+Node+WASM+Rust | yes |
Wickra is the only library that combines all of: clean install, streaming, multi-language reach, and active maintenance.
Benchmark: how much faster is "streaming-first"?
The numbers below were measured on a single developer workstation and are not guaranteed to reproduce identically on different hardware — absolute µs values depend on CPU, memory clock and OS scheduler. Read them as relative speedups between libraries on identical input, not as a universal performance contract.
- Reproduced on: Windows 11 Pro 26200, AMD Ryzen 9 7950X3D, 64 GB DDR5,
Rust 1.92 (release profile,
lto = "fat",codegen-units = 1), Python 3.12, Node 20. - Reproduce yourself:
pip install -e bindings/python[bench]thenpython -m benchmarks.compare_libraries. The script auto-detects every installed peer library and runs them on the same generated inputs as Wickra. The CI jobcross-library-benchruns the same script on every push and uploads the raw report as a build artefact.
Lower µs/op = faster. Wickra wins every batch category outright, and the streaming gap widens linearly with how much history a batch-only library has to recompute on every tick.
Batch — single full pass over a 20 000-bar series
Reading the table: each cell shows that library's runtime, plus how many times slower it is than Wickra in parentheses. ★ marks the winner per row.
| Indicator | Wickra | finta | talipp |
|---|---|---|---|
| SMA(20) | 95.6 µs ★ | 343.5 µs (3.6× slower) | 7 640.6 µs (79.9× slower) |
| EMA(20) | 64.6 µs ★ | 223.1 µs (3.5× slower) | 12 160.9 µs (188.2× slower) |
| RSI(14) | 126.2 µs ★ | 1 107.1 µs (8.8× slower) | 15 792.2 µs (125.1× slower) |
| MACD(12, 26, 9) | 119.0 µs ★ | 531.8 µs (4.5× slower) | 49 788.1 µs (418.2× slower) |
| Bollinger(20, 2.0) | 105.3 µs ★ | 812.0 µs (7.7× slower) | 130 938.3 µs (1 243.7× slower) |
| ATR(14) | 123.5 µs ★ | 5 144.8 µs (41.7× slower) | 28 816.0 µs (233.4× slower) |
Streaming — per-tick latency after seeding with 5 000 historical bars
A batch-only library has to re-run its full indicator over the entire history on every new tick; Wickra updates state in O(1).
| Indicator | Wickra (per tick) | talipp (per tick) |
|---|---|---|
| RSI(14) | 0.119 µs ★ | 1.644 µs (13.8× slower) |
TA-Lib and pandas-ta are not included here because both fail to install cleanly on Windows without C build tooling — which is precisely the install pain Wickra was built to remove. The benchmark script auto-detects every peer library it can find and runs them on the same inputs as Wickra; install them in your environment to see those rows light up too.
Run the suite yourself:
pip install -e bindings/python[bench]
python -m benchmarks.compare_libraries
Indicators
71 streaming-first indicators across eight families. Every one passes the
batch == streaming equivalence test, reference-value tests, and reset
semantics tests.
| Family | Indicators |
|---|---|
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA |
| Momentum Oscillators | RSI (Wilder), Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator |
| Trend & Directional | MACD, ADX (+DI/-DI), Aroon, TRIX, Aroon Oscillator, Vortex, Mass Index, Choppiness Index, Vertical Horizontal Filter |
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power |
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility |
| Trailing Stops | Parabolic SAR, SuperTrend, Chandelier Exit, Chande Kroll Stop, ATR Trailing Stop |
| Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement |
| Price Statistics | Typical Price, Median Price, Weighted Close, Linear Regression, Linear Regression Slope, Z-Score, Linear Regression Angle |
Adding a new indicator means implementing one trait in Rust; all four bindings inherit it automatically.
Languages
| Binding | Install | Example |
|---|---|---|
| Python (PyO3) | pip install wickra |
examples/python/backtest.py |
| Node.js (napi-rs) | npm install wickra |
examples/node/backtest.js |
| Browser / WASM | npm install wickra-wasm |
examples/wasm/index.html |
| Rust | cargo add wickra |
examples/rust/src/bin/backtest.rs |
Each binding ships several runnable examples (streaming, backtest, live feed);
examples/README.md is the full cross-language index.
The wickra-core crate is unsafe-forbidden, so every binding inherits a
memory-safe implementation.
Rust API
use wickra::{Indicator, BatchExt, Chain, Ema, Rsi, Sma};
// Streaming or batch — same trait, same code.
let mut sma = Sma::new(14)?;
let out: Vec<Option<f64>> = sma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let mut rsi = Rsi::new(14)?;
for price in live_feed {
if let Some(v) = rsi.update(price) {
println!("RSI = {v}");
}
}
// Compose indicators: RSI(7) on top of EMA(14).
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
chain.update(price);
Live data sources
wickra-data (separate crate, opt-in) ships:
- A streaming OHLCV CSV reader.
- A tick-to-candle aggregator with arbitrary timeframes.
- A candle resampler for multi-timeframe analysis (1m → 5m → 1h on the fly).
- A Binance Spot WebSocket kline adapter (feature
live-binance).
use wickra::{Indicator, Rsi};
use wickra_data::live::binance::{BinanceKlineStream, Interval};
let mut stream = BinanceKlineStream::connect(&["BTCUSDT".into()], Interval::OneMinute).await?;
let mut rsi = Rsi::new(14)?;
while let Some(event) = stream.next_event().await? {
if event.is_closed {
if let Some(v) = rsi.update(event.candle.close) {
println!("RSI = {v:.2}");
}
}
}
A Python live-trading example using the public websockets package lives at
examples/python/live_trading.py.
Project layout
wickra/
├── crates/
│ ├── wickra-core/ core engine + all 71 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
├── bindings/
│ ├── python/ PyO3 + maturin (publishes on PyPI)
│ ├── node/ napi-rs (publishes on npm)
│ └── wasm/ wasm-bindgen (browsers, bundlers, Node)
├── examples/ examples/README.md indexes every language
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
│ ├── rust/ Rust workspace member (`wickra-examples`)
│ ├── python/ backtest, live trading, parallel assets, multi-tf
│ ├── node/ streaming, backtest, live trading (load `wickra`)
│ └── wasm/ browser demo for `wickra-wasm`
└── .github/workflows/ CI and release pipelines
Rust benchmarks live in crates/wickra/benches/; runnable Rust examples live
in the workspace member crate at examples/rust/. There is no top-level
benches/ directory.
Building everything from source
# Rust core + tests
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo bench -p wickra
# Python binding (requires Rust toolchain + maturin)
cd bindings/python
maturin develop --release
pytest
# WASM binding (requires wasm-pack + wasm32-unknown-unknown target)
wasm-pack build bindings/wasm --target web --release --features panic-hook
# Node binding (requires @napi-rs/cli)
cd bindings/node && npm install && npm run build && npm test
Testing
Every layer is covered; run the suites with the commands in Building everything from source.
wickra-core: unit tests per indicator — textbook reference values (Wilder RSI, Bollinger Bands, MACD, ATR, Stochastic),batch == streamingequivalence,resetsemantics, NaN/Inf handling, and property tests.wickra-data: unit tests for CSV decoding, the tick aggregator, the resampler, and the Binance payload parser.bindings/python: pytest covering smoke checks, streaming/batch equivalence, reference values, lifecycle, input validation, and dict/tuple candle inputs.bindings/node:node --testcases for batch, streaming, and reference values across all indicators.bindings/wasm:wasm-bindgen-testcases for constructors, equivalence, and reference values.
Contributing
Contributions are very welcome — issues, bug reports, ideas, and pull requests all land in the same place: https://github.com/kingchenc/wickra.
A short orientation for first-time contributors:
- Adding an indicator. Implement the
Indicatortrait incrates/wickra-core/src/indicators/<name>.rs, wire it intoindicators/mod.rsand the crate root, and add reference-value tests, abatch == streamingequivalence test, and (where it makes sense) a proptest. The four bindings inherit your indicator automatically once you expose it in the language wrappers. - Fixing a numeric bug. Add a failing test that pins the textbook value
first, then fix the math. Property tests in
crates/wickra-corecatch most regressions; please don't disable them. - Improving a binding. Each binding lives under
bindings/<lang>with its own tests; please keep thebatch == streaminginvariant. - Style.
cargo fmt --all+cargo clippy --workspace --all-targets -- -D warningsare CI gates; running them locally before pushing keeps reviews short.
For larger architectural changes, open an issue first so we can sketch the shape together before you invest the time.
License
Licensed under the PolyForm Noncommercial License 1.0.0. See LICENSE.
In plain English: use it, fork it, modify it, redistribute it, file issues, send pull requests — all welcome. Personal projects, research, education, non-profits, government, hobby trading bots: all fine. The one thing that's not allowed is commercial sale of the software or of services built around it. If you want to use Wickra commercially, get in touch about a license.
If Wickra saved you time, the cheapest way to say thanks is to ⭐ the repo.