quantedge-ta
A streaming technical analysis library for Rust. Correct, tested, documented.
Features
Type-safe convergence
Indicators return Option<Self::Output>. No value until there's enough data.
No silent NaN, no garbage early values. The type system enforces correctness.
For indicators with infinite memory (EMA), full_convergence() reports how
many bars are needed for the seed's influence to decay below 1%.
Bring your own data
Indicators accept any type implementing the Ohlcv trait. No forced conversion
to a library-specific struct. Implement five required methods on your existing
type and you're done. Volume has a default implementation for data sources that
don't provide it.
O(1) incremental updates
Indicators maintain running state and update in constant time per tick. No re-scanning the window.
WASM compatible
Works in WebAssembly environments. The library compiles for
wasm32-unknown-unknown (browser) and wasm32-wasip1 (WASI runtimes). Zero
dependencies, no filesystem or OS calls in the library itself. CI verifies
WASM compatibility on every commit.
Live repainting
Indicators track bar boundaries using open_time. A kline with a new
open_time advances the window; same open_time replaces the current value.
Useful for trading terminals and real-time systems that need indicator values
on forming bars.
Typed outputs
Each indicator defines its own output type via an associated type on the
Indicator trait. SMA, EMA, RSI, and ATR return f64. Bollinger Bands returns
BbValue { upper, middle, lower }. MACD returns
MacdValue { macd, signal, histogram }. Stochastic returns
StochValue { k, d }. Keltner Channel returns
KcValue { upper, middle, lower }. No downcasting, no enums, full type safety.
Usage
use ;
use NonZero;
let mut sma = new;
for kline in stream
Bollinger Bands returns a struct:
use ;
use NonZero;
let config = builder
.length
.build;
let mut bb = new;
for kline in stream
Custom standard deviation multiplier:
use ;
use NonZero;
let config = builder
.length
.std_dev
.build;
Live data with repainting:
// Open kline arrives (open_time = 1000)
sma.compute; // computes with current bar
// Same bar, new trade (open_time = 1000, updated close)
sma.compute; // replaces current bar value
// Next bar (open_time = 2000)
sma.compute; // advances the window
The caller controls bar boundaries. The library handles the rest.
Indicator Trait
Each indicator defines its output type. No downcasting needed:
// Sma: Output = f64
// Ema: Output = f64
// Rsi: Output = f64
// Bb: Output = BbValue { upper: f64, middle: f64, lower: f64 }
// Macd: Output = MacdValue { macd: f64, signal: Option<f64>, histogram: Option<f64> }
// Stoch: Output = StochValue { k: f64, d: Option<f64> }
// Atr: Output = f64
// Kc: Output = KcValue { upper: f64, middle: f64, lower: f64 }
Ohlcv Trait
Implement the Ohlcv trait on your own data type:
use ;
Convergence
Every indicator config exposes convergence() — the number of bars that
compute() must process before it starts returning Some. During backtesting
this defines the warm-up (seeding) phase: bars where the indicator is
stabilising and should not drive trading decisions.
use ;
use NonZero;
let sma = close;
let rsi = close;
let macd = default_close; // MACD(12, 26, 9)
// The slowest indicator determines the warm-up length
let warmup = sma.convergence // 20
.max // 15
.max; // 26
// → skip the first 26 bars before acting on signals
SMA and BB converge as soon as the window fills (length bars). EMA and RSI
use exponential smoothing with infinite memory; the SMA seed influences all
subsequent values. RSI output begins at bar length + 1. For EMA, EmaConfig
provides full_convergence() — the number of bars until the seed's
contribution decays below 1% (e.g. 63 for EMA(20) = 3 × (20 + 1)).
Price Sources
Each indicator is configured with a PriceSource that determines which value
to extract from the Ohlcv input:
| Source | Formula |
|---|---|
| Close | close |
| Open | open |
| High | high |
| Low | low |
| HL2 | (high + low) / 2 |
| HLC3 | (high + low + close) / 3 |
| OHLC4 | (open + high + low + close) / 4 |
| HLCC4 | (high + low + close + close) / 4 |
| TrueRange | max(high - low, |high - prev_close|, |low - prev_close|) |
Indicators
| Indicator | Output | Description |
|---|---|---|
| SMA | f64 |
Simple Moving Average |
| EMA | f64 |
Exponential Moving Average |
| RSI | f64 |
Relative Strength Index (Wilder's smoothing) |
| BB | BbValue |
Bollinger Bands (upper, mid, lower) |
| MACD | MacdValue |
Moving Average Convergence Divergence |
| ATR | f64 |
Average True Range |
| Stoch | StochValue |
Stochastic Oscillator (%K, %D) |
| KC | KcValue |
Keltner Channel (upper, mid, lower) |
Planned
CHOP, CCI, ADX, VWAP, OBV and more.
Benchmarks
Measured with Criterion.rs on 744 BTC/USDT 1-hour bars from Binance.
Stream measures end-to-end throughput including window fill.
Tick isolates steady-state per-bar cost on a fully converged indicator.
Repaint measures single-tick repaint cost (same open_time, perturbed close)
on a converged indicator.
Repaint Stream measures end-to-end throughput with 3 ticks per bar
(open → mid → final), 2232 total observations.
Hardware: Apple M5 Max (18 cores), 128 GB RAM, macOS 26.3.2, rustc 1.93.1,
--release profile.
Stream — process 744 bars from cold start
| Indicator | Period | Time (median) | Throughput |
|---|---|---|---|
| SMA | 20 | 810 ns | 918 Melem/s |
| SMA | 200 | 883 ns | 842 Melem/s |
| EMA | 20 | 986 ns | 754 Melem/s |
| EMA | 200 | 956 ns | 778 Melem/s |
| BB | 20 | 1.01 µs | 737 Melem/s |
| BB | 200 | 959 ns | 776 Melem/s |
| RSI | 14 | 3.04 µs | 245 Melem/s |
| RSI | 140 | 2.94 µs | 253 Melem/s |
| MACD | 12/26/9 | 2.83 µs | 263 Melem/s |
| MACD | 120/260/90 | 2.93 µs | 254 Melem/s |
| ATR | 14 | 1.42 µs | 524 Melem/s |
| ATR | 140 | 1.32 µs | 564 Melem/s |
| Stoch | 14/3/3 | 6.35 µs | 117 Melem/s |
| Stoch | 140/30/30 | 13.2 µs | 56.2 Melem/s |
| KC | 20/10 | 1.53 µs | 486 Melem/s |
| KC | 200/100 | 1.56 µs | 477 Melem/s |
Tick — single compute() on a converged indicator
| Indicator | Period | Time (median) |
|---|---|---|
| SMA | 20 | 8.31 ns |
| SMA | 200 | 17.7 ns |
| EMA | 20 | 2.80 ns |
| EMA | 200 | 1.80 ns |
| BB | 20 | 9.21 ns |
| BB | 200 | 18.4 ns |
| RSI | 14 | 8.62 ns |
| RSI | 140 | 5.72 ns |
| MACD | 12/26/9 | 8.80 ns |
| MACD | 120/260/90 | 8.81 ns |
| ATR | 14 | 1.86 ns |
| ATR | 140 | 1.45 ns |
| Stoch | 14/3/3 | 43.0 ns |
| Stoch | 140/30/30 | 54.0 ns |
| KC | 20/10 | 4.32 ns |
| KC | 200/100 | 3.84 ns |
Repaint — single compute() repaint on a converged indicator
| Indicator | Period | Time (median) |
|---|---|---|
| SMA | 20 | 8.44 ns |
| SMA | 200 | 17.5 ns |
| EMA | 20 | 2.87 ns |
| EMA | 200 | 1.51 ns |
| BB | 20 | 9.84 ns |
| BB | 200 | 19.6 ns |
| RSI | 14 | 7.84 ns |
| RSI | 140 | 3.68 ns |
| MACD | 12/26/9 | 8.80 ns |
| MACD | 120/260/90 | 8.68 ns |
| ATR | 14 | 1.95 ns |
| ATR | 140 | 1.45 ns |
| Stoch | 14/3/3 | 40.9 ns |
| Stoch | 140/30/30 | 63.4 ns |
| KC | 20/10 | 4.05 ns |
| KC | 200/100 | 3.81 ns |
Repaint Stream — process 744 bars × 3 ticks from cold start
| Indicator | Period | Time (median) | Throughput |
|---|---|---|---|
| SMA | 20 | 2.49 µs | 896 Melem/s |
| SMA | 200 | 2.60 µs | 859 Melem/s |
| EMA | 20 | 2.65 µs | 842 Melem/s |
| EMA | 200 | 2.67 µs | 836 Melem/s |
| BB | 20 | 2.97 µs | 751 Melem/s |
| BB | 200 | 3.42 µs | 653 Melem/s |
| RSI | 14 | 4.87 µs | 458 Melem/s |
| RSI | 140 | 5.18 µs | 431 Melem/s |
| MACD | 12/26/9 | 7.73 µs | 289 Melem/s |
| MACD | 120/260/90 | 8.07 µs | 277 Melem/s |
| ATR | 14 | 3.41 µs | 654 Melem/s |
| ATR | 140 | 3.43 µs | 651 Melem/s |
| Stoch | 14/3/3 | 13.4 µs | 166 Melem/s |
| Stoch | 140/30/30 | 18.6 µs | 120 Melem/s |
| KC | 20/10 | 5.23 µs | 427 Melem/s |
| KC | 200/100 | 4.62 µs | 483 Melem/s |
Run locally:
Minimum Supported Rust Version
1.93
Licence
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contributing
Contributions welcome. Please open an issue before submitting large changes.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 licence, shall be dual-licensed as above, without any additional terms or conditions.