fin-primitives 2.14.0

Financial market primitives: validated types, order book, OHLCV aggregation, 725+ streaming technical indicators, option Greeks/Black-Scholes, backtester, async streaming signals, position ledger, and risk monitoring.
Documentation

fin-primitives

Validated, decimal-precise building blocks for trading and quantitative systems in Rust: price and quantity types that cannot hold invalid values, a sequence-checked level-2 order book, tick-to-OHLCV aggregation, streaming indicators with an explicit warm-up contract, a position ledger, risk rules, Black-Scholes Greeks and a walk-forward backtester. One error type, FinError, covers all of it.

Research and engineering library. Nothing here is a trading strategy, and nothing in this repository is financial advice. Backtest results depend entirely on your data and assumptions.

Quickstart

cargo add fin-primitives rust_decimal_macros

crates.io has 2.12.0; the main branch is 2.14.0 and some newer modules (portfolio optimizer, Kelly sizing, signal composition, factor model, yield curve, event study and others) may only be available from git:

fin-primitives = { git = "https://github.com/Mattbusel/fin-primitives" }

Build a book from sequenced deltas, read the top of book, and price a market order by walking the levels. This is the crate-level doctest, so it is compiled and run by cargo test --doc:

use fin_primitives::orderbook::{BookDelta, DeltaAction, OrderBook};
use fin_primitives::types::{Price, Quantity, Side, Symbol};
use rust_decimal_macros::dec;

fn main() -> Result<(), fin_primitives::FinError> {
    let mut book = OrderBook::new(Symbol::new("BTC-USD")?);
    let levels = [
        (Side::Ask, dec!(64250.50), dec!(0.842)),
        (Side::Ask, dec!(64251.00), dec!(1.310)),
        (Side::Bid, dec!(64250.00), dec!(1.204)),
        (Side::Bid, dec!(64249.50), dec!(0.655)),
    ];
    for (seq, (side, price, qty)) in (1..).zip(levels) {
        book.apply_delta(BookDelta {
            side,
            price: Price::new(price)?,
            quantity: Quantity::new(qty)?,
            action: DeltaAction::Set,
            sequence: seq,
        })?;
    }

    assert_eq!(book.spread(), Some(dec!(0.50)));
    assert_eq!(book.mid_price(), Some(dec!(64250.25)));

    // Buying 1 BTC takes all 0.842 at the best ask and 0.158 at the next level.
    let vwap = book.vwap_for_qty(Side::Ask, Quantity::new(dec!(1))?)?;
    assert_eq!(vwap, dec!(64250.579));

    // A delta that would cross the book is rejected and rolled back.
    let crossing = BookDelta {
        side: Side::Bid,
        price: Price::new(dec!(64251.00))?,
        quantity: Quantity::new(dec!(2))?,
        action: DeltaAction::Set,
        sequence: 5,
    };
    assert!(book.apply_delta(crossing).is_err());
    assert_eq!(book.sequence(), 4);
    Ok(())
}

Run the examples

Four examples ship in examples/. Clone and run them; the images below are their real output (colors on in a terminal, NO_COLOR=1 turns them off).

git clone https://github.com/Mattbusel/fin-primitives && cd fin-primitives

cargo run --example order_book

order_book: a BTC-USD book built from 14 deltas, printed as a depth ladder with spread, micro-price, imbalance and the VWAP of a 5 BTC market buy. Then two bad deltas: one that would cross the book (rejected and rolled back) and one with a sequence gap.

candles: 337 ticks from a seeded random walk go through OhlcvAggregator into 32 one-minute bars, drawn as candles with EMA(9). RSI(14) reports Unavailable for the first 14 bars instead of a NaN or a zero.

A PositionLedger takes fills and marks; its net_liquidation_value feeds a RiskMonitor with a 4% drawdown rule and an equity floor. The last buy is refused for insufficient cash.

Architecture

Each stage is plain data in and a typed value or FinError out, so every piece works on its own: use the order book without the ledger, or the indicators without the aggregator. The types are not internally synchronized; wrap a component in Arc<Mutex<_>> to share it across threads.

What is included

Module What it provides Key guarantee
[types] Price, Quantity, Symbol, NanoTimestamp, Side newtypes Validation at construction; no invalid value can exist at runtime
[tick] Tick, TickFilter, TickReplayer Filter is pure; replayer always yields ticks in ascending timestamp order
[orderbook] L2 OrderBook with apply_delta, spread, mid-price, VWAP, top-N levels Sequence validation; inverted spreads are detected, logged, and rolled back
[ohlcv] OhlcvBar, Timeframe, OhlcvAggregator, OhlcvSeries (360+ analytics) Bar invariants (high >= low, etc.) enforced on every push
[signals] Signal trait, SignalPipeline, 725+ built-in indicators, SignalMap (60+ methods), CompositeSignal, SignalExpr composition DSL, WarmupGuard Returns Unavailable until warm-up period is satisfied; no silent NaN
[position] Position, Fill, PositionLedger (110+ methods) VWAP average cost; realized and unrealized P&L net of commissions
[risk] DrawdownTracker (90 methods), RiskRule trait, RiskMonitor, RiskAttributor (6-factor), BhbAttribution All breaches returned as a typed Vec<RiskBreach>; never silently swallowed
[greeks] BlackScholes, OptionGreeks, OptionSpec, SpreadGreeks All math returns Result<T, FinError>; no panics on edge-case inputs
[backtest] Backtester, Strategy trait, BacktestResult, WalkForwardOptimizer, WfPeriod, ParamRange Bar-by-bar; no look-ahead; grid-search walk-forward with OOS stability score
[async_signals] StreamingSignalPipeline, SignalUpdate, spawn_signal_stream Tokio MPSC; pre-allocated output buffers on the hot path
[regime] RegimeDetector, MarketRegime, Garch11, CorrelationBreakdownDetector, RegimeConditionalSignal, RegimeHistory Hurst + GARCH(1,1) + cross-asset correlation breakdown; regime-adaptive RSI

Additional modules in src/ (see docs.rs or the source for their APIs): pnl, correlation, latency, scenario, ml, ml_features, cross_asset, attribution, options, volatility, impact, portfolio, factor, execution, execution_cost, montecarlo, yield_curve, events, microstructure, crypto, derivatives, technical, fixed_income, credit, liquidity, pairs_trading, performance, clustering, tax, rebalancing, funding, alternative_data, arbitrage. Optional Python bindings are behind the python feature (maturin develop --features python).

Design principles

  • Typed errors. Fallible operations return Result<_, FinError>. The crate's Clippy config warns on unwrap, expect and panic.
  • Decimal precision. Prices and quantities use [rust_decimal::Decimal], so price arithmetic has no binary floating-point rounding. Statistical models (GARCH, Monte Carlo, portfolio optimization) work in f64.
  • Nanosecond timestamps. NanoTimestamp is a newtype over i64 nanoseconds since Unix epoch, suitable for microsecond-accurate event ordering and replay.
  • Composable by design. RiskRule, Signal, and TickFilter are traits; plug in your own implementations without forking.
  • Separation of concerns. Each module has a documented responsibility contract and an explicit "NOT Responsible For" section.

Module guides

Detailed notes per subsystem, collapsed by default. Counts in headings (for example "725+") are the approximate number of indicator types or public methods in that module.

All indicators implement the Signal trait and return SignalValue::Unavailable until warm-up is satisfied. No silent NaN or panic.

Trend / Moving Averages

Sma, Ema, Dema, Tema, Wma, HullMa, Alma, Smma, Zlema, T3, Trima, Kama, Lsma, Vidya, Swma, McGinley, LinRegSlope, Frama, DemaRatio, DemaCross, EmaCross, EmaSlope, EmaConvergence, TypicalPriceMa, TrueRangeEma, CoralTrend, HalfTrend, MesaAdaptiveMa, JurikMa, ChandeKrollStop, EmaRatio, EmaAlignment, EmaBandWidth, SmaDistancePct, TrendMagic, AdaptiveSupertrend, RollingVwap

Momentum / Oscillators

Rsi, Macd, Cci, Roc, Momentum, Apo, Ppo, Cmo, Tsi, Rvi, StochasticK, StochasticD, StochRsi, StochRsiSmoothed, WilliamsR, UltimateOscillator, Coppock, Kst, Trix, Dpo, Pgo, Rmi, Cog, Pfe, ConnorsRsi, DualRsi, RsiMa, RsiDivergence, SmoothedRsi, AdaptiveRsi, RsiStochastic, VolumeWeightedRsi, Qqe, Pmo, Tii, AwesomeOscillator, Smi, Ctm, PriceMomentumOscillator, MomentumOscillator, DeltaMomentum, CumReturnMomentum, NormalizedMomentum, MomentumQuality, MomentumReversal, MomentumStreak, MomentumDivergence, MomentumConsistency, UpMomentumPct, BodyMomentum, SlopeOscillator, EhlersCyberCycle, ChandeForecastOsc, ChandeMomentumSmoothed, DynamicMomentumIndex

Volatility

Atr, Natr, BollingerB, BollingerPctB, BollingerWidth, KeltnerChannel, DonchianMidpoint, DonchianWidth, Vhf, ChoppinessIndex, HistoricalVolatility, RelativeVolatility, ChaikinVolatility, VolatilityRatio, VolatilityBands, VolatilityAdjustedMomentum, VolatilitySkew, StdDevChannel, LinRegChannel, Inertia, Stiffness, TtmSqueeze, VolatilityOfVolatility, VolatilityBreak, VolatilityMomentum, VolatilityPercentile, VolatilityRegimeDetector, VolatilitySpike, VolatilityStop, RegimeVolatility, LogReturnVolatility, WeightedCloseVolatility, AccelerationBands, AtrPercent, AtrNormalizedClose, AtrRatio, DualATRRatio, WilderSmoothedRange, TrueRangeExpansion, TrueRangePercentile, TrueRangeZScore, TrueRangeRatio

Volume

Cmf, Obv, Mfi, Vwap, Vwma, Pvo, Emv, Kvo, Vpt, Nvi, ChaikinOsc, ForceIndex, NetVolume, VolumeRsi, VolumeSpike, VolumeTrend, VolumeOscillator, VolumeImbalance, Vroc, ObvMomentum, ClimaxVolume, BwMfi, Vzo, VwMomentum, VolumeBreadth, VolumeAcceleration, VolumeWeightedClose, VolumeAccumulation, VolumeDeltaOscillator, VolumeToRangeRatio, VolumeRateOfChange, VolumeSpikeRatio, VolumeSpikeScore, VolumeReturnCorrelation, VolumeTrendSlope, VolumePriceEfficiency, VolumePriceCorr, VolumePriceImpact, VolumeDirectionRatio, VolumeEnergy, VolumeExhaustion, VolumeFlowRatio, VolumeDensity, VolumeDeviation, VolumeClimaxRatio, VolumeMomentum, VolumeMomentumDivergence, VolumeOpenBias, VolumePerRange, VolumeRatioSignal, VolumeSurge, VolumeSurge2, VolumeUpDownRatio, VolumeWeightedAtr, VolumeWeightedRange, VolumeWeightedStdDev, VolumeWeightedMomentum, UpVolumeFraction, UpVolumeRatio, UpDownVolumeRatio, NegativeVolumeIndex, PositiveVolumeIndex, RelativeVolumeRank, RelativeVolumeScore, NormalizedVolume, MedianVolume, CumulativeVolume, CumulativeDelta, ConsecutiveVolumeGrowth, VolumeStreakCount, RollingVolumeCV, DeltaVolume

Trend Direction / Multi-component

Adx, Dmi, Aroon, AroonOscillator, Ichimoku, ParabolicSar, SuperTrend, ElderRay, ElderImpulse, ChandelierExit, Stc, Vortex, WilliamsAD, GannHiLo, TrendFollowingFilter, TrendStrength, TrendAngle, TrendScore, Alligator, Rwi, TrendAge, TrendConsistency, TrendConsistencyScore, TrendPersistence, TrendPurity, MarketRegimeFilter, NetHighLowCount, BullBearBalance, TdSequential, WilliamsFractal, KeyReversal

Price Structure / Pattern

PriceChannel, PriceCompression, PriceDistanceMa, PriceGap, PriceIntensity, PriceOscillator, PriceOscillator2, PricePosition, PriceRangePct, PriceAboveMa, PriceAboveMaPct, PriceAcceleration, PriceVelocity, PriceVelocityRatio, PriceVelocityScore, PriceEnvelope, PriceReversal, PriceReversalStrength, NormalizedPrice, DisparityIndex, DeviationFromMa, LinearDeviation, PriceDensity, CandleBodySize, CandleColor, CandleMomentum, CandlePattern, HeikinAshi, WickRatio, HighLowPct, HighLowPctRange, HighLowSpread, HlRatio, OpenCloseRatio, CloseToOpen, CloseLocationValue, WeightedClose, CloseToOpenGap, CloseToOpenReturn, HighLowReturnCorrelation, UpperWickPct, LowerWickPct, HigherHighLowerLow, OpenHighLowCloseAvg, CloseToLowDistance, ReturnMeanDeviation, PriceAboveRollingHigh, OpenCloseSpread, GapFillRatio, PriceCompressionRatio, ShadowRatio, PriceMeanDeviation, AbsReturnSum, AbsReturnMean, RollingMaxDrawdown, PriceRelativeStrength, OpenLowRange, HighOpenRange, BodyAtrRatio, GapStreak, BarEfficiency, MedianBodySize, WickAsymmetryStreak, FibonacciRetrace, PriceEntropyScore, PriceCompressionIndex, PriceCompressionBreakout, PriceSymmetry, PricePathEfficiency, PriceEfficiencyRatio, PriceGravity, PriceImpulse, PriceBandwidth, PriceLevelPct, PricePositionRank, PriceRangeExpansion, PriceRangeRank, PriceToSmaRatio, PriceZScore, PriceOscillatorPct, PriceOscillatorSign, PriceChangeCount, PriceChangePct, PriceChannelPosition, PriceChannelWidth, PriceGapFrequency, OpenToHighRatio, RangeMomentum, RangePersistence, RangeReturnRatio, RangeCompressionRatio, RangeContractionCount, RangeExpansionIndex, RangeMidpointPosition, RangePctOfClose, RangeTrendSlope, RangeZScore, RangeEfficiency, RangeBreakoutCount, RangeReturnRatio, CloseMidpointDiff, CloseMidpointStrength, CloseAboveMidpoint, CloseVsOpenRange, CloseVsPriorHigh, CloseVsVwap, ClosePositionInRange, CloseRetracePct, CloseReturnAcceleration, CloseReturnZ, CloseToHighRatio, CloseToMidRange, CloseToRangeTop, CloseRelativeToEma, CloseRelativeToRange, ClosePctFromHigh, ClosePctFromLow, CloseAboveEma, CloseAboveOpen, CloseAbovePrevClose, CloseAbovePrevClosePct, CloseAbovePrevHigh, CloseAbovePriorClose, CloseAboveSmaStreak, CloseAboveHighPrev, CloseBelowLowPrev, CloseDistanceFromEma, CloseDistanceFromOpen, CloseHighFrequency, CloseMinusOpenMa, CloseOpenEma, CloseAcceleration, CloseAccelerationSign, OpenAbovePrevClose, OpenCloseMomentum, OpenGapDirection, OpenGapPct, OpenGapSize, OpenHighRatio, OpenRangeStrength, OpenToCloseRatio, OpenToCloseReturn, OpenCloseSymmetry, OpenDrive, OpenMidpointDeviation, OvernightReturn, IntrabarReturn, HighBreakCount, HigherCloseStreak, HigherHighCount, HigherLowCount, HigherLowStreak, HighLowCrossover, HighLowDivergence, HighLowMidpoint, HighLowOscillator, HighOfPeriod, LowOfPeriod, LowerHighCount, LowerHighStreak, LowerLowCount, LowerShadowRatio, UpperShadowRatio, UpperToLowerWick, ShadowImbalance, WickImbalance, WickToAtrRatio, WickToBodyRatio, WickRejectionScore, BodyDirectionRatio, BodyFillRatio, BodyHeightRatio, BodySizeRank, BodyStreak, BodyToRangeRatio, BarCloseRank, BarFollowThrough, BarMomentumIndex, BarMomentumScore, BarOpenPosition, BarOverlapRatio, BarRangeConsistency, BarRangeExpansionPct, BarRangeStdDev, BarStrengthIndex, BarType, BearishBarRatio, BodyPosition, BodyToShadowRatio, HighVolumeBarRatio, CandleEfficiency, CandleRangeMa, CandleSymmetry, FlatBarPct, NarrowRangeBar, UpBarRatio, NetBarBias, ThreeBarPattern, EngulfingDetector, EngulfingPattern, HammerDetector, HammerPattern, DojiDetector, InsideBarCounter, InsideBarRatio, OutsideBarCount

Statistical / Adaptive

StdDev, PercentRank, Fisher, MassIndex, PsychologicalLine, KaufmanEr, Zscore, Bop, Atrp, Envelope, Pivots, PivotDistance, PivotPoint, PivotStrength, SupportResistanceDistance, AtrStop, ChangeFromHigh, BarsSince, ConsecutiveBars, SwingIndex, Dsp, Usm, Vam, LinRegR2, UlcerIndex, MeanReversionScore, MaxDrawdownWindow, MaxAdverseExcursion, MaxDrawupWindow, RangeFilter, RangeRatio, GapDetector, GapFillDetector, GapMomentum, GapRangeRatio, GapSignal, SignedGapSum, AverageGap, AnchoredVwap, LaguerreRsi, BullBearPower, BullPowerBearPower, VixFix, RocRatio, TypicalPriceDeviation, MedianPrice, MedianCloseDev, MedianReturnDeviation, RollingMAD, RollingKurtosis, RollingSkewness, RollingReturnKurtosis, RollingSkewReturns, RollingMaxReturn, RollingMinReturn, RollingCorrelation, RollingHighLowPosition, RollingHighLowRatio, RollingLowBreak, RollingOpenBias, RollingMaxDd, ReturnAutoCorrelation, ReturnDispersion, ReturnIqr, ReturnPersistence, ReturnSignChanges, ReturnSignSum, ReturnAboveZeroPct, ReturnOverVolatility, ReturnPercentRank, CumulativeLogReturn, DailyReturnSkew, DirectionChanges, DirectionalEfficiency, EfficiencyRatio, DownsideDeviation, EaseOfMovement, FairValueGap, HurstExponent, AverageBarRange, AverageGain, AverageLoss, AmplitudeRatio, Zscore, ZigZag, ValueAtRisk5, ConditionalVar5, PayoffRatio, ProfitFactor, VarianceRatio, ConsolidationScore, SupportTestCount, CusumPriceChange, NewHighPct, NewHighStreak, NewLowPct, RelativeBarRange, RelativeClose, TailRatio, TailRatioPct, BreakoutSignal, MidpointOscillator, IntradaySpreadPct, OhlcSpread, RollingShadowBalance, AtrPercentile

Core formulas:

Indicator Formula Warm-up bars
SMA(n) sum(close, n) / n n
EMA(n) close × k + prev × (1−k), k = 2/(n+1) n
RSI(n) 100 − 100 / (1 + avg_gain / avg_loss) Wilder smoothing n + 1
ATR(n) Wilder-smoothed true range n
MACD(f,s,sig) EMA(f) − EMA(s); signal = EMA(sig) of MACD slow + signal
Fibonacci(n) Swing high/low over n bars; 0/23.6/38.2/50/61.8/100% levels n
VolumeReturnCorrelation(n) Pearson r between close return and volume n + 1
PriceEntropyScore(n) Shannon entropy of up/flat/down bins, normalized to [0,1] n + 1
VolatilityOfVolatility(n) Std dev of rolling ATR values 2n − 1

OhlcvSeries ships an extensive built-in analytics library. A selection:

Returns & Volatility: realized_volatility, rolling_sharpe, hurst_exponent, ulcer_index, cvar, skewness, kurtosis, autocorrelation, std_dev, close_returns, log_returns, drawdown_series, max_drawdown, max_drawdown_pct

Volume: vwap, vwap_deviation, volume_price_correlation, relative_volume, volume_spike, up_down_volume_ratio, net_volume, volume_weighted_return, close_above_vwap_pct, volume_coefficient_of_variation, avg_volume_on_up_bars, avg_volume_on_down_bars

Momentum & Trend: close_momentum, price_velocity, price_acceleration, close_momentum_ratio, recent_close_trend, trend_strength, trend_consistency, momentum_score, close_above_ma_streak, bars_above_ma, bars_above_sma

Candle Patterns: count_doji, pct_doji, bullish_engulfing_count, bearish_engulfing_count, is_hammer, is_shooting_star, is_marubozu, inside_bar_count, outside_bar_count, candle_symmetry, candle_color_changes

Range & Structure: atr_series, true_range_series, high_low_range, price_contraction, range_expansion_ratio, close_distance_from_high, pct_from_low, is_breakout_up, reversal_count, open_gap_fill_rate, pivot_highs, pivot_lows

Streaks: consecutive_higher_closes, consecutive_higher_highs, consecutive_lower_lows, longest_winning_streak, longest_losing_streak, longest_flat_streak, bars_since_new_high, bars_since_new_low

SignalValue carries a scalar or Unavailable and propagates unavailability through every operation:

sv.abs() / sv.negate() / sv.signum()
sv.clamp(lo, hi)                   // clamp to [lo, hi]
sv.cap_at(max) / sv.floor_at(min)  // one-sided clamps
sv.lerp(other, t)                  // linear interpolation, t ∈ [0, 1]
sv.blend(other, weight)            // weighted blend
sv.quantize(step)                  // round to nearest multiple of step
sv.distance_to(other)              // absolute difference
sv.delta(prev)                     // signed change
sv.cross_above(prev, threshold)    // true on upward threshold cross
sv.within_range(lo, hi)            // boolean range test
sv.as_percent() / sv.pct_of(base)  // percentage helpers
sv.sign_match(other)               // true if same sign
sv.map(f) / sv.zip_with(other, f)  // functor / applicative style

SignalMap is the output of SignalPipeline::update. Fleet-wide analytics:

map.average_scalar()          // mean of all scalar values
map.std_dev() / .variance()   // dispersion
map.z_scores()                // HashMap<String, f64> z-score per signal
map.entropy()                 // Shannon entropy of the distribution
map.gini_coefficient()        // Gini inequality coefficient
map.normalize_all()           // min-max normalize all scalars to [0, 1]
map.top_n(3) / .bottom_n(3)   // top/bottom signals by value
map.weighted_sum(&weights)    // dot product with weight map
map.scale_all(factor)         // multiply all scalars by factor
map.percentile_rank_of(name)  // percentile of one signal among all
map.signal_ratio(a, b)        // ratio of two named signals
map.count_positive() / .count_negative() / .count_zero()
map.all_positive() / .all_negative()

Every indicator has an implicit warmup period. signals::warmup makes that period queryable, enforceable, and reportable:

Type Purpose
WarmupContract Trait: warmup_period(), is_ready(), bars_remaining(), implemented by all Signal types
WarmupGuard<S> Wraps any signal; returns Err(NotReady) until warmup completes instead of silent Unavailable
WarmupReporter Tracks warmup progress for N signals and produces WarmupReport snapshots
WarmupReport all_ready(), pipeline_bars_remaining(), warming_signals(), display()
use fin_primitives::signals::indicators::Sma;
use fin_primitives::signals::{BarInput, Signal};
use fin_primitives::signals::warmup::{WarmupContract, WarmupGuard, WarmupReporter};
use rust_decimal_macros::dec;

// Guard: explicit error instead of silent Unavailable
let sma = Sma::new("sma5", 5).unwrap();
let mut guard = WarmupGuard::new(sma);

for _ in 0..4 {
    let bar = BarInput::from_close(dec!(100));
    // Err(NotReady { bars_remaining: 4, 3, 2, 1 })
    assert!(guard.update_checked(&bar).is_err());
}
// 5th bar, Ok(SignalValue::Scalar(...))
assert!(guard.update_checked(&BarInput::from_close(dec!(100))).is_ok());

// Reporter: pipeline-level warmup snapshot
let mut reporter = WarmupReporter::new(
    vec![5, 14, 20],
    vec!["sma5".into(), "rsi14".into(), "bb20".into()],
);
reporter.tick_n(5); // 5 bars consumed
let report = reporter.report(reporter.bars_consumed());
assert!(report.statuses[0].is_ready);   // sma5 done
assert!(!report.statuses[1].is_ready);  // rsi14 still warming
println!("{}", report.display());
// WarmupReport [bars_consumed=5, ready=1/3, pipeline_remaining=15]
//   [READY]   sma5  (period=5)
//   [WARMING] rsi14 (period=14, remaining=9)
//   [WARMING] bb20  (period=20, remaining=15)

signals::compose provides a composable expression-tree DSL for building derived signals from existing indicators without writing bespoke structs.

Expression Nodes

Node Description
Raw(name) Leaf: raw output of a named indicator
Add(a, b) Element-wise sum; Unavailable if either is
Sub(a, b) a - b; Unavailable if either is
Mul(expr, f) Scale by constant f
Lag(expr, n) Delay by n bars; Unavailable until buffer fills
Normalize(expr, method, window) MinMax, ZScore, or Percentile normalisation
Threshold(expr, level, dir) Above → +1/0, Below → -1/0, Cross → +1/-1/0

Fluent SignalBuilder API

use fin_primitives::signals::indicators::Rsi;
use fin_primitives::signals::{BarInput, Signal};
use fin_primitives::signals::compose::{SignalBuilder, NormMethod, Direction};
use rust_decimal_macros::dec;

// RSI(14) → lag(1) → ZScore(20) → threshold(+2σ, Above)
let rsi = Rsi::new("rsi14", 14).unwrap();
let mut momentum_signal = SignalBuilder::new(rsi)
    .lag(1)
    .normalize_window(NormMethod::ZScore, 20)
    .threshold(dec!(2), Direction::Above)
    .build_named("rsi_zscore_cross");

// Feed bars; signal returns Scalar(1) when RSI z-score crosses above +2σ
let bar = BarInput::from_close(dec!(100));
let _ = momentum_signal.update(&bar); // Unavailable during warmup

// Combine two signals manually
use fin_primitives::signals::compose::{SignalExpr, ComposedSignal};
use fin_primitives::signals::indicators::Sma;

let sma_fast = Sma::new("sma5", 5).unwrap();
let sma_slow = Sma::new("sma20", 20).unwrap();
let expr = SignalExpr::raw("sma5")
    .sub(SignalExpr::raw("sma20"))      // MACD-style crossover
    .threshold(dec!(0), Direction::Cross);
let leaves: Vec<Box<dyn Signal>> = vec![Box::new(sma_fast), Box::new(sma_slow)];
let mut cross = ComposedSignal::new("fast_slow_cross", expr, leaves).unwrap();

Normalisation Methods

Method Formula Output
MinMax (v - min) / (max - min) [0, 1]
ZScore (v - mean) / std_dev Unbounded; typically [-3, +3]
Percentile Rank within rolling window [0, 1]
ledger.net_liquidation_value(&prices)       // cash + market value of open positions
ledger.equity(&prices)                      // cash + unrealized P&L (not account value)
ledger.total_unrealized_pnl(&prices)        // sum of all open position P&L
ledger.concentration_ratio()               // Herfindahl-Hirschman Index
ledger.long_exposure() / .short_exposure()  // directional gross exposure
ledger.avg_long_entry_price()               // VWAP of long entries
ledger.avg_short_entry_price()              // VWAP of short entries
ledger.pct_long() / .pct_short()            // directional balance
ledger.win_rate()                           // % of closed positions with positive P&L
ledger.largest_position() / .smallest_position()
ledger.symbols_with_unrealized_loss(&prices)
tracker.current_drawdown_pct()      // (peak − equity) / peak × 100
tracker.worst_drawdown_pct()        // worst drawdown seen
tracker.calmar_ratio()              // annualized return / max drawdown
tracker.sharpe_ratio()              // using per-update equity changes
tracker.sortino_ratio()             // downside-deviation adjusted
tracker.win_rate()                  // fraction of updates that gained equity
tracker.avg_gain_pct()              // average gain per gaining update
tracker.avg_loss_pct()              // average loss per losing update
tracker.equity_change_std()         // std dev of per-update equity changes
tracker.gain_loss_asymmetry()       // ratio of avg gain magnitude to avg loss magnitude
tracker.recovery_factor()           // net return / max drawdown
tracker.omega_ratio()               // probability-weighted gain/loss ratio
tracker.equity_multiple()           // current / initial equity
tracker.return_drawdown_ratio()     // net return % / worst drawdown %
tracker.streak_win_rate()           // max_gain_streak / total streak length
tracker.time_to_recover_est()       // estimated updates to recover from current drawdown

risk::attribution decomposes portfolio risk into six named factors and supports Brinson-Hood-Beebower P&L attribution. Use RiskMonitor::attribution_report or construct RiskAttributor directly.

Six-Factor Risk Decomposition

Factor Driver Estimation
Market Systematic beta exposure β² × σ²_market
Sector Industry concentration HHI × σ²_market × 0.5
Idiosyncratic Stock-specific residual Σ w_i² × σ²_idio_i
Leverage Borrowed capital amplification (L−1)² × σ²_market
Concentration Large single-position weight HHI × σ²_market × 0.3
Liquidity Illiquid exit risk (1 − avg_liquidity) × σ²_market
use fin_primitives::risk::RiskMonitor;
use fin_primitives::risk::attribution::{MarketData, RiskAttributor};
use fin_primitives::position::PositionLedger;
use rust_decimal_macros::dec;

let ledger = PositionLedger::new(dec!(100_000));
// Populate ledger with positions via ledger.apply_fill(...)

let market_data = MarketData::new(0.15)   // 15% annualised market vol
    .with_beta("AAPL", 1.2)
    .with_beta("MSFT", 0.9)
    .with_sector("AAPL", "Technology")
    .with_sector("MSFT", "Technology")
    .with_liquidity("AAPL", 0.98)
    .with_idio_vol("AAPL", 0.25);

// Via RiskMonitor (one-liner)
let monitor = RiskMonitor::new(dec!(100_000));
let report = monitor.attribution_report(&ledger, market_data.clone());

println!("{}", report.summary());
// AttributionReport [equity=..., total_risk=..., beta=1.05, hhi=0.5, leverage=1.0x]
//   Market (Beta)        42.3%  (...)
//   Sector               18.1%  (...)
//   Idiosyncratic        27.4%  (...)
//   ...

// Or directly via RiskAttributor
let attributor = RiskAttributor::new(&ledger, market_data);
let report = attributor.compute();
let dominant = report.dominant_factor().unwrap();
println!("Largest risk factor: {}", dominant.factor.name());

Brinson-Hood-Beebower P&L Attribution

use fin_primitives::risk::attribution::{RiskAttributor, BhbInput, BhbSectorInput, MarketData};
use fin_primitives::position::PositionLedger;
use rust_decimal_macros::dec;

let ledger = PositionLedger::new(dec!(100_000));
let attributor = RiskAttributor::new(&ledger, MarketData::default());

let bhb = attributor.compute_bhb(&BhbInput {
    benchmark_total_return: 0.05,
    sectors: vec![
        BhbSectorInput {
            sector: "Technology".into(),
            portfolio_weight:   0.60,  // overweight vs benchmark
            benchmark_weight:   0.40,
            portfolio_sector_return: 0.08,
            benchmark_sector_return: 0.06,
        },
        BhbSectorInput {
            sector: "Energy".into(),
            portfolio_weight:   0.40,
            benchmark_weight:   0.60,
            portfolio_sector_return: 0.02,
            benchmark_sector_return: 0.04,
        },
    ],
});

println!("Active return: {:.2}%", bhb.total_active_return * 100.0);
println!("  Allocation:   {:.4}", bhb.total_allocation);
println!("  Selection:    {:.4}", bhb.total_selection);
println!("  Interaction:  {:.4}", bhb.total_interaction);

The greeks module provides a European option pricing engine whose functions return Result instead of panicking.

use fin_primitives::greeks::{BlackScholes, OptionSpec, OptionType, SpreadGreeks};
use rust_decimal_macros::dec;

fn main() -> Result<(), fin_primitives::FinError> {
    let spec = OptionSpec {
        strike:          dec!(100),
        expiry_days:     30,
        spot:            dec!(100),
        risk_free_rate:  dec!(0.05),
        volatility:      dec!(0.20),
        option_type:     OptionType::Call,
    };

    // Theoretical price
    let price = BlackScholes::price(&spec)?;

    // All five Greeks
    let g = BlackScholes::greeks(&spec)?;
    println!("delta={} gamma={} theta={} vega={} rho={}", g.delta, g.gamma, g.theta, g.vega, g.rho);

    // Implied volatility from a market quote
    let iv = BlackScholes::implied_vol(price, &spec)?;

    // Multi-leg spreads
    let straddle = SpreadGreeks::straddle(dec!(100), dec!(100), 30, dec!(0.05), dec!(0.20));
    let net = straddle.net_greeks()?;
    println!("straddle net delta ≈ {}", net.delta);
    Ok(())
}

Spread constructors:

Constructor Description
SpreadGreeks::bull_call_spread(…) Long low-strike call, short high-strike call
SpreadGreeks::bear_put_spread(…) Long high-strike put, short low-strike put
SpreadGreeks::straddle(…) Long ATM call + long ATM put
SpreadGreeks::iron_condor(…) Short put spread + short call spread
SpreadGreeks::new(legs) Arbitrary legs with signed quantities

Formulas:

Greek Formula
delta ∂V/∂S (N(d₁) call, N(d₁)−1 put)
gamma φ(d₁) / (S σ √T)
theta −(S φ(d₁) σ) / (2√T) ± r K e^{−rT} N(±d₂), per day
vega S φ(d₁) √T / 100 (per 1 vol-point)
rho ±K T e^{−rT} N(±d₂) / 100 (per 1 rate-point)

Implied vol is solved by bisection over [1e-6, 5.0] (up to 200 iterations, tolerance 1e-7).

The regime module classifies the current market state using four complementary quantitative signals, then adapts strategy parameters per regime.

Regimes

Regime Primary Signal Condition
Trending Hurst exponent H > 0.6 (persistent process)
MeanReverting Hurst exponent H < 0.4 (anti-persistent)
HighVolatility Realized vol / long-run mean ratio > 2.0x; GARCH confirms
LowVolatility Realized vol / long-run mean ratio < 0.5x; BB width compressed
Crisis Cross-asset correlation > 60% of pairs decorrelated simultaneously
Neutral , No dominant signal
Unknown , Warm-up phase incomplete

Priority order: Crisis > HighVolatility > Trending > MeanReverting > LowVolatility > Neutral

GARCH(1,1) Persistent Volatility

The Garch11 struct fits an online GARCH(1,1) model, ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁, and flags when conditional vol exceeds the long-run level by a configurable multiplier. This catches regimes where volatility is structurally elevated (crisis, rate shock) rather than just transiently spiked.

use fin_primitives::regime::{Garch11, RegimeDetector, RegimeConfig, MarketRegime, RegimeConditionalSignal};
use fin_primitives::signals::BarInput;
use rust_decimal_macros::dec;

// ── Standalone GARCH ──────────────────────────────────────────────────────────
let mut garch = Garch11::new(0.1, 0.85, 1e-6).unwrap();
let log_returns = [-0.01_f64, 0.02, -0.03, 0.015, -0.025];
for ret in log_returns {
    let sigma = garch.update(ret);
    println!("σ = {sigma:.6}  elevated = {}", garch.is_vol_elevated(1.5));
}
println!("long-run σ = {:.6}", garch.long_run_sigma());

// ── Full regime detector ──────────────────────────────────────────────────────
let mut detector = RegimeDetector::new(14, RegimeConfig::default()).unwrap();

let bars = vec![
    BarInput::new(dec!(100), dec!(102), dec!(98), dec!(100), dec!(5_000)),
    // ... more bars
];

for bar in &bars {
    let (regime, confidence) = detector.update(bar, &[]).unwrap();
    // cross_returns: &[(asset_idx, log_return)] for multi-asset crisis detection
    // e.g. detector.update(bar, &[(1, -0.02), (2, 0.01)]).unwrap()

    println!("[{}] regime = {regime}  confidence = {confidence:.2}  risk_off = {}",
        bar.close, regime.short_code(), regime.is_risk_off());
}

// Inspect regime history
for epoch in detector.history() {
    println!("  {:?}  started_at={} confidence={:.2} duration={:?}",
        epoch.regime, epoch.started_at_bar, epoch.confidence, epoch.duration_bars());
}

Regime-Conditional Signal Adaptation

RegimeConditionalSignal applies RSI with a short period in trending markets, a longer period when mean-reverting, and suppresses the signal entirely in crisis:

use fin_primitives::regime::{RegimeConditionalSignal, MarketRegime};
use fin_primitives::signals::BarInput;
use rust_decimal_macros::dec;

let mut signal = RegimeConditionalSignal::new(
    14,  // RSI period in Trending regime
    21,  // RSI period in MeanReverting regime
    14,  // RSI period in all other non-risk-off regimes
).unwrap();

let bar = BarInput::new(dec!(100), dec!(102), dec!(98), dec!(100), dec!(1000));

match signal.update(&bar, MarketRegime::Trending) {
    Some(Ok(rsi)) => println!("RSI(14) in trending = {rsi:.2}"),
    Some(Err(e))  => eprintln!("error: {e}"),
    None          => println!("signal suppressed (warm-up or risk-off)"),
}
// Crisis/Unknown → None (flat signal, no trading)
assert!(signal.update(&bar, MarketRegime::Crisis).is_none());

RegimeConfig Thresholds

RegimeConfig {
    hurst_trending:              0.6,   // H > 0.6 → Trending
    hurst_mean_reverting:        0.4,   // H < 0.4 → MeanReverting
    vol_high_multiplier:         2.0,   // realized vol > 2x long-run mean → HighVolatility
    vol_low_multiplier:          0.5,   // realized vol < 0.5x long-run mean → LowVolatility
    adx_trend_threshold:        25.0,   // ADX above this confirms trend
    bb_width_quiet:             0.02,   // BB width below this confirms LowVolatility
    crisis_correlation_threshold: 0.3, // |r| below this = decorrelated pair
    crisis_pair_fraction:        0.6,  // 60%+ of pairs decorrelated → Crisis
    garch_alpha:                 0.1,  // GARCH innovation weight
    garch_beta:                  0.85, // GARCH persistence weight
    garch_omega:                 1e-6, // GARCH long-run floor
    garch_vol_multiplier:        1.5,  // GARCH sigma multiplier for high-vol flag
}

The backtest module provides a bar-by-bar event-driven backtester and a rolling walk-forward optimizer.

use fin_primitives::backtest::{
    Backtester, BacktestConfig, Signal, SignalDirection, Strategy, WalkForwardOptimizer,
};
use fin_primitives::ohlcv::OhlcvBar;
use rust_decimal_macros::dec;

// 1. Implement the Strategy trait
struct MomentumStrategy { last_close: Option<rust_decimal::Decimal> }

impl Strategy for MomentumStrategy {
    fn on_bar(&mut self, bar: &OhlcvBar) -> Option<Signal> {
        let close = bar.close.value();
        let dir = match self.last_close {
            Some(prev) if close > prev => SignalDirection::Buy,
            Some(prev) if close < prev => SignalDirection::Sell,
            _ => SignalDirection::Hold,
        };
        self.last_close = Some(close);
        Some(Signal::new(dir, dec!(1)))
    }
}

fn main() -> Result<(), fin_primitives::FinError> {
    // 2. Configure and run
    let config = BacktestConfig::new(dec!(100_000), dec!(0.001))?;
    let bars: Vec<OhlcvBar> = vec![/* … */];
    let result = Backtester::new(config.clone()).run(&bars, &mut MomentumStrategy { last_close: None })?;

    println!("total_return={:.2}%  sharpe={:.2}  max_dd={:.2}%  trades={}",
        result.total_return * dec!(100),
        result.sharpe_ratio,
        result.max_drawdown * dec!(100),
        result.trade_count);

    // 3. Walk-forward optimization
    let wfo = WalkForwardOptimizer::new(200, 50, config)?;
    let wf = wfo.run(&bars, |_train| Box::new(MomentumStrategy { last_close: None }))?;
    println!("mean OOS return={:.2}%  worst dd={:.2}%",
        wf.mean_return * dec!(100), wf.worst_drawdown * dec!(100));
    Ok(())
}

BacktestResult fields:

Field Description
total_return (final_equity − initial_capital) / initial_capital
sharpe_ratio Annualised Sharpe (252-day, sample stddev), 0 if flat returns
max_drawdown Peak-to-trough fraction, always in [0, 1]
win_rate Fraction of closed trades with positive realized P&L
trade_count Total fills executed
equity_curve Vec<Decimal> sampled once per bar

The backtest::walk_forward module provides proper out-of-sample validation via a rolling train/test split with parameter grid search.

Algorithm

|────── train ──────|── test ──|
       step ──►
               |────── train ──────|── test ──|

For each window:

  1. Grid search over all parameter combinations on the training slice.
  2. Select parameters that maximize in-sample Sharpe ratio.
  3. Evaluate those parameters on the held-out test slice.
  4. Record WfPeriod with both IS and OOS metrics.

Aggregate: aggregate_sharpe = mean(OOS Sharpe), stability_score = fraction of OOS windows with positive Sharpe.

Example

use fin_primitives::backtest::walk_forward::{WalkForwardOptimizer, WalkForwardConfig, ParamRange};
use fin_primitives::backtest::{BacktestConfig, Signal, SignalDirection, Strategy};
use fin_primitives::ohlcv::OhlcvBar;
use std::collections::HashMap;
use rust_decimal_macros::dec;

// ── Define a parametric strategy ─────────────────────────────────────────────
struct SmaStrategy { period: usize, bar_count: usize, window: std::collections::VecDeque<rust_decimal::Decimal> }

impl Strategy for SmaStrategy {
    fn on_bar(&mut self, bar: &OhlcvBar) -> Option<Signal> {
        let close = bar.close.value();
        self.window.push_back(close);
        if self.window.len() > self.period { self.window.pop_front(); }
        if self.window.len() < self.period { return None; }
        let sma: rust_decimal::Decimal = self.window.iter().sum::<rust_decimal::Decimal>()
            / rust_decimal::Decimal::from(self.period);
        let dir = if close > sma { SignalDirection::Buy } else { SignalDirection::Sell };
        Some(Signal::new(dir, dec!(1)))
    }
}

// ── Configure the optimizer ───────────────────────────────────────────────────
let config = WalkForwardConfig {
    train_window: 120,  // 120 bars for in-sample fitting
    test_window:   30,  // 30 bars for out-of-sample evaluation
    step:          30,  // advance by 30 bars each iteration
    param_space: vec![
        ParamRange { name: "sma_period".to_owned(), min: 5.0, max: 25.0, step: 5.0 },
    ],
};

let bt_config = BacktestConfig::new(dec!(100_000), dec!(0.001)).unwrap();
let optimizer = WalkForwardOptimizer::new(config, bt_config).unwrap();

let bars: Vec<OhlcvBar> = vec![/* ... historical bars */];

let result = optimizer.run(&bars, |train_bars, params| {
    let period = params.get("sma_period").copied().unwrap_or(10.0) as usize;
    Box::new(SmaStrategy { period, bar_count: 0, window: Default::default() })
}).unwrap();

// ── Interpret results ─────────────────────────────────────────────────────────
println!("Periods evaluated:    {}", result.periods.len());
println!("Aggregate OOS Sharpe: {:.2}", result.aggregate_sharpe);
println!("Stability score:      {:.1}%", result.stability_score * 100.0);
println!("Mean OOS return:      {:.2}%", result.mean_oos_return * 100.0);
println!("Worst OOS drawdown:   {:.2}%", result.worst_oos_drawdown * 100.0);

// Robustness check: Sharpe > 0 and at least 65% of windows profitable
if result.is_robust(0.65) {
    println!("Strategy PASSED walk-forward robustness check");
}

// Per-period detail
for (i, period) in result.periods.iter().enumerate() {
    println!("  [WF {}] IS Sharpe={:.2}  OOS Sharpe={:.2}  best_params={:?}",
        i, period.in_sample_sharpe, period.out_of_sample_sharpe, period.best_params);
}

// Best / worst OOS windows
if let Some(best) = result.best_period() {
    println!("Best OOS window:  bars {}–{} (Sharpe {:.2})", best.test_start, best.test_end, best.out_of_sample_sharpe);
}

WalkForwardResult Fields

Field Description
periods Vec<WfPeriod>, one entry per rolling window
aggregate_sharpe Mean out-of-sample Sharpe across all periods
stability_score Fraction of OOS windows with positive Sharpe; [0, 1]
mean_oos_return Mean OOS total return across all periods
worst_oos_drawdown Maximum OOS drawdown seen across any single period

WfPeriod Fields

Field Description
train_start / train_end Bar index range of the training window
test_start / test_end Bar index range of the test window
best_params HashMap<String, f64>, winning parameter combination
in_sample_sharpe Best Sharpe achieved on training data
out_of_sample_sharpe Sharpe achieved on held-out test data
oos_result Full BacktestResult for the OOS period

The backtest::engine module provides an event-driven backtester with realistic fill simulation.

Key Types

Type Description
BacktestEngine Stateless engine; call BacktestEngine::run(signals, config)
EngineConfig initial_capital, commission, slippage_bps, data: Vec<OhlcvBar>, capital_fraction
EngineSignal timestamp, symbol, direction: Direction, strength: f64
Direction Long, Short, Flat
BacktestResult equity_curve: Vec<f64>, trades: Vec<CompletedTrade>, metrics: BacktestMetrics
CompletedTrade entry_ts, exit_ts, direction, entry_price, exit_price, pnl, pnl_pct
BacktestMetrics total_return, annualized_return, sharpe, sortino, max_drawdown, calmar, win_rate, profit_factor, avg_trade_return, num_trades

Fill Model

  • Signals fire at bar N; fills execute at bar N+1 open
  • Slippage is applied symmetrically: longs pay more on entry, receive less on exit
  • Commission is deducted as a fraction of notional on every fill
  • Position size = strength * capital_fraction * current_equity / fill_price

The montecarlo module runs N Geometric Brownian Motion price-path simulations using a seeded LCG for reproducibility.

Key Types

Type Description
MonteCarloSimulator Stateless simulator
GbmParams mu (annual drift), sigma (annual vol), s0 (initial price)
MonteCarloConfig simulations, horizon_days, seed: Option<u64>
MonteCarloResult paths, var_95, cvar_95, median_final, best_case_final, worst_case_final

Methods

Method Description
simulate_paths(params, config) Run N GBM paths; returns Vec<Vec<f64>>
var(paths, confidence) Value at Risk at given confidence level
cvar(paths, confidence) Conditional VaR (Expected Shortfall), always ≤ VaR
percentile_paths(paths, percentiles) Extract paths at given percentiles (e.g. [5, 50, 95])
run(params, config) Convenience: simulate + compute all metrics

Implementation Notes

  • RNG: Linear Congruential Generator (Numerical Recipes constants)
  • Normal samples: Box-Muller transform
  • Same seed always produces identical paths (reproducible)
  • sigma = 0 → all paths are pure drift (deterministic)

The factor module provides Fama-French style multi-factor OLS regression.

Structs

Type Description
Factor { name, returns } Named return series for one risk factor (e.g. market, value, momentum)
FactorExposure { asset, betas, alpha, r_squared, residual_variance } OLS regression output for one asset
VarianceDecomposition { systematic_variance, idiosyncratic_variance, factor_contributions } Variance attribution
FactorPortfolio Aggregates per-asset exposures weighted by portfolio weights

Formulas

OLS via normal equations:

β̂ = (X'X)⁻¹ X'y

where X is the T × (K+1) design matrix (first column = intercept ones).

Matrix inversion: analytic for 2×2 and 3×3, Gaussian elimination with partial pivoting for larger systems.

Variance decomposition:

factor_contributions[i] = β_i² σ_i² + 2 Σ_{j>i} β_i β_j cov(i,j)
systematic_variance     = β' Σ β
total_variance          ≈ systematic_variance + residual_variance

Quick Example

use fin_primitives::factor::{Factor, FactorModel};

let market = Factor::new("MKT", vec![0.01, -0.02, 0.015, 0.005, -0.01]);
let asset  = vec![0.008, -0.018, 0.012, 0.003, -0.009];

let exposure = FactorModel::fit("AAPL", &asset, &[market]);
println!("alpha={:.4}  beta={:.4}  R²={:.4}",
    exposure.alpha, exposure.betas[0], exposure.r_squared);

The execution module estimates round-trip trading costs and finds optimal rebalancing trades.

Structs

Type Description
ExecutionCost { commission_usd, spread_cost_usd, market_impact_usd, total_cost_usd, cost_bps } Full cost breakdown
CostParams { commission_per_share, spread_bps, impact_coefficient, avg_daily_volume } Model parameters
Trade { symbol, direction, weight_change, estimated_cost_bps } A single rebalancing trade

Formulas

commission_usd    = commission_per_share × shares
spread_cost_usd   = (spread_bps / 10_000) × notional_usd
impact_bps        = impact_coefficient × sqrt(shares / avg_daily_volume) × 10_000
market_impact_usd = (impact_bps / 10_000) × notional_usd
total_cost_usd    = commission_usd + spread_cost_usd + market_impact_usd
cost_bps          = total_cost_usd / notional_usd × 10_000

Quick Example

use fin_primitives::execution::{CostModel, CostParams, TurnoverOptimizer};
use std::collections::HashMap;

let params = CostParams {
    commission_per_share: 0.005,
    spread_bps: 5.0,
    impact_coefficient: 0.1,
    avg_daily_volume: 1_000_000.0,
};

let cost = CostModel::estimate(100_000.0, 10_000.0, 10.0, &params);
println!("Total cost: ${:.2} ({:.1} bps)", cost.total_cost_usd, cost.cost_bps);

let current: HashMap<String, f64> = [("SPY".into(), 0.6), ("TLT".into(), 0.4)].into();
let target:  HashMap<String, f64> = [("SPY".into(), 0.5), ("TLT".into(), 0.5)].into();
let trades = TurnoverOptimizer::optimize(&current, &target, &params, 0.005);
for t in &trades {
    println!("{}: {:?} {:.1}% @ {:.1} bps", t.symbol, t.direction, t.weight_change * 100.0, t.estimated_cost_bps);
}

The yield_curve module provides a full yield curve construction and analytics toolkit.

Key Types

Type Description
YieldPoint A single (maturity_years: f64, yield_rate: f64) observation
YieldCurve Collection of YieldPoints sorted by maturity; primary analytics surface
CurveShape Normal, Inverted, Flat, Humped, classified from first/last yields and interior peak
NelsonSiegel Parametric model: beta0, beta1, beta2, tau

Interpolation

Method Description
YieldCurve::linear_interp(t) Piecewise-linear interpolation; clamps at endpoints
YieldCurve::cubic_spline(t) Natural cubic spline via tridiagonal (Thomas) solver

Analytics

Method Formula
forward_rate(t1, t2) f = (r2·t2 − r1·t1) / (t2 − t1)
duration(cash_flows) Macaulay: Σ(t · CF · e^(−r·t)) / Σ(CF · e^(−r·t))
convexity(cash_flows) Σ(t² · CF · e^(−r·t)) / PV
shape() Classifies as Normal / Inverted / Flat / Humped

Nelson-Siegel Model

r(t) = β₀ + β₁·(1−e^(−t/τ))/(t/τ) + β₂·((1−e^(−t/τ))/(t/τ) − e^(−t/τ))

NelsonSiegel::fit(points) fits all four parameters via gradient descent (500 iterations).

The events module implements a standard event-study methodology for measuring abnormal returns around discrete market events (earnings releases, guidance, macro shocks).

Key Types

Type Description
MarketEvent event_id, event_date (Unix secs), event_type, description
EventWindow pre_days: i32, post_days: i32, e.g. (-10, +10)
AbnormalReturn Per-day: day, raw_return, expected_return, abnormal_return, car
EventResult Full result: car_pre, car_post, peak_day, trough_day, abnormal_returns

Methods

Method Description
EventStudy::compute(event, prices, benchmark, window) Market-model abnormal returns; benchmark return = expected return
EventStudy::significance(results) t-statistic: mean_CAR / (std_CAR / √N)

Formulas

AR(d)  = raw_return(d) − benchmark_return(d)
CAR(d) = Σ AR from window_start to d
t-stat = mean(CAR) / (std(CAR) / √N)

The microstructure module detects three illegal order-book manipulation patterns in real time: spoofing, layering, and quote stuffing. All detection runs locally with no external calls.

use fin_primitives::microstructure::{
    MicrostructureDetector, DetectorConfig, OrderEvent, OrderAction, AlertKind,
};
use fin_primitives::types::{Price, Quantity, Side};
use rust_decimal::Decimal;

let mut detector = MicrostructureDetector::new(DetectorConfig {
    spoof_min_quantity: Decimal::from(1_000),   // flag orders > 1000 qty
    spoof_cancel_window_ns: 500_000_000,        // cancelled within 500 ms
    layer_min_levels: 3,                         // 3+ distinct price levels
    layer_window_ns: 200_000_000,               // within 200 ms
    stuff_rate_threshold: 100,                  // 100+ cancels per second
    stuff_window_ns: 1_000_000_000,
});

// Feed events from your exchange adapter.
detector.on_event(OrderEvent {
    order_id: 1,
    action: OrderAction::Add,
    price: Price::new("100.00").unwrap(),
    quantity: Quantity::new("5000").unwrap(),
    side: Side::Bid,
    timestamp_ns: 0,
});
detector.on_event(OrderEvent {
    order_id: 1,
    action: OrderAction::Cancel,
    price: Price::new("100.00").unwrap(),
    quantity: Quantity::new("5000").unwrap(),
    side: Side::Bid,
    timestamp_ns: 200_000_000, // 200 ms, within spoof window
});

for alert in detector.drain_alerts() {
    match alert.kind {
        AlertKind::Spoofing      => println!("SPOOF: {}", alert.detail),
        AlertKind::Layering      => println!("LAYER: {}", alert.detail),
        AlertKind::QuoteStuffing => println!("STUFF: {}", alert.detail),
    }
}

// Aggregate stats.
let s = detector.stats();
println!("Events: {}, Cancels: {}, Spoof: {}, Layer: {}, Stuff: {}",
    s.events_total, s.cancels_total, s.spoof_alerts, s.layer_alerts, s.stuff_alerts);

Detection heuristics are based on public CFTC/SEC regulatory guidance and academic literature (Comerton-Forde & Putniņš, 2015). All thresholds are configurable via [DetectorConfig].

The async_signals module wraps any SignalPipeline with Tokio MPSC channels for non-blocking, zero-copy signal streaming.

use fin_primitives::async_signals::{StreamingSignalPipeline, spawn_signal_stream};
use fin_primitives::signals::pipeline::SignalPipeline;
use fin_primitives::signals::indicators::Sma;
use fin_primitives::ohlcv::OhlcvBar;
use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let pipeline = SignalPipeline::new().add(Sma::new("sma20", 20));

    // Option A: high-level wrapper
    let (tick_tx, mut update_rx) = StreamingSignalPipeline::new(pipeline).spawn();

    // Option B: convenience function with your own tick channel
    // let (tick_tx, tick_rx) = mpsc::channel::<OhlcvBar>(1024);
    // let mut update_rx = spawn_signal_stream(pipeline, tick_rx);

    // Push bars from any async producer
    tokio::spawn(async move {
        // tick_tx.send(bar).await.unwrap();
        drop(tick_tx); // closing sender shuts down the pipeline task
    });

    while let Some(update) = update_rx.recv().await {
        if update.is_ready() {
            println!("{} = {}", update.signal_name, update.value);
        }
    }
}

Key properties:

  • Output buffers are pre-allocated at construction time (default: 4 096 slots).
  • The background task shuts down cleanly when all tick senders are dropped.
  • Multiple signals in the same pipeline each emit one SignalUpdate per bar.
  • SignalUpdate::timestamp carries wall-clock DateTime<Utc> of computation.
NanoTimestamp::now()                // current UTC nanoseconds
ts.add_days(n) / .sub_hours(n)
ts.add_months(n)                    // calendar-accurate month arithmetic
ts.start_of_week() / .end_of_month()
ts.start_of_quarter()               // Jan 1 / Apr 1 / Jul 1 / Oct 1
ts.end_of_quarter()                 // last nanosecond of the quarter
ts.is_same_quarter(other)           // same calendar quarter and year
ts.floor_to_hour() / .floor_to_minute() / .floor_to_second()
ts.is_market_hours()                // 09:30–16:00 ET (approximate)
ts.is_weekend()
ts.quarter()                        // 1–4
ts.elapsed_days() / .elapsed_hours() / .elapsed_minutes()
ts.nanoseconds_between(other)

Portfolio Optimization and Kelly Criterion Position Sizing

Change Module Detail
Portfolio Optimizer portfolio::optimizer Markowitz mean-variance optimization, MinVariance, MaxSharpe, RiskParity, EqualWeight via projected gradient descent (200 iters, simplex projection); CovarianceMatrix with ledoit_wolf_shrinkage; MaxWeight, MinWeight, LongOnly, SectorConstraint constraints; effective_n (inverse HHI)
Kelly Criterion Sizer position::kelly full_kelly, fractional_kelly, KellyResult with position size, max loss, and expected log growth; KellyPortfolio::allocate, multi-asset Kelly with correlation penalty and total-fraction cap
Portfolio Optimization: quick example
use fin_primitives::portfolio::{Asset, CovarianceMatrix, OptimizationObjective, Constraint, PortfolioOptimizer};

let assets = vec![
    Asset { symbol: "SPY".into(),  expected_return: 0.10, variance: 0.04 },
    Asset { symbol: "TLT".into(),  expected_return: 0.05, variance: 0.01 },
    Asset { symbol: "GLD".into(),  expected_return: 0.07, variance: 0.025 },
];

let mut cov = CovarianceMatrix::new(vec!["SPY".into(), "TLT".into(), "GLD".into()]);
cov.set(0, 0, 0.04);  cov.set(0, 1, -0.01); cov.set(0, 2, 0.005);
cov.set(1, 1, 0.01);  cov.set(1, 2, 0.002);
cov.set(2, 2, 0.025);
cov.ledoit_wolf_shrinkage();  // analytical Ledoit-Wolf shrinkage toward scaled identity

let result = PortfolioOptimizer::optimize(
    &assets,
    &cov,
    &OptimizationObjective::MaxSharpe { risk_free_rate: 0.04 },
    &[Constraint::LongOnly, Constraint::MaxWeight(0.6)],
);

println!("Sharpe: {:.3}", result.sharpe_ratio);
println!("Effective N: {:.2}", result.effective_n);  // inverse HHI diversification measure
for (sym, w) in &result.weights {
    println!("  {sym}: {:.1}%", w * 100.0);
}

Math:

  • Portfolio variance: σ²_p = w' Σ w
  • Sharpe ratio: S = (μ_p − r_f) / σ_p
  • Ledoit-Wolf shrinkage: Σ* = (1−α)Σ + α·μ·I where α = Σ_{i≠j} Σ²_{ij} / ((n+2)·Σ_{i≠j} Σ²_{ij})
  • Effective N (inverse HHI): N* = 1 / Σ_i w²_i
Kelly Criterion Position Sizing: quick example
use fin_primitives::position::{KellyInput, full_kelly, fractional_kelly, KellyPortfolio};
use fin_primitives::portfolio::CovarianceMatrix;

let input = KellyInput {
    win_probability: 0.60,   // 60 % win rate
    win_return:      1.0,    // +100 % on win
    loss_return:     1.0,    // −100 % on loss (full loss)
    bankroll:        50_000.0,
};

let full = full_kelly(&input);
println!("Full Kelly fraction: {:.1}%",  full.fraction * 100.0);  // 20.0 %
println!("Position size: ${:.0}",        full.position_size_usd);  // $10,000
println!("Max loss: ${:.0}",             full.max_loss_usd);
println!("Expected log growth: {:.4}",   full.expected_growth);

let half = fractional_kelly(&input, 0.5);
println!("Half-Kelly fraction: {:.1}%",  half.fraction * 100.0);   // 10.0 %

// Multi-asset Kelly with correlation penalty
let assets = vec![input.clone(), KellyInput { win_probability: 0.55, win_return: 1.5, loss_return: 1.0, bankroll: 50_000.0 }];
let mut cov = CovarianceMatrix::new(vec!["BTC".into(), "ETH".into()]);
cov.set(0, 1, 0.8);  // high positive correlation → penalty applied
let allocs = KellyPortfolio::allocate(&assets, &cov, 0.5);  // cap total at 50 %

Math:

  • Full Kelly: f* = (b·p − q) / b where b = win_return, p = win_probability, q = 1−p
  • Expected log growth: g = p·ln(1 + b·f) + q·ln(1 − f)
  • Correlation penalty: f̃_i = f_i / (1 + Σ_{j≠i} |ρ_{ij}|·f_j)

Signal Warmup Contracts, Signal Composition Engine, Risk Attribution

Change Module Detail
Signal Warmup Contracts signals::warmup WarmupContract trait, WarmupGuard (returns Err(NotReady) instead of Unavailable), WarmupReporter (pipeline warmup snapshot)
Signal Composition Engine signals::compose SignalExpr DSL, ComposedSignal evaluator, fluent SignalBuilder API for lag/normalize/threshold chains
Risk Attribution risk::attribution RiskAttributor decomposes portfolio risk into 6 factors; BhbAttribution for Brinson-Hood-Beebower P&L attribution
RiskMonitor::attribution_report risk Convenience method on RiskMonitor to get an AttributionReport in one call

Composite Signal Builder, OrderBook Diagnostic Logging

Change Module Detail
Multi-signal composite builder signals::composite Combine N indicators with WeightedSum, All (AND), Any (OR), or First (priority fallback) strategies using a fluent builder API
OrderBook inversion diagnostic log orderbook Inverted-spread detection now emits a WARN log with symbol, prices, and sequence number before rolling back, operators can correlate bad feed data without instrumenting call sites
Composite signal: quick example
use fin_primitives::signals::composite::{CompositeSignal, CompositeMode};
use fin_primitives::signals::indicators::{Sma, Rsi};
use rust_decimal_macros::dec;

// 50% SMA + 50% RSI blend: returns Unavailable until both indicators warm up.
let mut blend = CompositeSignal::builder("sma_rsi_blend")
    .add(Sma::new("sma20", 20)?, dec!(0.5))
    .add(Rsi::new("rsi14", 14)?, dec!(0.5))
    .mode(CompositeMode::WeightedSum)
    .build();

// AND gate: fire only when both are non-zero.
let mut gate = CompositeSignal::builder("trend_confirm")
    .add(Sma::new("sma50", 50)?, dec!(1))
    .add(Rsi::new("rsi14", 14)?, dec!(1))
    .mode(CompositeMode::All)
    .build();

Reference

Price and Quantity Types

Type Invariant Backing type
Price d > 0 (strictly positive) rust_decimal::Decimal
Quantity d >= 0 (non-negative) rust_decimal::Decimal
NanoTimestamp any i64; nanoseconds since Unix epoch (UTC) i64
Symbol non-empty, no whitespace String

OHLCV Invariants

Every OhlcvBar that enters an OhlcvSeries has been validated to satisfy:

high >= open    high >= close
low  <= open    low  <= close
high >= low

Any bar that violates these relationships is rejected with FinError::BarInvariant.

Order Book Guarantees

  • Bids are maintained in descending price order (best bid = highest price).
  • Asks are maintained in ascending price order (best ask = lowest price).
  • Sequence numbers are strictly monotone; delta.sequence must equal book.sequence() + 1.
  • A delta that would produce best_bid >= best_ask is rejected and the book is rolled back atomically.

Risk Metrics

  • Drawdown %: (peak_equity − current_equity) / peak_equity × 100. Always ≥ 0.
  • MaxDrawdownRule triggers when drawdown_pct > threshold_pct (strictly greater).
  • MinEquityRule triggers when equity < floor (strictly less).

Position P&L

  • Realized P&L (on reduce/close): closed_qty × (fill_price − avg_cost) for long.
  • Unrealized P&L: position_qty × (current_price − avg_cost).
  • Both are net of commissions.

types module

Price::new(d)        -> Result<Price, FinError>       // d > 0
Quantity::new(d)     -> Result<Quantity, FinError>    // d >= 0
Quantity::zero()     -> Quantity
Symbol::new(s)       -> Result<Symbol, FinError>      // non-empty, no whitespace
NanoTimestamp::now() -> NanoTimestamp                 // current UTC nanoseconds

orderbook module

OrderBook::new(symbol)
  .apply_delta(delta)          -> Result<(), FinError>
  .best_bid() / .best_ask()    -> Option<PriceLevel>
  .spread()                    -> Option<Decimal>       // best_ask - best_bid
  .mid_price()                 -> Option<Decimal>
  .vwap_for_qty(side, qty)     -> Result<Decimal, FinError>
  .top_bids(n) / .top_asks(n)  -> Vec<PriceLevel>

ohlcv module

OhlcvAggregator::new(symbol, tf) -> Result<Self, FinError>
  .push_tick(&tick)            -> Result<Option<OhlcvBar>, FinError>
  .flush()                     -> Option<OhlcvBar>

OhlcvSeries::new()
  .push(bar)                   -> Result<(), FinError>
  .closes()                    -> Vec<Decimal>
  .window(n)                   -> &[OhlcvBar]
  // ...360+ analytics methods

signals module

// Signal trait
trait Signal {
    fn name(&self)   -> &str;
    fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError>;
    fn is_ready(&self) -> bool;
    fn period(&self) -> usize;
    fn reset(&mut self);
}

SignalPipeline::new()
  .add(signal)           // builder pattern; chainable
  .update(&bar)          -> Result<SignalMap, FinError>

SignalMap::get(name)     -> Option<&SignalValue>
// SignalValue: Scalar(Decimal) | Unavailable

// Warmup contracts (signals::warmup)
WarmupGuard::new(signal)
  .update_checked(&bar)  -> Result<SignalValue, WarmupError>
  .is_ready()            -> bool
  .bars_remaining()      -> usize
  .bars_seen()           -> usize

WarmupReporter::new(periods, names)
  .tick() / .tick_n(n)
  .report(bars_consumed) -> WarmupReport

WarmupReport::all_ready()              -> bool
  .pipeline_bars_remaining()           -> usize
  .warming_signals()                   -> impl Iterator
  .display()                           -> String

// Signal composition (signals::compose)
SignalBuilder::new(signal)
  .lag(n)
  .normalize(NormMethod)
  .normalize_window(NormMethod, window)
  .threshold(level, Direction)
  .scale(factor)
  .build()               -> ComposedSignal  // implements Signal
  .build_named(name)     -> ComposedSignal

ComposedSignal::new(name, expr, leaves) -> Result<Self, FinError>
  // implements Signal: update / is_ready / period / reset

position module

PositionLedger::new(initial_cash)
  .apply_fill(fill)               -> Result<(), FinError>
  .equity(&prices)                -> Result<Decimal, FinError>
  .unrealized_pnl_total(&prices)  -> Result<Decimal, FinError>
  .realized_pnl_total()           -> Decimal
  // ...110+ portfolio analytics methods

risk module

DrawdownTracker::new(initial_equity)
  .update(equity)
  .current_drawdown_pct()   -> Decimal
  .calmar_ratio()           -> Option<Decimal>
  // ...90 risk/statistics methods

RiskMonitor::new(initial_equity)
  .add_rule(rule)           -> Self     // builder pattern
  .update(equity)           -> Vec<RiskBreach>
  .attribution_report(&ledger, market_data) -> AttributionReport

// Risk attribution (risk::attribution)
MarketData::new(market_vol)
  .with_beta(symbol, beta)
  .with_sector(symbol, sector)
  .with_liquidity(symbol, score)
  .with_idio_vol(symbol, vol)

RiskAttributor::new(&ledger, market_data)
  .compute()                -> AttributionReport
  .compute_bhb(&input)      -> BhbAttribution

AttributionReport::get(factor)          -> Option<&RiskAttribution>
  .dominant_factor()                    -> Option<&RiskAttribution>
  .factors_above(threshold_pct)         -> Vec<&RiskAttribution>
  .summary()                            -> String
  // Fields: attributions, total_risk, portfolio_beta, concentration_hhi, leverage_ratio

BhbAttribution                          // P&L decomposition
  .total_active_return: f64
  .total_allocation: f64
  .total_selection: f64
  .total_interaction: f64
  .best_allocation_sector()             -> Option<&SectorEffect>

greeks module

BlackScholes::price(&spec)              -> Result<Decimal, FinError>
BlackScholes::greeks(&spec)             -> Result<OptionGreeks, FinError>
BlackScholes::implied_vol(price, &spec) -> Result<Decimal, FinError>

// OptionGreeks fields: delta, gamma, theta, vega, rho (all Decimal)

SpreadGreeks::bull_call_spread(spot, low_k, high_k, days, r, vol) -> SpreadGreeks
SpreadGreeks::bear_put_spread(spot, low_k, high_k, days, r, vol)  -> SpreadGreeks
SpreadGreeks::straddle(spot, strike, days, r, vol)                  -> SpreadGreeks
SpreadGreeks::iron_condor(spot, p_lo, p_hi, c_lo, c_hi, days, r, vol) -> SpreadGreeks
SpreadGreeks::new(legs)                                             -> SpreadGreeks
  .net_greeks()                         -> Result<OptionGreeks, FinError>
  .leg_count()                          -> usize

backtest module

BacktestConfig::new(initial_capital, commission_rate) -> Result<Self, FinError>

Backtester::new(config)
  .run(bars, &mut strategy)             -> Result<BacktestResult, FinError>

// Strategy trait
trait Strategy {
    fn on_bar(&mut self, bar: &OhlcvBar) -> Option<Signal>;
}

WalkForwardOptimizer::new(train_size, test_size, config) -> Result<Self, FinError>
  .run(bars, make_strategy_fn)          -> Result<WalkForwardResult, FinError>

// WalkForwardResult fields: windows, mean_return, mean_sharpe, worst_drawdown

// Grid-search walk-forward (backtest::walk_forward)
WalkForwardOptimizer::new(config, bt_config)  -> Result<Self, FinError>
  .run(bars, make_strategy_fn)                -> Result<WalkForwardResult, FinError>

// WalkForwardResult fields: periods, aggregate_sharpe, stability_score, mean_oos_return, worst_oos_drawdown
WalkForwardResult::is_robust(min_stability)   -> bool
  .best_period() / .worst_period()            -> Option<&WfPeriod>

// WfPeriod fields: train_start, train_end, test_start, test_end,
//                  best_params, in_sample_sharpe, out_of_sample_sharpe, oos_result

regime module

RegimeDetector::new(period, config)     -> Result<Self, FinError>
  .update(&bar, cross_returns)          -> Result<(MarketRegime, f64), FinError>
  .current_regime()                     -> MarketRegime
  .history()                            -> &[RegimeHistory]
  .is_ready()                           -> bool
  .garch()                              -> &Garch11
  .correlation_detector()               -> &CorrelationBreakdownDetector
  .reset()

// MarketRegime variants
MarketRegime::Trending | MeanReverting | HighVolatility | LowVolatility | Crisis | Neutral | Unknown
MarketRegime::is_risk_off()             -> bool   // Crisis | Unknown
MarketRegime::short_code()              -> &str   // "TRD", "MRV", "HVL", etc.

// GARCH(1,1)
Garch11::new(alpha, beta, omega)        -> Result<Self, FinError>
  .update(log_return)                   -> f64    // returns σₜ
  .sigma() / .variance()               -> f64
  .long_run_sigma()                     -> f64
  .is_vol_elevated(multiplier)          -> bool

// Correlation breakdown
CorrelationBreakdownDetector::new(window, threshold, crisis_fraction) -> Result<Self, FinError>
  .update(asset_idx, log_return)
  .is_crisis()                          -> bool

// RegimeHistory
RegimeHistory { regime, started_at_bar, confidence, ended_at_bar }
  .duration_bars()                      -> Option<usize>
  .is_active()                          -> bool

// Regime-conditional RSI
RegimeConditionalSignal::new(trending_period, mean_reverting_period, neutral_period)
  .update(&bar, regime)                 -> Option<Result<f64, FinError>>
  .is_ready()                           -> bool

async_signals module

StreamingSignalPipeline::new(pipeline)
  .spawn()                              -> (mpsc::Sender<OhlcvBar>, mpsc::Receiver<SignalUpdate>)

spawn_signal_stream(pipeline, tick_rx) -> mpsc::Receiver<SignalUpdate>

// SignalUpdate fields: signal_name: String, value: SignalValue, timestamp: DateTime<Utc>

Custom implementations

RiskRule and Signal are traits, so your own rules and indicators sit next to the built-in ones.

use fin_primitives::risk::{RiskBreach, RiskRule};
use rust_decimal::Decimal;

struct HaltOnLoss {
    limit: Decimal,
}

impl RiskRule for HaltOnLoss {
    fn name(&self) -> &str {
        "halt_on_loss"
    }
    fn check(&self, equity: Decimal, _drawdown_pct: Decimal) -> Option<RiskBreach> {
        (equity < self.limit).then(|| RiskBreach {
            rule: self.name().into(),
            detail: format!("equity {equity} < halt limit {}", self.limit),
        })
    }
}
use fin_primitives::error::FinError;
use fin_primitives::signals::{BarInput, Signal, SignalValue};
use rust_decimal::Decimal;

/// Close minus open of the latest bar.
struct BarBody {
    name: String,
    seen: bool,
}

impl Signal for BarBody {
    fn name(&self) -> &str {
        &self.name
    }
    fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
        self.seen = true;
        Ok(SignalValue::Scalar(bar.close - bar.open))
    }
    fn is_ready(&self) -> bool {
        self.seen
    }
    fn period(&self) -> usize {
        1
    }
    fn reset(&mut self) {
        self.seen = false;
    }
}

Performance notes

  • O(log n) order book mutations: apply_delta performs a single BTreeMap::insert or BTreeMap::remove. Inverted-spread check reads two keys and does not allocate.
  • O(1) streaming indicators: Ema and Rsi maintain constant-size state regardless of history length. Sma uses a VecDeque capped at period elements.
  • Zero-copy tick replay: TickReplayer sorts once at construction and returns shared references on each call; no per-tick heap allocation.

Tests and benchmarks

cargo test                 # unit, integration and property tests

cargo test --doc           # every doc example, including the one above

cargo run --example candles

cargo bench --bench tick_bench

cargo doc --no-deps --open

Status on main as of this README: all 255 integration tests in tests/ and all 716 doc examples pass (one is marked ignore). Of 5,540 unit tests inside the modules, 18 currently fail (in clustering, events, execution, factor, fixed_income::swaps, options::greeks, pairs_trading, portfolio, regime::hmm, risk, signals::multi_tf and yield_curve), so cargo test exits non-zero until those are fixed. The core types, order book, aggregator, ledger and risk monitor have no failing tests.

CI (.github/workflows/ci.yml) runs cargo check, builds and runs the examples, and runs the doc tests on every push.

Criterion microbenchmarks for tick filtering, order book deltas and indicators live in benches/tick_bench.rs. No benchmark numbers are published here; run them on your own hardware.

Contributing

  1. Fork the repository and create a branch from main.
  2. All public items must have /// doc comments with purpose, arguments, return values, and errors.
  3. All fallible operations must return Result; no unwrap, expect, or panic! in non-test code.
  4. Every new behavior must have at least one happy-path test and one edge-case test.
  5. Run cargo fmt, cargo clippy -- -D warnings, and cargo test before opening a PR.

License

MIT. See LICENSE.

Related projects

  • fin-stream: real-time market data streaming (tick normalization, SPSC ring buffer, OHLCV, order book) that depends on this crate.