{"id":48595256,"url":"https://github.com/Mattbusel/fin-primitives","last_synced_at":"2026-04-24T14:01:05.952Z","repository":{"id":342871881,"uuid":"1175485632","full_name":"Mattbusel/fin-primitives","owner":"Mattbusel","description":"Financial market primitives — price types, order book, OHLCV, indicators, position ledger, risk monitor","archived":false,"fork":false,"pushed_at":"2026-03-23T11:03:47.000Z","size":2242,"stargazers_count":8,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-20T07:37:44.691Z","etag":null,"topics":["finance","ohlcv","order-book","rust","trading"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Mattbusel.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-07T19:22:02.000Z","updated_at":"2026-04-14T06:31:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Mattbusel/fin-primitives","commit_stats":null,"previous_names":["mattbusel/fin-primitives"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/Mattbusel/fin-primitives","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mattbusel%2Ffin-primitives","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mattbusel%2Ffin-primitives/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mattbusel%2Ffin-primitives/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mattbusel%2Ffin-primitives/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Mattbusel","download_url":"https://codeload.github.com/Mattbusel/fin-primitives/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mattbusel%2Ffin-primitives/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32226408,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T13:21:15.438Z","status":"ssl_error","status_checked_at":"2026-04-24T13:21:15.005Z","response_time":64,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["finance","ohlcv","order-book","rust","trading"],"created_at":"2026-04-08T21:00:31.107Z","updated_at":"2026-04-24T14:01:05.934Z","avatar_url":"https://github.com/Mattbusel.png","language":"Rust","funding_links":[],"categories":["Technical Indicators"],"sub_categories":[],"readme":"# fin-primitives\n\n## Yield Curve Modeler\n\nThe `yield_curve` module provides a full yield curve construction and analytics toolkit.\n\n### Key Types\n\n| Type | Description |\n|------|-------------|\n| `YieldPoint` | A single `(maturity_years: f64, yield_rate: f64)` observation |\n| `YieldCurve` | Collection of `YieldPoint`s sorted by maturity; primary analytics surface |\n| `CurveShape` | `Normal`, `Inverted`, `Flat`, `Humped` — classified from first/last yields and interior peak |\n| `NelsonSiegel` | Parametric model: `beta0`, `beta1`, `beta2`, `tau` |\n\n### Interpolation\n\n| Method | Description |\n|--------|-------------|\n| `YieldCurve::linear_interp(t)` | Piecewise-linear interpolation; clamps at endpoints |\n| `YieldCurve::cubic_spline(t)` | Natural cubic spline via tridiagonal (Thomas) solver |\n\n### Analytics\n\n| Method | Formula |\n|--------|---------|\n| `forward_rate(t1, t2)` | `f = (r2·t2 − r1·t1) / (t2 − t1)` |\n| `duration(cash_flows)` | Macaulay: `Σ(t · CF · e^(−r·t)) / Σ(CF · e^(−r·t))` |\n| `convexity(cash_flows)` | `Σ(t² · CF · e^(−r·t)) / PV` |\n| `shape()` | Classifies as `Normal / Inverted / Flat / Humped` |\n\n### Nelson-Siegel Model\n\n```\nr(t) = β₀ + β₁·(1−e^(−t/τ))/(t/τ) + β₂·((1−e^(−t/τ))/(t/τ) − e^(−t/τ))\n```\n\n`NelsonSiegel::fit(points)` fits all four parameters via gradient descent (500 iterations).\n\n---\n\n## Event Study Framework\n\nThe `events` module implements a standard event-study methodology for measuring abnormal\nreturns around discrete market events (earnings releases, guidance, macro shocks).\n\n### Key Types\n\n| Type | Description |\n|------|-------------|\n| `MarketEvent` | `event_id`, `event_date` (Unix secs), `event_type`, `description` |\n| `EventWindow` | `pre_days: i32`, `post_days: i32` — e.g. `(-10, +10)` |\n| `AbnormalReturn` | Per-day: `day`, `raw_return`, `expected_return`, `abnormal_return`, `car` |\n| `EventResult` | Full result: `car_pre`, `car_post`, `peak_day`, `trough_day`, `abnormal_returns` |\n\n### Methods\n\n| Method | Description |\n|--------|-------------|\n| `EventStudy::compute(event, prices, benchmark, window)` | Market-model abnormal returns; benchmark return = expected return |\n| `EventStudy::significance(results)` | t-statistic: `mean_CAR / (std_CAR / √N)` |\n\n### Formulas\n\n```\nAR(d)  = raw_return(d) − benchmark_return(d)\nCAR(d) = Σ AR from window_start to d\nt-stat = mean(CAR) / (std(CAR) / √N)\n```\n\n---\n\n\n[![CI](https://github.com/Mattbusel/fin-primitives/actions/workflows/ci.yml/badge.svg)](https://github.com/Mattbusel/fin-primitives/actions/workflows/ci.yml)\n[![Crates.io](https://img.shields.io/crates/v/fin-primitives.svg)](https://crates.io/crates/fin-primitives)\n[![docs.rs](https://docs.rs/fin-primitives/badge.svg)](https://docs.rs/fin-primitives)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Minimum Rust Version](https://img.shields.io/badge/rust-1.81%2B-orange.svg)](https://www.rust-lang.org)\n\nA zero-panic, decimal-precise foundation for high-frequency trading and quantitative\nsystems in Rust. `fin-primitives` provides the building blocks: validated types,\norder book, OHLCV aggregation, **725+ streaming technical indicators**, position ledger,\nand composable risk monitoring — so that upstream crates and applications can focus on\nstrategy rather than infrastructure.\n\n---\n\n## Backtesting Engine\n\nThe `backtest::engine` module provides an event-driven backtester with realistic fill simulation.\n\n### Key Types\n\n| Type | Description |\n|------|-------------|\n| `BacktestEngine` | Stateless engine; call `BacktestEngine::run(signals, config)` |\n| `EngineConfig` | `initial_capital`, `commission`, `slippage_bps`, `data: Vec\u003cOhlcvBar\u003e`, `capital_fraction` |\n| `EngineSignal` | `timestamp`, `symbol`, `direction: Direction`, `strength: f64` |\n| `Direction` | `Long`, `Short`, `Flat` |\n| `BacktestResult` | `equity_curve: Vec\u003cf64\u003e`, `trades: Vec\u003cCompletedTrade\u003e`, `metrics: BacktestMetrics` |\n| `CompletedTrade` | `entry_ts`, `exit_ts`, `direction`, `entry_price`, `exit_price`, `pnl`, `pnl_pct` |\n| `BacktestMetrics` | `total_return`, `annualized_return`, `sharpe`, `sortino`, `max_drawdown`, `calmar`, `win_rate`, `profit_factor`, `avg_trade_return`, `num_trades` |\n\n### Fill Model\n\n- Signals fire at bar N; fills execute at bar N+1 open\n- Slippage is applied symmetrically: longs pay more on entry, receive less on exit\n- Commission is deducted as a fraction of notional on every fill\n- Position size = `strength * capital_fraction * current_equity / fill_price`\n\n---\n\n## Monte Carlo Simulator\n\nThe `montecarlo` module runs N Geometric Brownian Motion price-path simulations using a seeded LCG for reproducibility.\n\n### Key Types\n\n| Type | Description |\n|------|-------------|\n| `MonteCarloSimulator` | Stateless simulator |\n| `GbmParams` | `mu` (annual drift), `sigma` (annual vol), `s0` (initial price) |\n| `MonteCarloConfig` | `simulations`, `horizon_days`, `seed: Option\u003cu64\u003e` |\n| `MonteCarloResult` | `paths`, `var_95`, `cvar_95`, `median_final`, `best_case_final`, `worst_case_final` |\n\n### Methods\n\n| Method | Description |\n|--------|-------------|\n| `simulate_paths(params, config)` | Run N GBM paths; returns `Vec\u003cVec\u003cf64\u003e\u003e` |\n| `var(paths, confidence)` | Value at Risk at given confidence level |\n| `cvar(paths, confidence)` | Conditional VaR (Expected Shortfall) — always ≤ VaR |\n| `percentile_paths(paths, percentiles)` | Extract paths at given percentiles (e.g. `[5, 50, 95]`) |\n| `run(params, config)` | Convenience: simulate + compute all metrics |\n\n### Implementation Notes\n\n- RNG: Linear Congruential Generator (Numerical Recipes constants)\n- Normal samples: Box-Muller transform\n- Same seed always produces identical paths (reproducible)\n- `sigma = 0` → all paths are pure drift (deterministic)\n\n---\n\n## Factor Model\n\nThe `factor` module provides Fama-French style multi-factor OLS regression.\n\n### Structs\n\n| Type | Description |\n|------|-------------|\n| `Factor { name, returns }` | Named return series for one risk factor (e.g. market, value, momentum) |\n| `FactorExposure { asset, betas, alpha, r_squared, residual_variance }` | OLS regression output for one asset |\n| `VarianceDecomposition { systematic_variance, idiosyncratic_variance, factor_contributions }` | Variance attribution |\n| `FactorPortfolio` | Aggregates per-asset exposures weighted by portfolio weights |\n\n### Formulas\n\n**OLS via normal equations:**\n\n```\nβ̂ = (X'X)⁻¹ X'y\n```\n\nwhere `X` is the `T × (K+1)` design matrix (first column = intercept ones).\n\nMatrix inversion: analytic for 2×2 and 3×3, Gaussian elimination with partial pivoting for larger systems.\n\n**Variance decomposition:**\n\n```\nfactor_contributions[i] = β_i² σ_i² + 2 Σ_{j\u003ei} β_i β_j cov(i,j)\nsystematic_variance     = β' Σ β\ntotal_variance          ≈ systematic_variance + residual_variance\n```\n\n### Quick Example\n\n```rust\nuse fin_primitives::factor::{Factor, FactorModel};\n\nlet market = Factor::new(\"MKT\", vec![0.01, -0.02, 0.015, 0.005, -0.01]);\nlet asset  = vec![0.008, -0.018, 0.012, 0.003, -0.009];\n\nlet exposure = FactorModel::fit(\"AAPL\", \u0026asset, \u0026[market]);\nprintln!(\"alpha={:.4}  beta={:.4}  R²={:.4}\",\n    exposure.alpha, exposure.betas[0], exposure.r_squared);\n```\n\n---\n\n## Execution Cost Model\n\nThe `execution` module estimates round-trip trading costs and finds optimal rebalancing trades.\n\n### Structs\n\n| Type | Description |\n|------|-------------|\n| `ExecutionCost { commission_usd, spread_cost_usd, market_impact_usd, total_cost_usd, cost_bps }` | Full cost breakdown |\n| `CostParams { commission_per_share, spread_bps, impact_coefficient, avg_daily_volume }` | Model parameters |\n| `Trade { symbol, direction, weight_change, estimated_cost_bps }` | A single rebalancing trade |\n\n### Formulas\n\n```\ncommission_usd    = commission_per_share × shares\nspread_cost_usd   = (spread_bps / 10_000) × notional_usd\nimpact_bps        = impact_coefficient × sqrt(shares / avg_daily_volume) × 10_000\nmarket_impact_usd = (impact_bps / 10_000) × notional_usd\ntotal_cost_usd    = commission_usd + spread_cost_usd + market_impact_usd\ncost_bps          = total_cost_usd / notional_usd × 10_000\n```\n\n### Quick Example\n\n```rust\nuse fin_primitives::execution::{CostModel, CostParams, TurnoverOptimizer};\nuse std::collections::HashMap;\n\nlet params = CostParams {\n    commission_per_share: 0.005,\n    spread_bps: 5.0,\n    impact_coefficient: 0.1,\n    avg_daily_volume: 1_000_000.0,\n};\n\nlet cost = CostModel::estimate(100_000.0, 10_000.0, 10.0, \u0026params);\nprintln!(\"Total cost: ${:.2} ({:.1} bps)\", cost.total_cost_usd, cost.cost_bps);\n\nlet current: HashMap\u003cString, f64\u003e = [(\"SPY\".into(), 0.6), (\"TLT\".into(), 0.4)].into();\nlet target:  HashMap\u003cString, f64\u003e = [(\"SPY\".into(), 0.5), (\"TLT\".into(), 0.5)].into();\nlet trades = TurnoverOptimizer::optimize(\u0026current, \u0026target, \u0026params, 0.005);\nfor t in \u0026trades {\n    println!(\"{}: {:?} {:.1}% @ {:.1} bps\", t.symbol, t.direction, t.weight_change * 100.0, t.estimated_cost_bps);\n}\n```\n\n---\n\n## What's New\n\n### v2.17.0 — Portfolio Optimization and Kelly Criterion Position Sizing\n\n| Change | Module | Detail |\n|--------|--------|--------|\n| **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) |\n| **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 |\n\n#### Portfolio Optimization — quick example\n\n```rust\nuse fin_primitives::portfolio::{Asset, CovarianceMatrix, OptimizationObjective, Constraint, PortfolioOptimizer};\n\nlet assets = vec![\n    Asset { symbol: \"SPY\".into(),  expected_return: 0.10, variance: 0.04 },\n    Asset { symbol: \"TLT\".into(),  expected_return: 0.05, variance: 0.01 },\n    Asset { symbol: \"GLD\".into(),  expected_return: 0.07, variance: 0.025 },\n];\n\nlet mut cov = CovarianceMatrix::new(vec![\"SPY\".into(), \"TLT\".into(), \"GLD\".into()]);\ncov.set(0, 0, 0.04);  cov.set(0, 1, -0.01); cov.set(0, 2, 0.005);\ncov.set(1, 1, 0.01);  cov.set(1, 2, 0.002);\ncov.set(2, 2, 0.025);\ncov.ledoit_wolf_shrinkage();  // analytical Ledoit-Wolf shrinkage toward scaled identity\n\nlet result = PortfolioOptimizer::optimize(\n    \u0026assets,\n    \u0026cov,\n    \u0026OptimizationObjective::MaxSharpe { risk_free_rate: 0.04 },\n    \u0026[Constraint::LongOnly, Constraint::MaxWeight(0.6)],\n);\n\nprintln!(\"Sharpe: {:.3}\", result.sharpe_ratio);\nprintln!(\"Effective N: {:.2}\", result.effective_n);  // inverse HHI diversification measure\nfor (sym, w) in \u0026result.weights {\n    println!(\"  {sym}: {:.1}%\", w * 100.0);\n}\n```\n\n**Math:**\n- Portfolio variance: `σ²_p = w' Σ w`\n- Sharpe ratio: `S = (μ_p − r_f) / σ_p`\n- Ledoit-Wolf shrinkage: `Σ* = (1−α)Σ + α·μ·I` where `α = Σ_{i≠j} Σ²_{ij} / ((n+2)·Σ_{i≠j} Σ²_{ij})`\n- Effective N (inverse HHI): `N* = 1 / Σ_i w²_i`\n\n#### Kelly Criterion Position Sizing — quick example\n\n```rust\nuse fin_primitives::position::{KellyInput, full_kelly, fractional_kelly, KellyPortfolio};\nuse fin_primitives::portfolio::CovarianceMatrix;\n\nlet input = KellyInput {\n    win_probability: 0.60,   // 60 % win rate\n    win_return:      1.0,    // +100 % on win\n    loss_return:     1.0,    // −100 % on loss (full loss)\n    bankroll:        50_000.0,\n};\n\nlet full = full_kelly(\u0026input);\nprintln!(\"Full Kelly fraction: {:.1}%\",  full.fraction * 100.0);  // 20.0 %\nprintln!(\"Position size: ${:.0}\",        full.position_size_usd);  // $10,000\nprintln!(\"Max loss: ${:.0}\",             full.max_loss_usd);\nprintln!(\"Expected log growth: {:.4}\",   full.expected_growth);\n\nlet half = fractional_kelly(\u0026input, 0.5);\nprintln!(\"Half-Kelly fraction: {:.1}%\",  half.fraction * 100.0);   // 10.0 %\n\n// Multi-asset Kelly with correlation penalty\nlet assets = vec![input.clone(), KellyInput { win_probability: 0.55, win_return: 1.5, loss_return: 1.0, bankroll: 50_000.0 }];\nlet mut cov = CovarianceMatrix::new(vec![\"BTC\".into(), \"ETH\".into()]);\ncov.set(0, 1, 0.8);  // high positive correlation → penalty applied\nlet allocs = KellyPortfolio::allocate(\u0026assets, \u0026cov, 0.5);  // cap total at 50 %\n```\n\n**Math:**\n- Full Kelly: `f* = (b·p − q) / b` where `b = win_return`, `p = win_probability`, `q = 1−p`\n- Expected log growth: `g = p·ln(1 + b·f) + q·ln(1 − f)`\n- Correlation penalty: `f̃_i = f_i / (1 + Σ_{j≠i} |ρ_{ij}|·f_j)`\n\n---\n\n### v2.16.0 — Signal Warmup Contracts, Signal Composition Engine, Risk Attribution\n\n| Change | Module | Detail |\n|--------|--------|--------|\n| **Signal Warmup Contracts** | `signals::warmup` | `WarmupContract` trait, `WarmupGuard` (returns `Err(NotReady)` instead of `Unavailable`), `WarmupReporter` (pipeline warmup snapshot) |\n| **Signal Composition Engine** | `signals::compose` | `SignalExpr` DSL, `ComposedSignal` evaluator, fluent `SignalBuilder` API for lag/normalize/threshold chains |\n| **Risk Attribution** | `risk::attribution` | `RiskAttributor` decomposes portfolio risk into 6 factors; `BhbAttribution` for Brinson-Hood-Beebower P\u0026L attribution |\n| **`RiskMonitor::attribution_report`** | `risk` | Convenience method on `RiskMonitor` to get an `AttributionReport` in one call |\n\n### v2.15.0 — Composite Signal Builder, OrderBook Diagnostic Logging\n\n| Change | Module | Detail |\n|--------|--------|--------|\n| **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 |\n| **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 |\n\n#### Composite signal — quick example\n\n```rust\nuse fin_primitives::signals::composite::{CompositeSignal, CompositeMode};\nuse fin_primitives::signals::indicators::{Sma, Rsi};\nuse rust_decimal_macros::dec;\n\n// 50% SMA + 50% RSI blend: returns Unavailable until both indicators warm up.\nlet mut blend = CompositeSignal::builder(\"sma_rsi_blend\")\n    .add(Sma::new(\"sma20\", 20)?, dec!(0.5))\n    .add(Rsi::new(\"rsi14\", 14)?, dec!(0.5))\n    .mode(CompositeMode::WeightedSum)\n    .build();\n\n// AND gate: fire only when both are non-zero.\nlet mut gate = CompositeSignal::builder(\"trend_confirm\")\n    .add(Sma::new(\"sma50\", 50)?, dec!(1))\n    .add(Rsi::new(\"rsi14\", 14)?, dec!(1))\n    .mode(CompositeMode::All)\n    .build();\n```\n\n---\n\n## What Is Included\n\n| Module | What it provides | Key guarantee |\n|--------|-----------------|---------------|\n| [`types`] | `Price`, `Quantity`, `Symbol`, `NanoTimestamp`, `Side` newtypes | Validation at construction; no invalid value can exist at runtime |\n| [`tick`] | `Tick`, `TickFilter`, `TickReplayer` | Filter is pure; replayer always yields ticks in ascending timestamp order |\n| [`orderbook`] | L2 `OrderBook` with `apply_delta`, spread, mid-price, VWAP, top-N levels | Sequence validation; inverted spreads are detected, logged, and rolled back |\n| [`ohlcv`] | `OhlcvBar`, `Timeframe`, `OhlcvAggregator`, `OhlcvSeries` (370+ analytics) | Bar invariants (`high \u003e= low`, etc.) enforced on every push |\n| [`signals`] | `Signal` trait, `SignalPipeline`, **725+ built-in indicators**, `SignalMap` (90+ methods), `CompositeSignal`, **`SignalExpr` composition DSL**, **`WarmupGuard`** | Returns `Unavailable` until warm-up period is satisfied; no silent NaN |\n| [`position`] | `Position`, `Fill`, `PositionLedger` (145+ methods) | VWAP average cost; realized and unrealized P\u0026L net of commissions |\n| [`risk`] | `DrawdownTracker` (120+ methods), `RiskRule` trait, `RiskMonitor`, **`RiskAttributor`** (6-factor), **`BhbAttribution`** | All breaches returned as a typed `Vec\u003cRiskBreach\u003e`; never silently swallowed |\n| [`greeks`] | `BlackScholes`, `OptionGreeks`, `OptionSpec`, `SpreadGreeks` | All math returns `Result\u003cT, FinError\u003e`; no panics on edge-case inputs |\n| [`backtest`] | `Backtester`, `Strategy` trait, `BacktestResult`, `WalkForwardOptimizer`, `WfPeriod`, `ParamRange` | Bar-by-bar; no look-ahead; grid-search walk-forward with OOS stability score |\n| [`async_signals`] | `StreamingSignalPipeline`, `SignalUpdate`, `spawn_signal_stream` | Tokio MPSC; pre-allocated output buffers on the hot path |\n| [`regime`] | `RegimeDetector`, `MarketRegime`, `Garch11`, `CorrelationBreakdownDetector`, `RegimeConditionalSignal`, `RegimeHistory` | Hurst + GARCH(1,1) + cross-asset correlation breakdown; regime-adaptive RSI |\n\n---\n\n## Why fin-primitives?\n\nMost financial Rust crates solve one problem. `fin-primitives` solves the whole\nstack — validated domain types through streaming indicators through risk monitoring\n— with a single consistent design contract:\n\n| Concern | How fin-primitives addresses it |\n|---------|--------------------------------|\n| **Correctness** | `Price`/`Quantity`/`Symbol` are validated newtypes; invalid values cannot exist at runtime |\n| **Precision** | All prices use `rust_decimal::Decimal`; floating-point drift is structurally impossible |\n| **No surprises** | Signals return `Unavailable` — never silent NaN — until warmup is complete; `WarmupGuard` converts that to a typed `Err` |\n| **Composability** | `Signal`, `RiskRule`, `TickFilter` are traits; plug in your own without forking |\n| **Expressiveness** | The `SignalExpr` DSL lets you write `rsi.lag(1).normalize(ZScore).threshold(2.0, Above)` instead of bespoke structs |\n| **Attribution** | The 6-factor `RiskAttributor` and BHB P\u0026L decomposition let you see *why* your portfolio is taking risk, not just *how much* |\n| **Scale** | 725+ streaming indicators, 370+ OHLCV analytics, 145+ ledger methods, 120+ drawdown statistics — all in one coherent API |\n| **Safety** | `#![forbid(unsafe_code)]`; zero `unwrap`/`expect` in production paths; every error is typed and propagatable |\n\n```\n\"The goal is to make correctness the path of least resistance.\"\n```\n\n---\n\n## Design Principles\n\n- **Zero panics.** Every fallible operation returns `Result\u003c_, FinError\u003e`.\n  No `unwrap` or `expect` in production code paths.\n- **Decimal precision.** All prices and quantities use [`rust_decimal::Decimal`].\n  Floating-point drift is structurally impossible.\n- **Nanosecond timestamps.** `NanoTimestamp` is a newtype over `i64` nanoseconds\n  since Unix epoch, suitable for microsecond-accurate event ordering and replay.\n- **Composable by design.** `RiskRule`, `Signal`, and `TickFilter` are traits;\n  plug in your own implementations without forking.\n- **Separation of concerns.** Each module has a documented responsibility contract\n  and an explicit \"NOT Responsible For\" section.\n\n---\n\n## Quickstart\n\nAdd to `Cargo.toml`:\n\n```toml\n[dependencies]\nfin-primitives = \"2.9\"\nrust_decimal_macros = \"1\"\n```\n\n### Example: Buy, mark-to-market, check risk\n\n```rust\nuse fin_primitives::position::{Fill, PositionLedger};\nuse fin_primitives::risk::{MaxDrawdownRule, RiskMonitor};\nuse fin_primitives::types::{NanoTimestamp, Price, Quantity, Side, Symbol};\nuse rust_decimal_macros::dec;\nuse std::collections::HashMap;\n\nfn main() -\u003e Result\u003c(), fin_primitives::FinError\u003e {\n    let mut ledger = PositionLedger::new(dec!(100_000));\n    let mut monitor = RiskMonitor::new(dec!(100_000))\n        .add_rule(MaxDrawdownRule { threshold_pct: dec!(10) });\n\n    ledger.apply_fill(Fill {\n        symbol: Symbol::new(\"AAPL\")?,\n        side: Side::Bid,\n        quantity: Quantity::new(dec!(100))?,\n        price: Price::new(dec!(175))?,\n        timestamp: NanoTimestamp::now(),\n        commission: dec!(1),\n    })?;\n\n    let mut prices = HashMap::new();\n    prices.insert(\"AAPL\".to_owned(), Price::new(dec!(155))?);\n    let equity = ledger.equity(\u0026prices)?;\n\n    let breaches = monitor.update(equity);\n    for b in \u0026breaches {\n        eprintln!(\"Risk breach [{}]: {}\", b.rule, b.detail);\n    }\n    Ok(())\n}\n```\n\n### Example: Tick-to-OHLCV with SMA signal\n\n```rust\nuse fin_primitives::ohlcv::{OhlcvAggregator, Timeframe};\nuse fin_primitives::signals::SignalPipeline;\nuse fin_primitives::signals::indicators::Sma;\nuse fin_primitives::tick::Tick;\nuse fin_primitives::types::{NanoTimestamp, Price, Quantity, Side, Symbol};\nuse rust_decimal_macros::dec;\n\nfn main() -\u003e Result\u003c(), fin_primitives::FinError\u003e {\n    let sym = Symbol::new(\"BTC\")?;\n    let mut agg = OhlcvAggregator::new(sym.clone(), Timeframe::Minutes(1))?;\n    let mut pipeline = SignalPipeline::new().add(Sma::new(\"sma20\", 20));\n\n    let tick = Tick::new(\n        sym,\n        Price::new(dec!(65_000))?,\n        Quantity::new(dec!(0.5))?,\n        Side::Ask,\n        NanoTimestamp::now(),\n    );\n\n    if let Some(bar) = agg.push_tick(\u0026tick)? {\n        let signals = pipeline.update(\u0026bar)?;\n        println!(\"sma20 = {:?}\", signals.get(\"sma20\"));\n    }\n    Ok(())\n}\n```\n\n### Example: RSI(14) computation\n\n```rust\nuse fin_primitives::signals::indicators::Rsi;\nuse fin_primitives::signals::{Signal, SignalValue};\nuse fin_primitives::ohlcv::OhlcvBar;\nuse fin_primitives::types::{NanoTimestamp, Price, Quantity, Symbol};\nuse rust_decimal_macros::dec;\n\nfn main() -\u003e Result\u003c(), fin_primitives::FinError\u003e {\n    let mut rsi = Rsi::new(\"rsi14\", 14);\n    let closes = [44, 44, 44, 43, 44, 44, 45, 45, 43, 44, 44, 45, 45, 43, 44u32];\n    for c in closes {\n        let p = Price::new(dec!(1) * rust_decimal::Decimal::from(c))?;\n        let bar = OhlcvBar {\n            symbol: Symbol::new(\"X\")?,\n            open: p, high: p, low: p, close: p,\n            volume: Quantity::zero(),\n            ts_open: NanoTimestamp(0),\n            ts_close: NanoTimestamp(1),\n            tick_count: 1,\n        };\n        if let SignalValue::Scalar(v) = rsi.update(\u0026bar)? {\n            println!(\"RSI(14) = {v:.2}\");\n        }\n    }\n    Ok(())\n}\n```\n\n---\n\n## Technical Indicators (725+)\n\nAll indicators implement the `Signal` trait and return `SignalValue::Unavailable`\nuntil warm-up is satisfied. No silent NaN or panic.\n\n**Trend / Moving Averages**\n\n`Sma`, `Ema`, `Dema`, `Tema`, `Wma`, `HullMa`, `Alma`, `Smma`, `Zlema`, `T3`,\n`Trima`, `Kama`, `Lsma`, `Vidya`, `Swma`, `McGinley`, `LinRegSlope`, `Frama`,\n`DemaRatio`, `DemaCross`, `EmaCross`, `EmaSlope`, `EmaConvergence`, `TypicalPriceMa`,\n`TrueRangeEma`, `CoralTrend`, `HalfTrend`, `MesaAdaptiveMa`, `JurikMa`,\n`ChandeKrollStop`, `EmaRatio`, `EmaAlignment`, `EmaBandWidth`, `SmaDistancePct`,\n`TrendMagic`, `AdaptiveSupertrend`, `RollingVwap`\n\n**Momentum / Oscillators**\n\n`Rsi`, `Macd`, `Cci`, `Roc`, `Momentum`, `Apo`, `Ppo`, `Cmo`, `Tsi`, `Rvi`,\n`StochasticK`, `StochasticD`, `StochRsi`, `StochRsiSmoothed`, `WilliamsR`,\n`UltimateOscillator`, `Coppock`, `Kst`, `Trix`, `Dpo`, `Pgo`, `Rmi`, `Cog`,\n`Pfe`, `ConnorsRsi`, `DualRsi`, `RsiMa`, `RsiDivergence`, `SmoothedRsi`,\n`AdaptiveRsi`, `RsiStochastic`, `VolumeWeightedRsi`, `Qqe`, `Pmo`, `Tii`,\n`AwesomeOscillator`, `Smi`, `Ctm`, `PriceMomentumOscillator`, `MomentumOscillator`,\n`DeltaMomentum`, `CumReturnMomentum`, `NormalizedMomentum`, `MomentumQuality`,\n`MomentumReversal`, `MomentumStreak`, `MomentumDivergence`, `MomentumConsistency`,\n`UpMomentumPct`, `BodyMomentum`, `SlopeOscillator`, `EhlersCyberCycle`,\n`ChandeForecastOsc`, `ChandeMomentumSmoothed`, `DynamicMomentumIndex`\n\n**Volatility**\n\n`Atr`, `Natr`, `BollingerB`, `BollingerPctB`, `BollingerWidth`, `KeltnerChannel`,\n`DonchianMidpoint`, `DonchianWidth`, `Vhf`, `ChoppinessIndex`, `HistoricalVolatility`,\n`RelativeVolatility`, `ChaikinVolatility`, `VolatilityRatio`, `VolatilityBands`,\n`VolatilityAdjustedMomentum`, `VolatilitySkew`, `StdDevChannel`, `LinRegChannel`,\n`Inertia`, `Stiffness`, `TtmSqueeze`, `VolatilityOfVolatility`, `VolatilityBreak`,\n`VolatilityMomentum`, `VolatilityPercentile`, `VolatilityRegimeDetector`,\n`VolatilitySpike`, `VolatilityStop`, `RegimeVolatility`, `LogReturnVolatility`,\n`WeightedCloseVolatility`, `AccelerationBands`, `AtrPercent`, `AtrNormalizedClose`,\n`AtrRatio`, `DualATRRatio`, `WilderSmoothedRange`, `TrueRangeExpansion`,\n`TrueRangePercentile`, `TrueRangeZScore`, `TrueRangeRatio`\n\n**Volume**\n\n`Cmf`, `Obv`, `Mfi`, `Vwap`, `Vwma`, `Pvo`, `Emv`, `Kvo`, `Vpt`, `Nvi`,\n`ChaikinOsc`, `ForceIndex`, `NetVolume`, `VolumeRsi`, `VolumeSpike`,\n`VolumeTrend`, `VolumeOscillator`, `VolumeImbalance`, `Vroc`, `ObvMomentum`,\n`ClimaxVolume`, `BwMfi`, `Vzo`, `VwMomentum`, `VolumeBreadth`, `VolumeAcceleration`,\n`VolumeWeightedClose`, `VolumeAccumulation`, `VolumeDeltaOscillator`,\n`VolumeToRangeRatio`, `VolumeRateOfChange`, `VolumeSpikeRatio`, `VolumeSpikeScore`,\n`VolumeReturnCorrelation`, `VolumeTrendSlope`, `VolumePriceEfficiency`,\n`VolumePriceCorr`, `VolumePriceImpact`, `VolumeDirectionRatio`, `VolumeEnergy`,\n`VolumeExhaustion`, `VolumeFlowRatio`, `VolumeDensity`, `VolumeDeviation`,\n`VolumeClimaxRatio`, `VolumeMomentum`, `VolumeMomentumDivergence`,\n`VolumeOpenBias`, `VolumePerRange`, `VolumeRatioSignal`, `VolumeSurge`,\n`VolumeSurge2`, `VolumeUpDownRatio`, `VolumeWeightedAtr`, `VolumeWeightedRange`,\n`VolumeWeightedStdDev`, `VolumeWeightedMomentum`, `UpVolumeFraction`,\n`UpVolumeRatio`, `UpDownVolumeRatio`, `NegativeVolumeIndex`, `PositiveVolumeIndex`,\n`RelativeVolumeRank`, `RelativeVolumeScore`, `NormalizedVolume`, `MedianVolume`,\n`CumulativeVolume`, `CumulativeDelta`, `ConsecutiveVolumeGrowth`, `VolumeStreakCount`,\n`RollingVolumeCV`, `DeltaVolume`\n\n**Trend Direction / Multi-component**\n\n`Adx`, `Dmi`, `Aroon`, `AroonOscillator`, `Ichimoku`, `ParabolicSar`, `SuperTrend`,\n`ElderRay`, `ElderImpulse`, `ChandelierExit`, `Stc`, `Vortex`, `WilliamsAD`,\n`GannHiLo`, `TrendFollowingFilter`, `TrendStrength`, `TrendAngle`, `TrendScore`,\n`Alligator`, `Rwi`, `TrendAge`, `TrendConsistency`, `TrendConsistencyScore`,\n`TrendPersistence`, `TrendPurity`, `MarketRegimeFilter`, `NetHighLowCount`,\n`BullBearBalance`, `TdSequential`, `WilliamsFractal`, `KeyReversal`\n\n**Price Structure / Pattern**\n\n`PriceChannel`, `PriceCompression`, `PriceDistanceMa`, `PriceGap`, `PriceIntensity`,\n`PriceOscillator`, `PriceOscillator2`, `PricePosition`, `PriceRangePct`,\n`PriceAboveMa`, `PriceAboveMaPct`, `PriceAcceleration`, `PriceVelocity`,\n`PriceVelocityRatio`, `PriceVelocityScore`, `PriceEnvelope`, `PriceReversal`,\n`PriceReversalStrength`, `NormalizedPrice`, `DisparityIndex`, `DeviationFromMa`,\n`LinearDeviation`, `PriceDensity`, `CandleBodySize`, `CandleColor`, `CandleMomentum`,\n`CandlePattern`, `HeikinAshi`, `WickRatio`, `HighLowPct`, `HighLowPctRange`,\n`HighLowSpread`, `HlRatio`, `OpenCloseRatio`, `CloseToOpen`, `CloseLocationValue`,\n`WeightedClose`, `CloseToOpenGap`, `CloseToOpenReturn`, `HighLowReturnCorrelation`,\n`UpperWickPct`, `LowerWickPct`, `HigherHighLowerLow`, `OpenHighLowCloseAvg`,\n`CloseToLowDistance`, `ReturnMeanDeviation`, `PriceAboveRollingHigh`,\n`OpenCloseSpread`, `GapFillRatio`, `PriceCompressionRatio`, `ShadowRatio`,\n`PriceMeanDeviation`, `AbsReturnSum`, `AbsReturnMean`, `RollingMaxDrawdown`,\n`PriceRelativeStrength`, `OpenLowRange`, `HighOpenRange`, `BodyAtrRatio`,\n`GapStreak`, `BarEfficiency`, `MedianBodySize`, `WickAsymmetryStreak`,\n`FibonacciRetrace`, `PriceEntropyScore`, `PriceCompressionIndex`,\n`PriceCompressionBreakout`, `PriceSymmetry`, `PricePathEfficiency`,\n`PriceEfficiencyRatio`, `PriceGravity`, `PriceImpulse`, `PriceBandwidth`,\n`PriceLevelPct`, `PricePositionRank`, `PriceRangeExpansion`, `PriceRangeRank`,\n`PriceToSmaRatio`, `PriceZScore`, `PriceOscillatorPct`, `PriceOscillatorSign`,\n`PriceChangeCount`, `PriceChangePct`, `PriceChannelPosition`, `PriceChannelWidth`,\n`PriceGapFrequency`, `OpenToHighRatio`, `RangeMomentum`, `RangePersistence`,\n`RangeReturnRatio`, `RangeCompressionRatio`, `RangeContractionCount`,\n`RangeExpansionIndex`, `RangeMidpointPosition`, `RangePctOfClose`,\n`RangeTrendSlope`, `RangeZScore`, `RangeEfficiency`, `RangeBreakoutCount`,\n`RangeReturnRatio`, `CloseMidpointDiff`, `CloseMidpointStrength`,\n`CloseAboveMidpoint`, `CloseVsOpenRange`, `CloseVsPriorHigh`, `CloseVsVwap`,\n`ClosePositionInRange`, `CloseRetracePct`, `CloseReturnAcceleration`,\n`CloseReturnZ`, `CloseToHighRatio`, `CloseToMidRange`, `CloseToRangeTop`,\n`CloseRelativeToEma`, `CloseRelativeToRange`, `ClosePctFromHigh`, `ClosePctFromLow`,\n`CloseAboveEma`, `CloseAboveOpen`, `CloseAbovePrevClose`, `CloseAbovePrevClosePct`,\n`CloseAbovePrevHigh`, `CloseAbovePriorClose`, `CloseAboveSmaStreak`,\n`CloseAboveHighPrev`, `CloseBelowLowPrev`, `CloseDistanceFromEma`, `CloseDistanceFromOpen`, `CloseHighFrequency`,\n`CloseMinusOpenMa`, `CloseOpenEma`, `CloseAcceleration`, `CloseAccelerationSign`,\n`OpenAbovePrevClose`, `OpenCloseMomentum`, `OpenGapDirection`, `OpenGapPct`,\n`OpenGapSize`, `OpenHighRatio`, `OpenRangeStrength`, `OpenToCloseRatio`,\n`OpenToCloseReturn`, `OpenCloseSymmetry`, `OpenDrive`, `OpenMidpointDeviation`,\n`OvernightReturn`, `IntrabarReturn`,\n`HighBreakCount`, `HigherCloseStreak`, `HigherHighCount`, `HigherLowCount`,\n`HigherLowStreak`, `HighLowCrossover`, `HighLowDivergence`, `HighLowMidpoint`,\n`HighLowOscillator`, `HighOfPeriod`, `LowOfPeriod`, `LowerHighCount`,\n`LowerHighStreak`, `LowerLowCount`, `LowerShadowRatio`, `UpperShadowRatio`,\n`UpperToLowerWick`, `ShadowImbalance`, `WickImbalance`, `WickToAtrRatio`,\n`WickToBodyRatio`, `WickRejectionScore`, `BodyDirectionRatio`, `BodyFillRatio`,\n`BodyHeightRatio`, `BodySizeRank`, `BodyStreak`, `BodyToRangeRatio`,\n`BarCloseRank`, `BarFollowThrough`, `BarMomentumIndex`, `BarMomentumScore`,\n`BarOpenPosition`, `BarOverlapRatio`, `BarRangeConsistency`, `BarRangeExpansionPct`,\n`BarRangeStdDev`, `BarStrengthIndex`, `BarType`, `BearishBarRatio`,\n`BodyPosition`, `BodyToShadowRatio`, `HighVolumeBarRatio`,\n`CandleEfficiency`, `CandleRangeMa`, `CandleSymmetry`, `FlatBarPct`,\n`NarrowRangeBar`, `UpBarRatio`, `NetBarBias`, `ThreeBarPattern`,\n`EngulfingDetector`, `EngulfingPattern`, `HammerDetector`, `HammerPattern`,\n`DojiDetector`, `InsideBarCounter`, `InsideBarRatio`, `OutsideBarCount`\n\n**Statistical / Adaptive**\n\n`StdDev`, `PercentRank`, `Fisher`, `MassIndex`, `PsychologicalLine`, `KaufmanEr`,\n`ZScore`, `Bop`, `Atrp`, `Envelope`, `Pivots`, `PivotDistance`, `PivotPoint`,\n`PivotStrength`, `SupportResistanceDistance`, `AtrStop`, `ChangeFromHigh`,\n`BarsSince`, `ConsecutiveBars`, `SwingIndex`, `Dsp`, `Usm`, `Vam`,\n`LinRegR2`, `UlcerIndex`, `MeanReversionScore`, `MaxDrawdownWindow`,\n`MaxAdverseExcursion`, `MaxDrawupWindow`, `RangeFilter`, `RangeRatio`,\n`GapDetector`, `GapFillDetector`, `GapMomentum`, `GapRangeRatio`, `GapSignal`,\n`SignedGapSum`, `AverageGap`, `AnchoredVwap`, `LaguerreRsi`, `BullBearPower`,\n`BullPowerBearPower`, `VixFix`, `RocRatio`, `TypicalPrice`, `TypicalPriceDeviation`,\n`MedianPrice`, `MedianCloseDev`, `MedianReturnDeviation`, `RollingMAD`,\n`RollingKurtosis`, `RollingSkewness`, `RollingReturnKurtosis`, `RollingSkewReturns`,\n`RollingMaxReturn`, `RollingMinReturn`, `RollingCorrelation`, `RollingHighLowPosition`,\n`RollingHighLowRatio`, `RollingLowBreak`, `RollingOpenBias`, `RollingMaxDd`,\n`AutoCorrelation1`, `ReturnAutoCorrelation`, `ReturnDispersion`, `ReturnIqr`,\n`ReturnPersistence`, `ReturnSignChanges`, `ReturnSignSum`, `ReturnAboveZeroPct`,\n`ReturnOverVolatility`, `ReturnPercentRank`, `CumulativeLogReturn`,\n`DailyReturnSkew`, `DirectionChanges`, `DirectionalEfficiency`, `EfficiencyRatio`, `DownsideDeviation`,\n`EaseOfMovement`, `FairValueGap`, `HurstExponent`, `AverageBarRange`,\n`AverageGain`, `AverageLoss`, `AmplitudeRatio`, `Zscore`, `ZigZag`,\n`ValueAtRisk5`, `ConditionalVar5`, `PayoffRatio`, `ProfitFactor`,\n`VarianceRatio`, `ConsolidationScore`, `SupportTestCount`,\n`CusumPriceChange`, `NewHighPct`, `NewHighStreak`, `NewLowPct`,\n`RelativeBarRange`, `RelativeClose`, `TailRatio`, `TailRatioPct`,\n`BreakoutSignal`, `MidpointOscillator`, `IntradaySpreadPct`,\n`OhlcSpread`, `RobustZScore`, `RollingShadowBalance`, `AtrPercentile`\n\n**Core formulas:**\n\n| Indicator | Formula | Warm-up bars |\n|-----------|---------|-------------|\n| **SMA(n)** | `sum(close, n) / n` | n |\n| **EMA(n)** | `close × k + prev × (1−k)`, `k = 2/(n+1)` | n |\n| **RSI(n)** | `100 − 100 / (1 + avg_gain / avg_loss)` Wilder smoothing | n + 1 |\n| **ATR(n)** | Wilder-smoothed true range | n |\n| **MACD(f,s,sig)** | `EMA(f) − EMA(s)`; signal = `EMA(sig)` of MACD | slow + signal |\n| **Fibonacci(n)** | Swing high/low over `n` bars; 0/23.6/38.2/50/61.8/100% levels | n |\n| **VolumeReturnCorrelation(n)** | Pearson r between close return and volume | n + 1 |\n| **PriceEntropyScore(n)** | Shannon entropy of up/flat/down bins, normalized to [0,1] | n + 1 |\n| **VolatilityOfVolatility(n)** | Std dev of rolling ATR values | 2n − 1 |\n\n---\n\n## OhlcvSeries Analytics (370+)\n\n`OhlcvSeries` ships an extensive built-in analytics library. A selection:\n\n**Returns \u0026 Volatility**: `realized_volatility`, `rolling_sharpe`, `hurst_exponent`,\n`ulcer_index`, `cvar`, `skewness`, `kurtosis`, `autocorrelation`, `std_dev`,\n`close_returns`, `log_returns`, `drawdown_series`, `max_drawdown`, `max_drawdown_pct`\n\n**Volume**: `vwap`, `vwap_deviation`, `volume_price_correlation`, `relative_volume`,\n`volume_spike`, `up_down_volume_ratio`, `net_volume`, `volume_weighted_return`,\n`close_above_vwap_pct`, `volume_coefficient_of_variation`, `avg_volume_on_up_bars`,\n`avg_volume_on_down_bars`\n\n**Momentum \u0026 Trend**: `close_momentum`, `price_velocity`, `price_acceleration`,\n`close_momentum_ratio`, `recent_close_trend`, `trend_strength`, `trend_consistency`,\n`momentum_score`, `close_above_ma_streak`, `bars_above_ma`, `bars_above_sma`\n\n**Candle Patterns**: `count_doji`, `pct_doji`, `bullish_engulfing_count`,\n`bearish_engulfing_count`, `is_hammer`, `is_shooting_star`, `is_marubozu`,\n`inside_bar_count`, `outside_bar_count`, `candle_symmetry`, `candle_color_changes`\n\n**Range \u0026 Structure**: `atr_series`, `true_range_series`, `high_low_range`,\n`price_contraction`, `range_expansion_ratio`, `close_distance_from_high`,\n`pct_from_low`, `is_breakout_up`, `reversal_count`, `open_gap_fill_rate`,\n`pivot_highs`, `pivot_lows`\n\n**Streaks**: `consecutive_higher_closes`, `consecutive_higher_highs`,\n`consecutive_lower_lows`, `longest_winning_streak`, `longest_losing_streak`,\n`longest_flat_streak`, `bars_since_new_high`, `bars_since_new_low`\n\n---\n\n## SignalValue Combinators (70+)\n\n`SignalValue` carries a scalar or `Unavailable` and propagates unavailability\nthrough every operation:\n\n```rust\nsv.abs() / sv.negate() / sv.signum()\nsv.clamp(lo, hi)                   // clamp to [lo, hi]\nsv.cap_at(max) / sv.floor_at(min)  // one-sided clamps\nsv.lerp(other, t)                  // linear interpolation, t ∈ [0, 1]\nsv.blend(other, weight)            // weighted blend\nsv.quantize(step)                  // round to nearest multiple of step\nsv.distance_to(other)              // absolute difference\nsv.delta(prev)                     // signed change\nsv.cross_above(prev, threshold)    // true on upward threshold cross\nsv.within_range(lo, hi)            // boolean range test\nsv.as_percent() / sv.pct_of(base)  // percentage helpers\nsv.sign_match(other)               // true if same sign\nsv.map(f) / sv.zip_with(other, f)  // functor / applicative style\n```\n\n---\n\n## SignalMap Analytics (90+)\n\n`SignalMap` is the output of `SignalPipeline::update`. Fleet-wide analytics:\n\n```rust\nmap.average_scalar()          // mean of all scalar values\nmap.std_dev() / .variance()   // dispersion\nmap.z_scores()                // HashMap\u003cString, f64\u003e z-score per signal\nmap.entropy()                 // Shannon entropy of the distribution\nmap.gini_coefficient()        // Gini inequality coefficient\nmap.normalize_all()           // min-max normalize all scalars to [0, 1]\nmap.top_n(3) / .bottom_n(3)   // top/bottom signals by value\nmap.weighted_sum(\u0026weights)    // dot product with weight map\nmap.scale_all(factor)         // multiply all scalars by factor\nmap.percentile_rank_of(name)  // percentile of one signal among all\nmap.signal_ratio(a, b)        // ratio of two named signals\nmap.count_positive() / .count_negative() / .count_zero()\nmap.all_positive() / .all_negative()\n```\n\n---\n\n## Signal Warmup Contracts\n\nEvery indicator has an implicit warmup period. `signals::warmup` makes that\nperiod queryable, enforceable, and reportable:\n\n| Type | Purpose |\n|------|---------|\n| `WarmupContract` | Trait: `warmup_period()`, `is_ready()`, `bars_remaining()` — implemented by all `Signal` types |\n| `WarmupGuard\u003cS\u003e` | Wraps any signal; returns `Err(NotReady)` until warmup completes instead of silent `Unavailable` |\n| `WarmupReporter` | Tracks warmup progress for N signals and produces `WarmupReport` snapshots |\n| `WarmupReport` | `all_ready()`, `pipeline_bars_remaining()`, `warming_signals()`, `display()` |\n\n```rust\nuse fin_primitives::signals::indicators::Sma;\nuse fin_primitives::signals::{BarInput, Signal};\nuse fin_primitives::signals::warmup::{WarmupContract, WarmupGuard, WarmupReporter};\nuse rust_decimal_macros::dec;\n\n// Guard: explicit error instead of silent Unavailable\nlet sma = Sma::new(\"sma5\", 5).unwrap();\nlet mut guard = WarmupGuard::new(sma);\n\nfor _ in 0..4 {\n    let bar = BarInput::from_close(dec!(100));\n    // Err(NotReady { bars_remaining: 4, 3, 2, 1 })\n    assert!(guard.update_checked(\u0026bar).is_err());\n}\n// 5th bar — Ok(SignalValue::Scalar(...))\nassert!(guard.update_checked(\u0026BarInput::from_close(dec!(100))).is_ok());\n\n// Reporter: pipeline-level warmup snapshot\nlet mut reporter = WarmupReporter::new(\n    vec![5, 14, 20],\n    vec![\"sma5\".into(), \"rsi14\".into(), \"bb20\".into()],\n);\nreporter.tick_n(5); // 5 bars consumed\nlet report = reporter.report(reporter.bars_consumed());\nassert!(report.statuses[0].is_ready);   // sma5 done\nassert!(!report.statuses[1].is_ready);  // rsi14 still warming\nprintln!(\"{}\", report.display());\n// WarmupReport [bars_consumed=5, ready=1/3, pipeline_remaining=15]\n//   [READY]   sma5  (period=5)\n//   [WARMING] rsi14 (period=14, remaining=9)\n//   [WARMING] bb20  (period=20, remaining=15)\n```\n\n---\n\n## Signal Composition Engine\n\n`signals::compose` provides a composable expression-tree DSL for building\nderived signals from existing indicators without writing bespoke structs.\n\n### Expression Nodes\n\n| Node | Description |\n|------|-------------|\n| `Raw(name)` | Leaf: raw output of a named indicator |\n| `Add(a, b)` | Element-wise sum; `Unavailable` if either is |\n| `Sub(a, b)` | `a - b`; `Unavailable` if either is |\n| `Mul(expr, f)` | Scale by constant `f` |\n| `Lag(expr, n)` | Delay by `n` bars; `Unavailable` until buffer fills |\n| `Normalize(expr, method, window)` | `MinMax`, `ZScore`, or `Percentile` normalisation |\n| `Threshold(expr, level, dir)` | `Above` → `+1/0`, `Below` → `-1/0`, `Cross` → `+1/-1/0` |\n\n### Fluent `SignalBuilder` API\n\n```rust\nuse fin_primitives::signals::indicators::Rsi;\nuse fin_primitives::signals::{BarInput, Signal};\nuse fin_primitives::signals::compose::{SignalBuilder, NormMethod, Direction};\nuse rust_decimal_macros::dec;\n\n// RSI(14) → lag(1) → ZScore(20) → threshold(+2σ, Above)\nlet rsi = Rsi::new(\"rsi14\", 14).unwrap();\nlet mut momentum_signal = SignalBuilder::new(rsi)\n    .lag(1)\n    .normalize_window(NormMethod::ZScore, 20)\n    .threshold(dec!(2), Direction::Above)\n    .build_named(\"rsi_zscore_cross\");\n\n// Feed bars; signal returns Scalar(1) when RSI z-score crosses above +2σ\nlet bar = BarInput::from_close(dec!(100));\nlet _ = momentum_signal.update(\u0026bar); // Unavailable during warmup\n\n// Combine two signals manually\nuse fin_primitives::signals::compose::{SignalExpr, ComposedSignal};\nuse fin_primitives::signals::indicators::Sma;\n\nlet sma_fast = Sma::new(\"sma5\", 5).unwrap();\nlet sma_slow = Sma::new(\"sma20\", 20).unwrap();\nlet expr = SignalExpr::raw(\"sma5\")\n    .sub(SignalExpr::raw(\"sma20\"))      // MACD-style crossover\n    .threshold(dec!(0), Direction::Cross);\nlet leaves: Vec\u003cBox\u003cdyn Signal\u003e\u003e = vec![Box::new(sma_fast), Box::new(sma_slow)];\nlet mut cross = ComposedSignal::new(\"fast_slow_cross\", expr, leaves).unwrap();\n```\n\n### Normalisation Methods\n\n| Method | Formula | Output |\n|--------|---------|--------|\n| `MinMax` | `(v - min) / (max - min)` | `[0, 1]` |\n| `ZScore` | `(v - mean) / std_dev` | Unbounded; typically `[-3, +3]` |\n| `Percentile` | Rank within rolling window | `[0, 1]` |\n\n---\n\n## Risk Attribution\n\n`risk::attribution` decomposes portfolio risk into six named factors and supports\nBrinson-Hood-Beebower P\u0026L attribution. Use `RiskMonitor::attribution_report` or\nconstruct `RiskAttributor` directly.\n\n### Six-Factor Risk Decomposition\n\n| Factor | Driver | Estimation |\n|--------|--------|------------|\n| `Market` | Systematic beta exposure | `β² × σ²_market` |\n| `Sector` | Industry concentration | HHI × `σ²_market × 0.5` |\n| `Idiosyncratic` | Stock-specific residual | `Σ w_i² × σ²_idio_i` |\n| `Leverage` | Borrowed capital amplification | `(L−1)² × σ²_market` |\n| `Concentration` | Large single-position weight | HHI × `σ²_market × 0.3` |\n| `Liquidity` | Illiquid exit risk | `(1 − avg_liquidity) × σ²_market` |\n\n```rust\nuse fin_primitives::risk::RiskMonitor;\nuse fin_primitives::risk::attribution::{MarketData, RiskAttributor};\nuse fin_primitives::position::PositionLedger;\nuse rust_decimal_macros::dec;\n\nlet ledger = PositionLedger::new(dec!(100_000));\n// Populate ledger with positions via ledger.apply_fill(...)\n\nlet market_data = MarketData::new(0.15)   // 15% annualised market vol\n    .with_beta(\"AAPL\", 1.2)\n    .with_beta(\"MSFT\", 0.9)\n    .with_sector(\"AAPL\", \"Technology\")\n    .with_sector(\"MSFT\", \"Technology\")\n    .with_liquidity(\"AAPL\", 0.98)\n    .with_idio_vol(\"AAPL\", 0.25);\n\n// Via RiskMonitor (one-liner)\nlet monitor = RiskMonitor::new(dec!(100_000));\nlet report = monitor.attribution_report(\u0026ledger, market_data.clone());\n\nprintln!(\"{}\", report.summary());\n// AttributionReport [equity=..., total_risk=..., beta=1.05, hhi=0.5, leverage=1.0x]\n//   Market (Beta)        42.3%  (...)\n//   Sector               18.1%  (...)\n//   Idiosyncratic        27.4%  (...)\n//   ...\n\n// Or directly via RiskAttributor\nlet attributor = RiskAttributor::new(\u0026ledger, market_data);\nlet report = attributor.compute();\nlet dominant = report.dominant_factor().unwrap();\nprintln!(\"Largest risk factor: {}\", dominant.factor.name());\n```\n\n### Brinson-Hood-Beebower P\u0026L Attribution\n\n```rust\nuse fin_primitives::risk::attribution::{RiskAttributor, BhbInput, BhbSectorInput, MarketData};\nuse fin_primitives::position::PositionLedger;\nuse rust_decimal_macros::dec;\n\nlet ledger = PositionLedger::new(dec!(100_000));\nlet attributor = RiskAttributor::new(\u0026ledger, MarketData::default());\n\nlet bhb = attributor.compute_bhb(\u0026BhbInput {\n    benchmark_total_return: 0.05,\n    sectors: vec![\n        BhbSectorInput {\n            sector: \"Technology\".into(),\n            portfolio_weight:   0.60,  // overweight vs benchmark\n            benchmark_weight:   0.40,\n            portfolio_sector_return: 0.08,\n            benchmark_sector_return: 0.06,\n        },\n        BhbSectorInput {\n            sector: \"Energy\".into(),\n            portfolio_weight:   0.40,\n            benchmark_weight:   0.60,\n            portfolio_sector_return: 0.02,\n            benchmark_sector_return: 0.04,\n        },\n    ],\n});\n\nprintln!(\"Active return: {:.2}%\", bhb.total_active_return * 100.0);\nprintln!(\"  Allocation:   {:.4}\", bhb.total_allocation);\nprintln!(\"  Selection:    {:.4}\", bhb.total_selection);\nprintln!(\"  Interaction:  {:.4}\", bhb.total_interaction);\n```\n\n---\n\n## PositionLedger Analytics (145+)\n\n```rust\nledger.equity(\u0026prices)                      // cash + unrealized P\u0026L\nledger.total_unrealized_pnl(\u0026prices)        // sum of all open position P\u0026L\nledger.concentration_ratio()               // Herfindahl-Hirschman Index\nledger.long_exposure() / .short_exposure()  // directional gross exposure\nledger.avg_long_entry_price()               // VWAP of long entries\nledger.avg_short_entry_price()              // VWAP of short entries\nledger.pct_long() / .pct_short()            // directional balance\nledger.win_rate()                           // % of closed positions with positive P\u0026L\nledger.largest_position() / .smallest_position()\nledger.symbols_with_unrealized_loss(\u0026prices)\nledger.risk_reward_ratio()\nledger.kelly_fraction()\n```\n\n---\n\n## DrawdownTracker Analytics (120+)\n\n```rust\ntracker.current_drawdown_pct()      // (peak − equity) / peak × 100\ntracker.max_drawdown_pct()          // worst drawdown seen\ntracker.calmar_ratio()              // annualized return / max drawdown\ntracker.sharpe_ratio()              // using per-update equity changes\ntracker.sortino_ratio()             // downside-deviation adjusted\ntracker.win_rate()                  // fraction of updates that gained equity\ntracker.avg_gain_pct()              // average gain per gaining update\ntracker.avg_loss_pct()              // average loss per losing update\ntracker.equity_change_std()         // std dev of per-update equity changes\ntracker.gain_loss_asymmetry()       // ratio of avg gain magnitude to avg loss magnitude\ntracker.recovery_factor()           // net return / max drawdown\ntracker.omega_ratio()               // probability-weighted gain/loss ratio\ntracker.equity_multiple()           // current / initial equity\ntracker.return_drawdown_ratio()     // net return % / worst drawdown %\ntracker.streak_win_rate()           // max_gain_streak / total streak length\ntracker.time_to_recover_est()       // estimated updates to recover from current drawdown\n```\n\n---\n\n## Options Greeks \u0026 Black-Scholes\n\nThe `greeks` module provides a zero-panic European option pricing engine.\n\n```rust\nuse fin_primitives::greeks::{BlackScholes, OptionSpec, OptionType, SpreadGreeks};\nuse rust_decimal_macros::dec;\n\nfn main() -\u003e Result\u003c(), fin_primitives::FinError\u003e {\n    let spec = OptionSpec {\n        strike:          dec!(100),\n        expiry_days:     30,\n        spot:            dec!(100),\n        risk_free_rate:  dec!(0.05),\n        volatility:      dec!(0.20),\n        option_type:     OptionType::Call,\n    };\n\n    // Theoretical price\n    let price = BlackScholes::price(\u0026spec)?;\n\n    // All five Greeks\n    let g = BlackScholes::greeks(\u0026spec)?;\n    println!(\"delta={} gamma={} theta={} vega={} rho={}\", g.delta, g.gamma, g.theta, g.vega, g.rho);\n\n    // Implied volatility from a market quote\n    let iv = BlackScholes::implied_vol(price, \u0026spec)?;\n\n    // Multi-leg spreads\n    let straddle = SpreadGreeks::straddle(dec!(100), dec!(100), 30, dec!(0.05), dec!(0.20));\n    let net = straddle.net_greeks()?;\n    println!(\"straddle net delta ≈ {}\", net.delta);\n    Ok(())\n}\n```\n\n**Spread constructors:**\n\n| Constructor | Description |\n|---|---|\n| `SpreadGreeks::bull_call_spread(…)` | Long low-strike call, short high-strike call |\n| `SpreadGreeks::bear_put_spread(…)` | Long high-strike put, short low-strike put |\n| `SpreadGreeks::straddle(…)` | Long ATM call + long ATM put |\n| `SpreadGreeks::iron_condor(…)` | Short put spread + short call spread |\n| `SpreadGreeks::new(legs)` | Arbitrary legs with signed quantities |\n\n**Formulas:**\n\n| Greek | Formula |\n|---|---|\n| delta | ∂V/∂S (`N(d₁)` call, `N(d₁)−1` put) |\n| gamma | φ(d₁) / (S σ √T) |\n| theta | −(S φ(d₁) σ) / (2√T) ± r K e^{−rT} N(±d₂), per day |\n| vega  | S φ(d₁) √T / 100 (per 1 vol-point) |\n| rho   | ±K T e^{−rT} N(±d₂) / 100 (per 1 rate-point) |\n\nImplied vol is solved by bisection over `[1e-6, 5.0]` (up to 200 iterations, tolerance 1e-7).\n\n---\n\n## Regime Detection Engine\n\nThe `regime` module classifies the current market state using four complementary\nquantitative signals, then adapts strategy parameters per regime.\n\n### Regimes\n\n| Regime | Primary Signal | Condition |\n|--------|---------------|-----------|\n| `Trending` | Hurst exponent | H \u003e 0.6 (persistent process) |\n| `MeanReverting` | Hurst exponent | H \u003c 0.4 (anti-persistent) |\n| `HighVolatility` | Realized vol / long-run mean | ratio \u003e 2.0x; GARCH confirms |\n| `LowVolatility` | Realized vol / long-run mean | ratio \u003c 0.5x; BB width compressed |\n| `Crisis` | Cross-asset correlation | \u003e 60% of pairs decorrelated simultaneously |\n| `Neutral` | — | No dominant signal |\n| `Unknown` | — | Warm-up phase incomplete |\n\nPriority order: `Crisis \u003e HighVolatility \u003e Trending \u003e MeanReverting \u003e LowVolatility \u003e Neutral`\n\n### GARCH(1,1) Persistent Volatility\n\nThe `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.\n\n```rust\nuse fin_primitives::regime::{Garch11, RegimeDetector, RegimeConfig, MarketRegime, RegimeConditionalSignal};\nuse fin_primitives::signals::BarInput;\nuse rust_decimal_macros::dec;\n\n// ── Standalone GARCH ──────────────────────────────────────────────────────────\nlet mut garch = Garch11::new(0.1, 0.85, 1e-6).unwrap();\nlet log_returns = [-0.01_f64, 0.02, -0.03, 0.015, -0.025];\nfor ret in log_returns {\n    let sigma = garch.update(ret);\n    println!(\"σ = {sigma:.6}  elevated = {}\", garch.is_vol_elevated(1.5));\n}\nprintln!(\"long-run σ = {:.6}\", garch.long_run_sigma());\n\n// ── Full regime detector ──────────────────────────────────────────────────────\nlet mut detector = RegimeDetector::new(14, RegimeConfig::default()).unwrap();\n\nlet bars = vec![\n    BarInput::new(dec!(100), dec!(102), dec!(98), dec!(100), dec!(5_000)),\n    // ... more bars\n];\n\nfor bar in \u0026bars {\n    let (regime, confidence) = detector.update(bar, \u0026[]).unwrap();\n    // cross_returns: \u0026[(asset_idx, log_return)] for multi-asset crisis detection\n    // e.g. detector.update(bar, \u0026[(1, -0.02), (2, 0.01)]).unwrap()\n\n    println!(\"[{}] regime = {regime}  confidence = {confidence:.2}  risk_off = {}\",\n        bar.close, regime.short_code(), regime.is_risk_off());\n}\n\n// Inspect regime history\nfor epoch in detector.history() {\n    println!(\"  {:?}  started_at={} confidence={:.2} duration={:?}\",\n        epoch.regime, epoch.started_at_bar, epoch.confidence, epoch.duration_bars());\n}\n```\n\n### Regime-Conditional Signal Adaptation\n\n`RegimeConditionalSignal` applies RSI with a short period in trending markets,\na longer period when mean-reverting, and suppresses the signal entirely in crisis:\n\n```rust\nuse fin_primitives::regime::{RegimeConditionalSignal, MarketRegime};\nuse fin_primitives::signals::BarInput;\nuse rust_decimal_macros::dec;\n\nlet mut signal = RegimeConditionalSignal::new(\n    14,  // RSI period in Trending regime\n    21,  // RSI period in MeanReverting regime\n    14,  // RSI period in all other non-risk-off regimes\n).unwrap();\n\nlet bar = BarInput::new(dec!(100), dec!(102), dec!(98), dec!(100), dec!(1000));\n\nmatch signal.update(\u0026bar, MarketRegime::Trending) {\n    Some(Ok(rsi)) =\u003e println!(\"RSI(14) in trending = {rsi:.2}\"),\n    Some(Err(e))  =\u003e eprintln!(\"error: {e}\"),\n    None          =\u003e println!(\"signal suppressed (warm-up or risk-off)\"),\n}\n// Crisis/Unknown → None (flat signal, no trading)\nassert!(signal.update(\u0026bar, MarketRegime::Crisis).is_none());\n```\n\n### RegimeConfig Thresholds\n\n```rust\nRegimeConfig {\n    hurst_trending:              0.6,   // H \u003e 0.6 → Trending\n    hurst_mean_reverting:        0.4,   // H \u003c 0.4 → MeanReverting\n    vol_high_multiplier:         2.0,   // realized vol \u003e 2x long-run mean → HighVolatility\n    vol_low_multiplier:          0.5,   // realized vol \u003c 0.5x long-run mean → LowVolatility\n    adx_trend_threshold:        25.0,   // ADX above this confirms trend\n    bb_width_quiet:             0.02,   // BB width below this confirms LowVolatility\n    crisis_correlation_threshold: 0.3, // |r| below this = decorrelated pair\n    crisis_pair_fraction:        0.6,  // 60%+ of pairs decorrelated → Crisis\n    garch_alpha:                 0.1,  // GARCH innovation weight\n    garch_beta:                  0.85, // GARCH persistence weight\n    garch_omega:                 1e-6, // GARCH long-run floor\n    garch_vol_multiplier:        1.5,  // GARCH sigma multiplier for high-vol flag\n}\n```\n\n---\n\n## Walk-Forward Optimizer (Grid Search)\n\nThe `backtest::walk_forward` module provides proper out-of-sample validation\nvia a rolling train/test split with parameter grid search.\n\n### Algorithm\n\n```text\n|────── train ──────|── test ──|\n       step ──►\n               |────── train ──────|── test ──|\n```\n\nFor each window:\n1. Grid search over all parameter combinations on the **training** slice.\n2. Select parameters that maximize in-sample Sharpe ratio.\n3. Evaluate those parameters on the **held-out test** slice.\n4. Record `WfPeriod` with both IS and OOS metrics.\n\nAggregate: `aggregate_sharpe = mean(OOS Sharpe)`, `stability_score = fraction of OOS windows with positive Sharpe`.\n\n### Example\n\n```rust\nuse fin_primitives::backtest::walk_forward::{WalkForwardOptimizer, WalkForwardConfig, ParamRange};\nuse fin_primitives::backtest::{BacktestConfig, Signal, SignalDirection, Strategy};\nuse fin_primitives::ohlcv::OhlcvBar;\nuse std::collections::HashMap;\nuse rust_decimal_macros::dec;\n\n// ── Define a parametric strategy ─────────────────────────────────────────────\nstruct SmaStrategy { period: usize, bar_count: usize, window: std::collections::VecDeque\u003crust_decimal::Decimal\u003e }\n\nimpl Strategy for SmaStrategy {\n    fn on_bar(\u0026mut self, bar: \u0026OhlcvBar) -\u003e Option\u003cSignal\u003e {\n        let close = bar.close.value();\n        self.window.push_back(close);\n        if self.window.len() \u003e self.period { self.window.pop_front(); }\n        if self.window.len() \u003c self.period { return None; }\n        let sma: rust_decimal::Decimal = self.window.iter().sum::\u003crust_decimal::Decimal\u003e()\n            / rust_decimal::Decimal::from(self.period);\n        let dir = if close \u003e sma { SignalDirection::Buy } else { SignalDirection::Sell };\n        Some(Signal::new(dir, dec!(1)))\n    }\n}\n\n// ── Configure the optimizer ───────────────────────────────────────────────────\nlet config = WalkForwardConfig {\n    train_window: 120,  // 120 bars for in-sample fitting\n    test_window:   30,  // 30 bars for out-of-sample evaluation\n    step:          30,  // advance by 30 bars each iteration\n    param_space: vec![\n        ParamRange { name: \"sma_period\".to_owned(), min: 5.0, max: 25.0, step: 5.0 },\n    ],\n};\n\nlet bt_config = BacktestConfig::new(dec!(100_000), dec!(0.001)).unwrap();\nlet optimizer = WalkForwardOptimizer::new(config, bt_config).unwrap();\n\nlet bars: Vec\u003cOhlcvBar\u003e = vec![/* ... historical bars */];\n\nlet result = optimizer.run(\u0026bars, |train_bars, params| {\n    let period = params.get(\"sma_period\").copied().unwrap_or(10.0) as usize;\n    Box::new(SmaStrategy { period, bar_count: 0, window: Default::default() })\n}).unwrap();\n\n// ── Interpret results ─────────────────────────────────────────────────────────\nprintln!(\"Periods evaluated:    {}\", result.periods.len());\nprintln!(\"Aggregate OOS Sharpe: {:.2}\", result.aggregate_sharpe);\nprintln!(\"Stability score:      {:.1}%\", result.stability_score * 100.0);\nprintln!(\"Mean OOS return:      {:.2}%\", result.mean_oos_return * 100.0);\nprintln!(\"Worst OOS drawdown:   {:.2}%\", result.worst_oos_drawdown * 100.0);\n\n// Robustness check: Sharpe \u003e 0 and at least 65% of windows profitable\nif result.is_robust(0.65) {\n    println!(\"Strategy PASSED walk-forward robustness check\");\n}\n\n// Per-period detail\nfor (i, period) in result.periods.iter().enumerate() {\n    println!(\"  [WF {}] IS Sharpe={:.2}  OOS Sharpe={:.2}  best_params={:?}\",\n        i, period.in_sample_sharpe, period.out_of_sample_sharpe, period.best_params);\n}\n\n// Best / worst OOS windows\nif let Some(best) = result.best_period() {\n    println!(\"Best OOS window:  bars {}–{} (Sharpe {:.2})\", best.test_start, best.test_end, best.out_of_sample_sharpe);\n}\n```\n\n### `WalkForwardResult` Fields\n\n| Field | Description |\n|---|---|\n| `periods` | `Vec\u003cWfPeriod\u003e` — one entry per rolling window |\n| `aggregate_sharpe` | Mean out-of-sample Sharpe across all periods |\n| `stability_score` | Fraction of OOS windows with positive Sharpe; `[0, 1]` |\n| `mean_oos_return` | Mean OOS total return across all periods |\n| `worst_oos_drawdown` | Maximum OOS drawdown seen across any single period |\n\n### `WfPeriod` Fields\n\n| Field | Description |\n|---|---|\n| `train_start / train_end` | Bar index range of the training window |\n| `test_start / test_end` | Bar index range of the test window |\n| `best_params` | `HashMap\u003cString, f64\u003e` — winning parameter combination |\n| `in_sample_sharpe` | Best Sharpe achieved on training data |\n| `out_of_sample_sharpe` | Sharpe achieved on held-out test data |\n| `oos_result` | Full `BacktestResult` for the OOS period |\n\n---\n\n## Backtester with Walk-Forward Optimization\n\nThe `backtest` module provides a bar-by-bar event-driven backtester and a\nrolling walk-forward optimizer.\n\n```rust\nuse fin_primitives::backtest::{\n    Backtester, BacktestConfig, Signal, SignalDirection, Strategy, WalkForwardOptimizer,\n};\nuse fin_primitives::ohlcv::OhlcvBar;\nuse rust_decimal_macros::dec;\n\n// 1. Implement the Strategy trait\nstruct MomentumStrategy { last_close: Option\u003crust_decimal::Decimal\u003e }\n\nimpl Strategy for MomentumStrategy {\n    fn on_bar(\u0026mut self, bar: \u0026OhlcvBar) -\u003e Option\u003cSignal\u003e {\n        let close = bar.close.value();\n        let dir = match self.last_close {\n            Some(prev) if close \u003e prev =\u003e SignalDirection::Buy,\n            Some(prev) if close \u003c prev =\u003e SignalDirection::Sell,\n            _ =\u003e SignalDirection::Hold,\n        };\n        self.last_close = Some(close);\n        Some(Signal::new(dir, dec!(1)))\n    }\n}\n\nfn main() -\u003e Result\u003c(), fin_primitives::FinError\u003e {\n    // 2. Configure and run\n    let config = BacktestConfig::new(dec!(100_000), dec!(0.001))?;\n    let bars: Vec\u003cOhlcvBar\u003e = vec![/* … */];\n    let result = Backtester::new(config.clone()).run(\u0026bars, \u0026mut MomentumStrategy { last_close: None })?;\n\n    println!(\"total_return={:.2}%  sharpe={:.2}  max_dd={:.2}%  trades={}\",\n        result.total_return * dec!(100),\n        result.sharpe_ratio,\n        result.max_drawdown * dec!(100),\n        result.trade_count);\n\n    // 3. Walk-forward optimization\n    let wfo = WalkForwardOptimizer::new(200, 50, config)?;\n    let wf = wfo.run(\u0026bars, |_train| Box::new(MomentumStrategy { last_close: None }))?;\n    println!(\"mean OOS return={:.2}%  worst dd={:.2}%\",\n        wf.mean_return * dec!(100), wf.worst_drawdown * dec!(100));\n    Ok(())\n}\n```\n\n**`BacktestResult` fields:**\n\n| Field | Description |\n|---|---|\n| `total_return` | `(final_equity − initial_capital) / initial_capital` |\n| `sharpe_ratio` | Annualised Sharpe (252-day, sample stddev), 0 if flat returns |\n| `max_drawdown` | Peak-to-trough fraction, always in `[0, 1]` |\n| `win_rate` | Fraction of closed trades with positive realized P\u0026L |\n| `trade_count` | Total fills executed |\n| `equity_curve` | `Vec\u003cDecimal\u003e` sampled once per bar |\n\n---\n\n## Async Streaming Signals\n\nThe `async_signals` module wraps any `SignalPipeline` with Tokio MPSC channels\nfor non-blocking, zero-copy signal streaming.\n\n```rust\nuse fin_primitives::async_signals::{StreamingSignalPipeline, spawn_signal_stream};\nuse fin_primitives::signals::pipeline::SignalPipeline;\nuse fin_primitives::signals::indicators::Sma;\nuse fin_primitives::ohlcv::OhlcvBar;\nuse tokio::sync::mpsc;\n\n#[tokio::main]\nasync fn main() {\n    let pipeline = SignalPipeline::new().add(Sma::new(\"sma20\", 20));\n\n    // Option A: high-level wrapper\n    let (tick_tx, mut update_rx) = StreamingSignalPipeline::new(pipeline).spawn();\n\n    // Option B: convenience function with your own tick channel\n    // let (tick_tx, tick_rx) = mpsc::channel::\u003cOhlcvBar\u003e(1024);\n    // let mut update_rx = spawn_signal_stream(pipeline, tick_rx);\n\n    // Push bars from any async producer\n    tokio::spawn(async move {\n        // tick_tx.send(bar).await.unwrap();\n        drop(tick_tx); // closing sender shuts down the pipeline task\n    });\n\n    while let Some(update) = update_rx.recv().await {\n        if update.is_ready() {\n            println!(\"{} = {}\", update.signal_name, update.value);\n        }\n    }\n}\n```\n\n**Key properties:**\n- Output buffers are pre-allocated at construction time (default: 4 096 slots).\n- The background task shuts down cleanly when all tick senders are dropped.\n- Multiple signals in the same pipeline each emit one `SignalUpdate` per bar.\n- `SignalUpdate::timestamp` carries wall-clock `DateTime\u003cUtc\u003e` of computation.\n\n---\n\n## NanoTimestamp Utilities (120+)\n\n```rust\nNanoTimestamp::now()                // current UTC nanoseconds\nts.add_days(n) / .sub_days(n)\nts.add_months(n)                    // calendar-accurate month arithmetic\nts.start_of_week() / .end_of_month()\nts.start_of_quarter()               // Jan 1 / Apr 1 / Jul 1 / Oct 1\nts.end_of_quarter()                 // last nanosecond of the quarter\nts.is_same_quarter(other)           // same calendar quarter and year\nts.floor_to_hour() / .floor_to_minute() / .floor_to_second()\nts.is_market_hours()                // 09:30–16:00 ET (approximate)\nts.is_weekend()\nts.quarter()                        // 1–4\nts.elapsed_days() / .elapsed_hours() / .elapsed_minutes()\nts.nanoseconds_between(other)\nts.lerp(other, t)                   // interpolate two timestamps\n```\n\n---\n\n## Mathematical Definitions\n\n### Price and Quantity Types\n\n| Type | Invariant | Backing type |\n|------|-----------|-------------|\n| `Price` | `d \u003e 0` (strictly positive) | `rust_decimal::Decimal` |\n| `Quantity` | `d \u003e= 0` (non-negative) | `rust_decimal::Decimal` |\n| `NanoTimestamp` | any `i64`; nanoseconds since Unix epoch (UTC) | `i64` |\n| `Symbol` | non-empty, no whitespace | `String` |\n\n### OHLCV Invariants\n\nEvery `OhlcvBar` that enters an `OhlcvSeries` has been validated to satisfy:\n\n```\nhigh \u003e= open    high \u003e= close\nlow  \u003c= open    low  \u003c= close\nhigh \u003e= low\n```\n\nAny bar that violates these relationships is rejected with `FinError::BarInvariant`.\n\n### Order Book Guarantees\n\n- Bids are maintained in descending price order (best bid = highest price).\n- Asks are maintained in ascending price order (best ask = lowest price).\n- Sequence numbers are strictly monotone; `delta.sequence` must equal `book.sequence() + 1`.\n- A delta that would produce `best_bid \u003e= best_ask` is rejected and the book is rolled back atomically.\n\n### Risk Metrics\n\n- **Drawdown %**: `(peak_equity − current_equity) / peak_equity × 100`. Always ≥ 0.\n- `MaxDrawdownRule` triggers when `drawdown_pct \u003e threshold_pct` (strictly greater).\n- `MinEquityRule` triggers when `equity \u003c floor` (strictly less).\n\n### Position P\u0026L\n\n- **Realized P\u0026L** (on reduce/close): `closed_qty × (fill_price − avg_cost)` for long.\n- **Unrealized P\u0026L**: `position_qty × (current_price − avg_cost)`.\n- Both are **net of commissions**.\n\n---\n\n## API Reference\n\n### `types` module\n\n```rust\nPrice::new(d)        -\u003e Result\u003cPrice, FinError\u003e       // d \u003e 0\nQuantity::new(d)     -\u003e Result\u003cQuantity, FinError\u003e    // d \u003e= 0\nQuantity::zero()     -\u003e Quantity\nSymbol::new(s)       -\u003e Result\u003cSymbol, FinError\u003e      // non-empty, no whitespace\nNanoTimestamp::now() -\u003e NanoTimestamp                 // current UTC nanoseconds\n```\n\n### `orderbook` module\n\n```rust\nOrderBook::new(symbol)\n  .apply_delta(delta)          -\u003e Result\u003c(), FinError\u003e\n  .best_bid() / .best_ask()    -\u003e Option\u003cPriceLevel\u003e\n  .spread()                    -\u003e Option\u003cDecimal\u003e       // best_ask - best_bid\n  .mid_price()                 -\u003e Option\u003cDecimal\u003e\n  .vwap_for_qty(side, qty)     -\u003e Result\u003cDecimal, FinError\u003e\n  .top_bids(n) / .top_asks(n)  -\u003e Vec\u003cPriceLevel\u003e\n```\n\n### `ohlcv` module\n\n```rust\nOhlcvAggregator::new(symbol, tf) -\u003e Result\u003cSelf, FinError\u003e\n  .push_tick(\u0026tick)            -\u003e Result\u003cOption\u003cOhlcvBar\u003e, FinError\u003e\n  .flush()                     -\u003e Option\u003cOhlcvBar\u003e\n\nOhlcvSeries::new()\n  .push(bar)                   -\u003e Result\u003c(), FinError\u003e\n  .closes()                    -\u003e Vec\u003cDecimal\u003e\n  .window(n)                   -\u003e \u0026[OhlcvBar]\n  // ...370+ analytics methods\n```\n\n### `signals` module\n\n```rust\n// Signal trait\ntrait Signal {\n    fn name(\u0026self)   -\u003e \u0026str;\n    fn update(\u0026mut self, bar: \u0026BarInput) -\u003e Result\u003cSignalValue, FinError\u003e;\n    fn is_ready(\u0026self) -\u003e bool;\n    fn period(\u0026self) -\u003e usize;\n    fn reset(\u0026mut self);\n}\n\nSignalPipeline::new()\n  .add(signal)           // builder pattern; chainable\n  .update(\u0026bar)          -\u003e Result\u003cSignalMap, FinError\u003e\n\nSignalMap::get(name)     -\u003e Option\u003c\u0026SignalValue\u003e\n// SignalValue: Scalar(Decimal) | Unavailable\n\n// Warmup contracts (signals::warmup)\nWarmupGuard::new(signal)\n  .update_checked(\u0026bar)  -\u003e Result\u003cSignalValue, WarmupError\u003e\n  .is_ready()            -\u003e bool\n  .bars_remaining()      -\u003e usize\n  .bars_seen()           -\u003e usize\n\nWarmupReporter::new(periods, names)\n  .tick() / .tick_n(n)\n  .report(bars_consumed) -\u003e WarmupReport\n\nWarmupReport::all_ready()              -\u003e bool\n  .pipeline_bars_remaining()           -\u003e usize\n  .warming_signals()                   -\u003e impl Iterator\n  .display()                           -\u003e String\n\n// Signal composition (signals::compose)\nSignalBuilder::new(signal)\n  .lag(n)\n  .normalize(NormMethod)\n  .normalize_window(NormMethod, window)\n  .threshold(level, Direction)\n  .scale(factor)\n  .build()               -\u003e ComposedSignal  // implements Signal\n  .build_named(name)     -\u003e ComposedSignal\n\nComposedSignal::new(name, expr, leaves) -\u003e Result\u003cSelf, FinError\u003e\n  // implements Signal: update / is_ready / period / reset\n```\n\n### `position` module\n\n```rust\nPositionLedger::new(initial_cash)\n  .apply_fill(fill)               -\u003e Result\u003c(), FinError\u003e\n  .equity(\u0026prices)                -\u003e Result\u003cDecimal, FinError\u003e\n  .unrealized_pnl_total(\u0026prices)  -\u003e Result\u003cDecimal, FinError\u003e\n  .realized_pnl_total()           -\u003e Decimal\n  // ...145+ portfolio analytics methods\n```\n\n### `risk` module\n\n```rust\nDrawdownTracker::new(initial_equity)\n  .update(equity)\n  .current_drawdown_pct()   -\u003e Decimal\n  .calmar_ratio()           -\u003e Option\u003cDecimal\u003e\n  // ...120+ risk/statistics methods\n\nRiskMonitor::new(initial_equity)\n  .add_rule(rule)           -\u003e Self     // builder pattern\n  .update(equity)           -\u003e Vec\u003cRiskBreach\u003e\n  .attribution_report(\u0026ledger, market_data) -\u003e AttributionReport\n\n// Risk attribution (risk::attribution)\nMarketData::new(market_vol)\n  .with_beta(symbol, beta)\n  .with_sector(symbol, sector)\n  .with_liquidity(symbol, score)\n  .with_idio_vol(symbol, vol)\n\nRiskAttributor::new(\u0026ledger, market_data)\n  .compute()                -\u003e AttributionReport\n  .compute_bhb(\u0026input)      -\u003e BhbAttribution\n\nAttributionReport::get(factor)          -\u003e Option\u003c\u0026RiskAttribution\u003e\n  .dominant_factor()                    -\u003e Option\u003c\u0026RiskAttribution\u003e\n  .factors_above(threshold_pct)         -\u003e Vec\u003c\u0026RiskAttribution\u003e\n  .summary()                            -\u003e String\n  // Fields: attributions, total_risk, portfolio_beta, concentration_hhi, leverage_ratio\n\nBhbAttribution                          // P\u0026L decomposition\n  .total_active_return: f64\n  .total_allocation: f64\n  .total_selection: f64\n  .total_interaction: f64\n  .best_allocation_sector()             -\u003e Option\u003c\u0026SectorEffect\u003e\n```\n\n### `greeks` module\n\n```rust\nBlackScholes::price(\u0026spec)              -\u003e Result\u003cDecimal, FinError\u003e\nBlackScholes::greeks(\u0026spec)             -\u003e Result\u003cOptionGreeks, FinError\u003e\nBlackScholes::implied_vol(price, \u0026spec) -\u003e Result\u003cDecimal, FinError\u003e\n\n// OptionGreeks fields: delta, gamma, theta, vega, rho (all Decimal)\n\nSpreadGreeks::bull_call_spread(spot, low_k, high_k, days, r, vol) -\u003e SpreadGreeks\nSpreadGreeks::bear_put_spread(spot, low_k, high_k, days, r, vol)  -\u003e SpreadGreeks\nSpreadGreeks::straddle(spot, strike, days, r, vol)                  -\u003e SpreadGreeks\nSpreadGreeks::iron_condor(spot, p_lo, p_hi, c_lo, c_hi, days, r, vol) -\u003e SpreadGreeks\nSpreadGreeks::new(legs)                                             -\u003e SpreadGreeks\n  .net_greeks()                         -\u003e Result\u003cOptionGreeks, FinError\u003e\n  .leg_count()                          -\u003e usize\n```\n\n### `backtest` module\n\n```rust\nBacktestConfig::new(initial_capital, commission_rate) -\u003e Result\u003cSelf, FinError\u003e\n\nBacktester::new(config)\n  .run(bars, \u0026mut strategy)             -\u003e Result\u003cBacktestResult, FinError\u003e\n\n// Strategy trait\ntrait Strategy {\n    fn on_bar(\u0026mut self, bar: \u0026OhlcvBar) -\u003e Option\u003cSignal\u003e;\n}\n\nWalkForwardOptimizer::new(train_size, test_size, config) -\u003e Result\u003cSelf, FinError\u003e\n  .run(bars, make_strategy_fn)          -\u003e Result\u003cWalkForwardResult, FinError\u003e\n\n// WalkForwardResult fields: windows, mean_return, mean_sharpe, worst_drawdown\n\n// Grid-search walk-forward (backtest::walk_forward)\nWalkForwardOptimizer::new(config, bt_config)  -\u003e Result\u003cSelf, FinError\u003e\n  .run(bars, make_strategy_fn)                -\u003e Result\u003cWalkForwardResult, FinError\u003e\n\n// WalkForwardResult fields: periods, aggregate_sharpe, stability_score, mean_oos_return, worst_oos_drawdown\nWalkForwardResult::is_robust(min_stability)   -\u003e bool\n  .best_period() / .worst_period()            -\u003e Option\u003c\u0026WfPeriod\u003e\n\n// WfPeriod fields: train_start, train_end, test_start, test_end,\n//                  best_params, in_sample_sharpe, out_of_sample_sharpe, oos_result\n```\n\n### `regime` module\n\n```rust\nRegimeDetector::new(period, config)     -\u003e Result\u003cSelf, FinError\u003e\n  .update(\u0026bar, cross_returns)          -\u003e Result\u003c(MarketRegime, f64), FinError\u003e\n  .current_regime()                     -\u003e MarketRegime\n  .history()                            -\u003e \u0026[RegimeHistory]\n  .is_ready()                           -\u003e bool\n  .garch()                              -\u003e \u0026Garch11\n  .correlation_detector()               -\u003e \u0026CorrelationBreakdownDetector\n  .reset()\n\n// MarketRegime variants\nMarketRegime::Trending | MeanReverting | HighVolatility | LowVolatility | Crisis | Neutral | Unknown\nMarketRegime::is_risk_off()             -\u003e bool   // Crisis | Unknown\nMarketRegime::short_code()              -\u003e \u0026str   // \"TRD\", \"MRV\", \"HVL\", etc.\n\n// GARCH(1,1)\nGarch11::new(alpha, beta, omega)        -\u003e Result\u003cSelf, FinError\u003e\n  .update(log_return)                   -\u003e f64    // returns σₜ\n  .sigma() / .variance()               -\u003e f64\n  .long_run_sigma()                     -\u003e f64\n  .is_vol_elevated(multiplier)          -\u003e bool\n\n// Correlation breakdown\nCorrelationBreakdownDetector::new(window, threshold, crisis_fraction) -\u003e Result\u003cSelf, FinError\u003e\n  .update(asset_idx, log_return)\n  .is_crisis()                          -\u003e bool\n\n// RegimeHistory\nRegimeHistory { regime, started_at_bar, confidence, ended_at_bar }\n  .duration_bars()                      -\u003e Option\u003cusize\u003e\n  .is_active()                          -\u003e bool\n\n// Regime-conditional RSI\nRegimeConditionalSignal::new(trending_period, mean_reverting_period, neutral_period)\n  .update(\u0026bar, regime)                 -\u003e Option\u003cResult\u003cf64, FinError\u003e\u003e\n  .is_ready()                           -\u003e bool\n```\n\n### `async_signals` module\n\n```rust\nStreamingSignalPipeline::new(pipeline)\n  .spawn()                              -\u003e (mpsc::Sender\u003cOhlcvBar\u003e, mpsc::Receiver\u003cSignalUpdate\u003e)\n\nspawn_signal_stream(pipeline, tick_rx) -\u003e mpsc::Receiver\u003cSignalUpdate\u003e\n\n// SignalUpdate fields: signal_name: String, value: SignalValue, timestamp: DateTime\u003cUtc\u003e\n```\n\n---\n\n## Custom Implementations\n\n### Custom `RiskRule`\n\n```rust\nuse fin_primitives::risk::{RiskBreach, RiskRule};\nuse rust_decimal::Decimal;\n\nstruct HaltOnLoss { limit: Decimal }\n\nimpl RiskRule for HaltOnLoss {\n    fn name(\u0026self) -\u003e \u0026str { \"halt_on_loss\" }\n    fn check(\u0026self, equity: Decimal, _dd: Decimal) -\u003e Option\u003cRiskBreach\u003e {\n        if equity \u003c self.limit {\n            Some(RiskBreach {\n                rule: self.name().into(),\n                detail: format!(\"equity {equity} \u003c halt limit {}\", self.limit),\n            })\n        } else {\n            None\n        }\n    }\n}\n```\n\n### Custom `Signal`\n\n```rust\nuse fin_primitives::signals::{Signal, SignalValue};\nuse fin_primitives::ohlcv::OhlcvBar;\nuse fin_primitives::error::FinError;\n\nstruct AlwaysZero { name: String }\n\nimpl Signal for AlwaysZero {\n    fn name(\u0026self) -\u003e \u0026str { \u0026self.name }\n    fn update(\u0026mut self, _bar: \u0026OhlcvBar) -\u003e Result\u003cSignalValue, FinError\u003e {\n        Ok(SignalValue::Scalar(rust_decimal::Decimal::ZERO))\n    }\n    fn is_ready(\u0026self) -\u003e bool { true }\n    fn period(\u0026self) -\u003e usize { 0 }\n}\n```\n\n---\n\n## Architecture Overview\n\n```\n                      Tick stream\n                          |\n                    TickReplayer / TickFilter\n                          |\n              +-----------+-----------+\n              |                       |\n        OhlcvAggregator          OrderBook\n              |                 (apply_delta)\n        OhlcvSeries                   |\n         (370+ analytics)   vwap_for_qty / spread\n              |\n        SignalPipeline ─────── CompositeSignal\n        (725+ indicators)            │\n              │                SignalExpr DSL\n        WarmupGuard / WarmupReporter │\n              │              (compose.rs)\n         SignalMap (90+ methods)\n              |\n     PositionLedger (145+ methods)\n              |          │\n        DrawdownTracker  └──── RiskAttributor ──── BhbAttribution\n        (120+ methods)         (6-factor model)   (BHB P\u0026L split)\n              |\n         RiskMonitor ──── attribution_report()\n              |\n       Vec\u003cRiskBreach\u003e\n```\n\nAll arrows represent pure data flow. No shared mutable state crosses module\nboundaries. Wrap any component in `Arc\u003cMutex\u003c_\u003e\u003e` for multi-threaded use.\n\n---\n\n## Performance Notes\n\n- **O(1) order book mutations**: `apply_delta` performs a single `BTreeMap::insert`\n  or `BTreeMap::remove`. Inverted-spread check reads two keys and does not allocate.\n- **O(1) streaming indicators**: `Ema` and `Rsi` maintain constant-size state\n  regardless of history length. `Sma` uses a `VecDeque` capped at `period` elements.\n- **Zero-copy tick replay**: `TickReplayer` sorts once at construction and returns\n  shared references on each call; no per-tick heap allocation.\n\n---\n\n## Running Tests\n\n```bash\ncargo test\ncargo test --release\ncargo clippy --all-features -- -D warnings\ncargo doc --no-deps --open\n```\n\nThe test suite includes unit tests in every module and property-based tests using `proptest`.\n\n---\n\n## Market Microstructure Anomaly Detection\n\nThe `microstructure` module detects three illegal order-book manipulation\npatterns in real time: spoofing, layering, and quote stuffing.  All detection\nruns locally with no external calls.\n\n```rust\nuse fin_primitives::microstructure::{\n    MicrostructureDetector, DetectorConfig, OrderEvent, OrderAction, AlertKind,\n};\nuse fin_primitives::types::{Price, Quantity, Side};\nuse rust_decimal::Decimal;\n\nlet mut detector = MicrostructureDetector::new(DetectorConfig {\n    spoof_min_quantity: Decimal::from(1_000),   // flag orders \u003e 1000 qty\n    spoof_cancel_window_ns: 500_000_000,        // cancelled within 500 ms\n    layer_min_levels: 3,                         // 3+ distinct price levels\n    layer_window_ns: 200_000_000,               // within 200 ms\n    stuff_rate_threshold: 100,                  // 100+ cancels per second\n    stuff_window_ns: 1_000_000_000,\n});\n\n// Feed events from your exchange adapter.\ndetector.on_event(OrderEvent {\n    order_id: 1,\n    action: OrderAction::Add,\n    price: Price::new(\"100.00\").unwrap(),\n    quantity: Quantity::new(\"5000\").unwrap(),\n    side: Side::Bid,\n    timestamp_ns: 0,\n});\ndetector.on_event(OrderEvent {\n    order_id: 1,\n    action: OrderAction::Cancel,\n    price: Price::new(\"100.00\").unwrap(),\n    quantity: Quantity::new(\"5000\").unwrap(),\n    side: Side::Bid,\n    timestamp_ns: 200_000_000, // 200 ms — within spoof window\n});\n\nfor alert in detector.drain_alerts() {\n    match alert.kind {\n        AlertKind::Spoofing      =\u003e println!(\"SPOOF: {}\", alert.detail),\n        AlertKind::Layering      =\u003e println!(\"LAYER: {}\", alert.detail),\n        AlertKind::QuoteStuffing =\u003e println!(\"STUFF: {}\", alert.detail),\n    }\n}\n\n// Aggregate stats.\nlet s = detector.stats();\nprintln!(\"Events: {}, Cancels: {}, Spoof: {}, Layer: {}, Stuff: {}\",\n    s.events_total, s.cancels_total, s.spoof_alerts, s.layer_alerts, s.stuff_alerts);\n```\n\nDetection heuristics are based on public CFTC/SEC regulatory guidance and\nacademic literature (Comerton-Forde \u0026 Putniņš, 2015).  All thresholds are\nconfigurable via [`DetectorConfig`].\n\n---\n\n## Contributing\n\n1. Fork the repository and create a branch from `main`.\n2. All public items must have `///` doc comments with purpose, arguments, return values, and errors.\n3. All fallible operations must return `Result`; no `unwrap`, `expect`, or `panic!` in non-test code.\n4. Every new behavior must have at least one happy-path test and one edge-case test.\n5. Run `cargo fmt`, `cargo clippy -- -D warnings`, and `cargo test` before opening a PR.\n\n---\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n\n\u003e Also used inside [tokio-prompt-orchestrator](https://github.com/Mattbusel/tokio-prompt-orchestrator),\n\u003e a production Rust orchestration layer for LLM pipelines.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMattbusel%2Ffin-primitives","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMattbusel%2Ffin-primitives","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMattbusel%2Ffin-primitives/lists"}