fin-primitives
A Rust library of the basic parts every trading program needs: exact prices that refuse bad values, an order book, candles built from trades, 700+ indicators, option pricing and risk limits.
For Rust developers writing trading bots, backtesters, market-data tools or quant research code who do not want to rebuild (and re-debug) the same pieces.
Install
A library, nothing to install system-wide. Rust 1.81 or newer (checked in CI).
rust_decimal_macros gives you the dec!(64250.50) literal used in the examples.
In 15 lines
use ;
use Price;
use dec;
Indicators answer Unavailable until they have enough bars (here the first 14), never a
made-up 0 or NaN. This block is compiled and run by cargo test --doc.
Why this crate and not ta, yata or TA-Lib
Measured on the same 100,000 closes (bench/competitors, bench/talib_compare):
- Exact decimals. Prices, sizes, bars and indicators are
rust_decimal::Decimal, so0.1 + 0.2is0.3and a P&L adds up to the cent. SMA and EMA agree withtaandyatato 1e-12, and with TA-Lib to 2e-11 or better on the 22 indicators both define the same way. - TA-Lib definitions.
Rsiis Wilder's RSI, like TA-Lib.ta's RSI smooths with a different factor and differed by up to 20 RSI points on the same data. - Checked inputs, typed errors. A zero price, a crossed order book or a skipped
sequence number is an
Err, not a silent wrong number. Nounwrapin library code. - One crate for the whole loop: order book, ticks to bars, 700+ indicators, positions, drawdown and VaR limits, Black-Scholes with Greeks.
- The cost is speed.
taandyataare 40 to 160 times faster per indicator update (f64 against 96-bit decimal arithmetic). fin-primitives still does 4 to 25 million updates per second per core. If you only crunch long float64 histories, use them or TA-Lib.
Already using ta, yata or wickra?
Keep them. Turn on the matching feature and their indicators read fin-primitives bars directly, and their bar types convert into fin-primitives ones:
= { = "2.15", = ["ta"] } # or "yata", "wickra", "arrow"
use MovingAverageConvergenceDivergence;
use Next;
let mut macd = new?;
for bar in aggregator.push_tick?
cargo run --example with_ta --features ta runs the full version. Going the other way,
BarInput::try_from(&ta::DataItem), BarInput::try_from(&yata::core::Candle) and
BarInput::try_from(&wickra_core::Candle) turn their bars into fin-primitives input.
Feature flags
| feature | default | adds |
|---|---|---|
serde |
yes | Serialize/Deserialize on the data types; deserializing re-runs validation |
async |
yes | async_signals: a Tokio task that runs a signal pipeline over a channel |
ta |
no | interop with the ta crate |
yata |
no | interop with the yata crate |
wickra |
no | interop with wickra-core (Rust 1.86+) |
arrow |
no | bars to and from Apache Arrow RecordBatch, prices as exact Decimal128 (Rust 1.88+) |
python |
no | PyO3 bindings; cd python && maturin develop --features python (Rust 1.83+) |
default-features = false leaves rust_decimal, chrono, thiserror, libm and statrs.
It also builds for wasm32-unknown-unknown.
How it works
Trades come in as Ticks. OhlcvAggregator::push_tick drops each one into a time bucket and
hands back the finished candle the moment a trade for the next bucket arrives. Candles feed
indicators, which say Unavailable until they have enough history instead of making up a number.
Money works the same way: fills and price marks go into a PositionLedger, its account value
goes into a RiskMonitor, and every rule that is broken comes back as a RiskBreach you can act on.
Examples
Six programs ship in examples/. No network, no API keys, same output every run.
| Run this | You get |
|---|---|
cargo run --example order_book |
a BTC-USD depth ladder, spread, micro-price, the cost of a 5 BTC market buy, and two bad updates rejected |
cargo run --example candles |
337 trades rolled into 32 one-minute candles, drawn with EMA(9) and RSI(14) |
cargo run --example position_risk |
a trading session marked to market, with a drawdown rule and an equity floor firing |
cargo run --example option_chain |
a Black-Scholes option chain with Greeks and an implied-volatility round trip |
cargo run --example value_at_risk |
one-day and ten-day VaR for a $1M book: historical, parametric and Monte Carlo at 95%, 97.5% and 99% |
cargo run --example with_ta --features ta |
ticks rolled into bars by fin-primitives, fed to ta's MACD and Bollinger Bands and to fin-primitives' RSI |
order_book: the book knows its spread and middle price, and refuses updates that would cross it or skip a sequence number.
position_risk (real output, run 2026-09-28, NO_COLOR=1):
# event equity drawdown risk
5 sell 100 AAPL @ 179.10 101,237.00 0.00% ok
6 mark MSFT 398.10 99,865.00 1.36% ok
7 mark AAPL 166.80 98,635.00 2.57% ok
8 mark MSFT 371.50 96,507.00 4.67% BREACH max_drawdown, min_equity
max_drawdown: drawdown 4.67% > 4.00%
min_equity: equity 96507.00 < floor 96600
rejected buy 1000 AAPL Insufficient funds: need 171351.00, have 79866.00
(rows 1 to 4 and 9 to 10 cut for length; the diagram above plays the whole session.)
Use it in 3 steps
1. Make a project and add the crate
&&
2. Put this in src/main.rs
use ;
use ;
use dec;
3. Run it
You will see:
Price::new(-5) -> Price must be positive, got -5
spread 0.50
mid price 64250.25
buy 1 BTC at 64250.58 average
A negative price never gets into your program, the book knows its own spread and middle
price, and "what would buying 1 BTC cost" is one call (0.842 BTC fill at the best ask, the
other 0.158 at the next level up). This exact program is also compiled and run by
cargo test --doc in this repository.
Compared with TA-Lib
Same 100,000 seeded OHLCV bars through both libraries (method, script and full tables;
i7-13700KF, TA-Lib 0.8.1). Where the definitions match, the values agree to 2e-11 or
better; fin-primitives works in exact decimals, so it is the float64 side that rounds.
Two definitions differ on purpose: Atr is a simple average of true range (it equals
TA-Lib's SMA(TRANGE), not Wilder's ATR), and Obv starts at 0 rather than at the first
bar's volume. TA-Lib is much faster: its batch functions take 0.2 to 12 ns per bar, about
100 times less than fin-primitives' exact-decimal update(). Use TA-Lib to crunch long
float64 histories; use fin-primitives for exact values, bar-by-bar state and no C dependency.
| indicator | max difference vs TA-Lib (after bar 1,000) | fin-primitives, ns per bar | TA-Lib batch, ns per bar |
|---|---|---|---|
| SMA(20) / EMA(20) | 9.8e-13 / 5.7e-14 | 74 / 149 | 0.8 / 1.3 |
| RSI(14) | 1.8e-13 | 252 | 2.1 |
| MACD(12,26,9) histogram | 5.0e-14 (seeded differently: 0.036 in the first bars) | 362 | 1.7 |
| Bollinger(20, 2) bands | 1.6e-11 | 1023 | 2.7 |
| ATR(14) | equals TA-Lib SMA(TRANGE, 14) to 1.2e-14 |
86 | 0.8 |
| Stochastic fast %K / %D (14, 3) | 6.7e-13 / 1.3e-12 | 174 / 259 | 7.7 |
| Williams %R(14) / CCI(20) / MFI(14) | 6.7e-13 / 1.0e-11 / 1.3e-12 | 170 / 1060 / 589 | 1.9 / 12.2 / 5.9 |
| ADX(14) | 3.7e-13, except near exact up/down ties that float64 splits | 794 | 6.2 |
| OBV | equals TA-Lib OBV - volume[0] exactly |
13 | 2.8 |
Documentation
| Read this | For |
|---|---|
| docs.rs/fin-primitives | every type and method, with examples that compile |
| docs/ARCHITECTURE.md | how the modules connect, what each one guarantees, design rules, writing your own indicator or risk rule |
| docs/REFERENCE.md | module guides (indicators, series analytics, ledger, drawdown, attribution, Greeks, regimes, backtester, Monte Carlo, factor models, yield curves and more), math definitions, API listing |
| docs/TESTING.md | running the tests and benchmarks, current test status |
| CHANGELOG.md | what changed in each version |
| Project site | the same overview as a web page |
Optional Python bindings are behind the python feature: cd python && maturin develop --release,
then python test_smoke.py.
Research and engineering library. It does not place orders, and nothing here is financial advice.
Contributing
Issues and pull requests are welcome. Public items need /// docs, fallible code returns
Result (no unwrap, expect or panic! outside tests), and new behavior needs a test.
Run cargo fmt, cargo clippy and cargo test --doc before opening a PR. See
CONTRIBUTING.md.
License and related projects
MIT, see LICENSE. fin-stream builds on this crate: it turns live Binance, Coinbase, Alpaca and Polygon trade messages into ticks, bars and order books.