* feat(rvi): add Relative Vigor Index
Dorsey's RVI = SMA(close - open, period) / SMA(high - low, period) over
a rolling window of period candles. Candle input, single parameter
period (default 10). Positive on average-bullish windows, negative on
average-bearish. Holds the previous value if the entire window has
zero range (denominator undefined).
Reference: Donald Dorsey, also pandas-ta rvi.
Touchpoints: rvi.rs + mod.rs + lib.rs re-export, PyRvi + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values reference,
RviNode (4-column OHLC batch) + index.d.ts/index.js + indicators.test
.js factory + reference, WasmRvi + make_candle_ohlc helper, candle-fuzz
target + criterion bench, README + CHANGELOG.
* feat(pgo): add Pretty Good Oscillator
Mark Johnson's PGO = (close - SMA(close, period)) / EMA(TR, period).
Counts roughly how many ATR-equivalents the close sits from its
period-bar mean. Candle input, single parameter period (default 14).
Johnson's heuristic uses +3/-3 crossings as entry signals.
Touchpoints: pgo.rs + mod.rs + lib.rs re-export, PyPgo + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values flat-close
reference, PgoNode (h/l/c) + index.d.ts/index.js + indicators.test.js
factory + reference, WasmPgo, candle-fuzz target + bench, README +
CHANGELOG.
* feat(kst): add Know Sure Thing (Pring)
Pring's long-horizon momentum oscillator: weighted sum of four
SMA-smoothed ROC series with fixed weights 1, 2, 3, 4, plus an SMA
signal line. Nine parameters (four ROC periods, four SMA periods, one
signal period); classic() applies Pring's recommended defaults.
Multi-output indicator emitting KstOutput { kst, signal }.
Touchpoints: kst.rs + mod.rs + lib.rs re-export, PyKst + __init__.py
+ test_new_indicators MULTI + test_known_values flat-input reference,
KstNode + KstValue + index.d.ts/index.js + indicators.test.js multi
factory + reference, WasmKst (manual JsValue object), scalar-fuzz
target (handled outside the f64-output drive helper), README +
CHANGELOG.
* feat(smi): add Stochastic Momentum Index (Blau)
Blau's doubly-EMA-smoothed bounded oscillator: measures the close's
displacement from the centre of the recent high-low range, scaled by
the smoothed range. Candle input, three parameters (period, d_period,
d2_period) with defaults 5 / 3 / 3.
Internally feeds both the displacement-EMA stack and the range-EMA
stack on every candle so they warm up in parallel (gating either
behind the other starves the second by one input).
Touchpoints: smi.rs + mod.rs + lib.rs re-export, PySmi + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values flat-input
reference, SmiNode + index.d.ts/index.js + indicators.test.js factory
+ reference, WasmSmi, candle-fuzz target, README + CHANGELOG.
* feat(laguerre-rsi): add Ehlers Laguerre RSI
Four-stage Laguerre polynomial filter wrapped in an RSI-style up/down
accumulator. Single gamma in [0, 1] (default 0.5) trades lag for
smoothness. State is seeded by setting all four L_i to the first input
so a constant series stays at the neutral 50. Output clamped to
[0, 100] to absorb floating-point rounding.
Reference: Ehlers, Time Warp - Without Space Travel, 2002.
Touchpoints: laguerre_rsi.rs + mod.rs + lib.rs re-export, PyLaguerreRsi
+ __init__.py + test_new_indicators SCALAR + test_known_values neutral
reference, LaguerreRsiNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmLaguerreRsi via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(connors-rsi): add Connors RSI (CRSI)
Larry Connors' 3-component aggregate: RSI(close), RSI(streak), and
PercentRank of the 1-period return over the last period_rank returns.
Each component is bounded in [0, 100] so the aggregate is too.
Three parameters (period_rsi, period_streak, period_rank) with
defaults 3 / 2 / 100. Streak tracks consecutive up/down runs (resets
to 0 on unchanged close).
Touchpoints: connors_rsi.rs + mod.rs + lib.rs re-export, PyConnorsRsi
+ __init__.py + test_new_indicators SCALAR + test_known_values bounded
reference, ConnorsRsiNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmConnorsRsi via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(inertia): add Dorsey Inertia (RVI + LinReg)
Donald Dorsey's Inertia — a LinearRegression smoothing of the RVI
series. Endpoint of an n-bar least-squares fit of RVI is the indicator
reading. Preserves trend direction while damping the ratio. Candle
input, two parameters (rvi_period, linreg_period) with defaults 14 / 20.
Touchpoints: inertia.rs + mod.rs + lib.rs re-export, PyInertia +
__init__.py + test_new_indicators CANDLE_SCALAR + test_known_values
constant reference, InertiaNode (4-column OHLC batch) + index.d.ts /
index.js + indicators.test.js factory + reference, WasmInertia,
candle-fuzz target, README + CHANGELOG.
* test(kst): Move KST out of MULTI dict (it is scalar-input)
KST sits in the MULTI dict (candle-input, multi-output) but its
update() takes a single f64, not a candle tuple. The shared streaming
loop in test_multi_streaming_matches_batch fed the OHLCV tuple in,
which crashed with `TypeError: argument 'value': must be real number,
not tuple` on every Python matrix entry.
Split into a new MULTI_SCALAR_INPUT dict with its own test function
that feeds the close-price stream as floats. KST is currently the
only such indicator; structure is ready for future scalar-input
multi-output additions (e.g. some MACD-shaped indicators).
* test(coverage): Cover SMI zero-range and ConnorsRsi zero-prev cold paths
codecov/patch on PR 40 flagged two uncovered defensive branches:
- SMI returns self.current early when the smoothed range collapses to
zero (`r2 <= 0.0`) so the formula stays defined. Exercised by feeding
bars where high == low.
- ConnorsRsi skips the ROC ring-buffer update when the previous price
is exactly zero so the divide-by-zero in `(input - prev) / prev` is
impossible. Exercised by seeding the first bar at 0.0.
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 |
|---|---|---|---|---|
| ★ Wickra | clean | yes | Python + Node + WASM + Rust | yes |
| 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 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 9950X, 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.