fin-primitives
Rust · crates.io · MIT

fin-primitives

Validated, decimal-precise building blocks for trading and quant systems: an order book, candles, indicators, a ledger and risk rules that refuse bad data instead of carrying it.

$ cargo add fin-primitives
ETH-USD 1m candles · ema9337 ticks → 32 bars
3,1803,2003,2203,24015:0015:0815:1615:243,201.61
BTC-USD L2 bookseq 14
Drawn from the library's own output: the same ticks, bars and book as cargo run --example candles and order_book.

Four examples, real output

Each block below is the captured stdout of an example in the repository, colors included. Clone, run, and you get the same numbers: the inputs are fixed or seeded.

Order book

A BTC-USD book from 14 sequenced deltas, printed as a depth ladder with spread, micro-price, imbalance and the VWAP of a 5 BTC market buy. Then a crossing delta is rejected and rolled back, and a sequence gap is refused.

cargo run --example order_book
source: examples/order_book.rs
$ cargo run --example order_book
  BTC-USD  level-2 book, top 7 levels, seq 14

  side       price     size      cum  depth
  ask    64,256.00    4.900   14.129  
  ask    64,254.50    1.125    9.229  
  ask    64,253.00    3.460    8.104  
  ask    64,252.00    2.077    4.644  
  ask    64,251.50    0.415    2.567  
  ask    64,251.00    1.310    2.152  
  ask    64,250.50    0.842    0.842  
  ---- spread 0.50 (0.08 bps)  mid 64,250.25 ----
  bid    64,250.00    1.204    1.204  
  bid    64,249.50    0.655    1.859  
  bid    64,249.00    2.380    4.239  
  bid    64,248.00    1.720    5.959  
  bid    64,247.50    3.015    8.974  
  bid    64,246.00    2.260   11.234  
  bid    64,244.50    5.400   16.634  

  mid            64,250.25  (best bid + best ask) / 2
  micro-price    64,250.29  size-weighted mid
  imbalance         +0.081  bid-heavy
  buy 5 BTC      64,251.52  VWAP walking the asks, 0.20 bps over mid

  seq 15       bid 64,251.00 x 2.0  rejected     Inverted spread: best_bid 64251.00 >= best_ask 64250.50
               book unchanged       rolled back  best bid still 64,250.00, seq still 14
  seq 17       ask 64,250.50 x 0.5  rejected     Order book sequence mismatch: expected 15, got 17

Ticks to candles

337 ticks from a seeded random walk go through OhlcvAggregator into 32 one-minute bars. EMA(9) is dotted over the candles; RSI(14) reports Unavailable for 14 bars instead of a NaN.

cargo run --example candles
source: examples/candles.rs
$ cargo run --example candles
  ETH-USD  337 ticks aggregated into 32 one-minute bars

                                        ┃ ┃ │                      3,249.57
                  │                     ┃ ┃ ┃                      
              │   ┃                   ┃ ┃ │ ┃                      
              ┃ ┃ ┃ ┃               ┃ ┃     ┃ │ │                  3,238.34
              ┃ ┃ ┃ │ ┃             ┃    · · ·┃·┃·                 
              ┃       ┃   │ ┃ ┃ │   ┃  ·      │ ┃  ·               
              ┃       ┃ ┃ ┃ ┃·┃·┃   ┃·          ┃    ·             3,227.10
            ┃        · ·┃·│·┃   ┃·┃·            ┃ ┃ │  · ·         
            ┃      ·    │         ┃             ┃ │ ┃   │  ·       
        ┃ ┃ │                                     │ ┃ ┃ ┃    ·     3,215.86
        ┃ ┃                                         ┃ ┃ ┃ │    ·   
      ┃ ┃ │                                               ┃      · 
    ┃ ┃ ┃                                                 ┃   ┃ ┃  3,204.63
    ┃ │                                                   ┃ ┃ ┃ ┃  
    ┃                                                       │      
    ┃                                                              3,193.40
  ┃                                                                
  ┃                                                                
  │                                                                3,182.16
  15:00           15:08           15:16           15:24

  ┃ up bar   ┃ down bar   · ema9

  time       open      high       low     close  volume ticks      ema9   rsi14
  15:00  3,186.17  3,190.94  3,182.16  3,190.94   10.58    12    warmup  warmup
  15:01  3,192.16  3,203.13  3,192.16  3,203.13   10.16    11    warmup  warmup
  15:02  3,203.11  3,206.99  3,201.35  3,206.99    4.26    10    warmup  warmup
    ...
  15:12  3,226.93  3,229.22  3,223.81  3,226.08   11.30     9  3,224.26  warmup
  15:13  3,224.91  3,232.65  3,224.21  3,229.71   16.59    11  3,225.35  warmup
  15:14  3,230.73  3,230.73  3,225.99  3,226.66    9.50    12  3,225.61   71.72
  15:15  3,228.21  3,230.54  3,221.88  3,221.88    8.49    11  3,224.86   67.50
    ...
  15:30  3,202.10  3,203.78  3,200.80  3,202.86    7.10     9  3,212.09   44.40
  15:31  3,202.96  3,202.96  3,199.02  3,201.61    9.01    10  3,209.99   43.70

  rsi14 returned Unavailable for bars 1-14; first value on bar 15 (15:14). No NaN, no zero-fill.

Ledger and risk

Fills and marks run through a PositionLedger; its net liquidation value feeds a RiskMonitor with a 4% drawdown rule and an equity floor. An unaffordable buy is refused.

cargo run --example position_risk
source: examples/position_risk.rs
$ cargo run --example position_risk
  session  start 100,000.00  rules: drawdown > 4%, equity < 96,600.00

  #   event                        equity  drawdown  risk
  1   buy  200 AAPL @ 175.00    99,999.00     0.00%  ok
  2   buy   80 MSFT @ 410.00    99,998.00     0.00%  ok
  3   mark AAPL 178.40         100,678.00     0.00%  ok
  4   mark MSFT 415.25         101,098.00     0.00%  ok
  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
  9   sell  80 MSFT @ 372.00    96,546.00     4.63%  BREACH max_drawdown, min_equity
        max_drawdown: drawdown 4.63% > 4.00%
        min_equity: equity 96546.00 < floor 96600
  10  mark AAPL 171.35          97,001.00     4.18%  BREACH max_drawdown
        max_drawdown: drawdown 4.18% > 4.00%

  rejected  buy 1000 AAPL  Insufficient funds: need 171351.00, have 79866.00

  symbol    qty   avg cost       mark   unrealized     realized
  AAPL      100     175.00     171.35      -365.00      +408.00
  MSFT     flat          -     372.00         0.00    -3,042.00

  cash              79,866.00   peak equity    101,237.00
  equity            97,001.00   drawdown           4.18%

Option chain

A Black-Scholes chain with prices and Greeks for eight strikes, then the implied-vol solver recovers the 28% input from a price.

cargo run --example option_chain
source: examples/option_chain.rs
$ cargo run --example option_chain
  chain  spot 212.40  30 days  r 4.50%  vol 28.00%

    theta   delta    call   strike       put   delta   theta    gamma   vega
   -0.079   0.875   19.21   195.00      1.09  -0.125  -0.055   0.0121  0.125
   -0.099   0.798   15.14   200.00      2.00  -0.202  -0.074   0.0165  0.171
   -0.116   0.701   11.53   205.00      3.37  -0.299  -0.090   0.0204  0.211
   -0.125   0.590    8.46   210.00*     5.28  -0.410  -0.099   0.0228  0.237
   -0.125   0.474    5.96   215.00      7.77  -0.526  -0.098   0.0233  0.242
   -0.116   0.363    4.04   220.00     10.83  -0.637  -0.089   0.0220  0.228
   -0.099   0.264    2.62   225.00     14.39  -0.736  -0.072   0.0192  0.199
   -0.080   0.183    1.63   230.00     18.38  -0.817  -0.052   0.0155  0.161

  * nearest the money. theta is per calendar day, vega per 1 vol point.

  implied vol  210 call priced at 8.4554 recovers vol 28.0000%

Guarantees you can lean on

The crate is small promises kept everywhere. These four come up in almost every trading system.

Invalid values do not construct

Price::new(dec!(-1))
→ Err(InvalidPrice(-1))  "Price must be positive, got -1"

Price rejects zero and negatives, Quantity rejects negatives, Symbol rejects empty or whitespace strings. Code holding one can trust it.

The book cannot silently drift

book.apply_delta(delta_with_seq_17)
→ "Order book sequence mismatch: expected 15, got 17"

Each delta must carry exactly the next sequence number. A delta that would cross the book is rolled back and returned as InvertedSpread.

Indicators say when they are not ready

rsi.update(&bar_1)?
→ SignalValue::Unavailable   (bars 1 to 14)

Every Signal returns Unavailable until its warm-up is met, so a chart never plots a fake zero and a strategy never trades on one.

Money math stays decimal

book.vwap_for_qty(Side::Ask, 1 BTC)?
→ 64250.579   (0.842 @ 64,250.50 + 0.158 @ 64,251.00)

Prices, quantities, P&L and book math use rust_decimal. Statistical models compute in f64 and convert at the boundary.

Architecture

Plain data in, a typed value or FinError out. Every stage works on its own: use the book without the ledger, or the indicators without the aggregator.

Data flow: ticks to TickFilter to OhlcvAggregator to Signal; BookDelta to OrderBook; Fill to PositionLedger to RiskMonitor; OptionSpec to BlackScholes; all sharing validated Price, Quantity, Symbol, NanoTimestamp and Side types

Start in two minutes

Add it

cargo add fin-primitives rust_decimal_macros

crates.io has 2.12.0. For the newest modules use the git dependency shown in the README.

Run an example

git clone https://github.com/Mattbusel/fin-primitives
cd fin-primitives
cargo run --example order_book

Four examples ship in examples/.

Read the docs

cargo doc --no-deps --open

Or browse docs.rs/fin-primitives. The crate-level page opens with a runnable example.