Use case · Microstructure
Cost a quote and aggregate a spread's round-trip
effective_spread turns a bid/ask into a typed QuoteCostOutcome — Available, Degraded, or Unavailable — and spread_round_trip_cost aggregates a multi-leg spread into one round-trip cost, with locked, crossed, and one-sided markets represented explicitly.
When to use it
- You need per-quote execution cost — quoted spread, half spread, mid, and round-trip cost — from a bid/ask.
- You need one round-trip cost for a multi-leg spread that respects per-leg quantity.
- You want locked, crossed, one-sided, and missing markets handled as typed states, not sentinel numbers.
Example
use ferro_risk::{effective_spread, spread_round_trip_cost, QuoteCostLeg, QuoteCostOutcome};
// Single quote: bid/ask are Option<f64> so a missing side is explicit.
match effective_spread(Some(1.20), Some(1.24))? {
QuoteCostOutcome::Available(cost) => {
println!("mid = {} half spread = {}", cost.mid(), cost.half_spread());
println!("round-trip cost = {}", cost.round_trip_cost());
}
QuoteCostOutcome::Degraded { cost, .. } => { /* locked market: bid == ask */ let _ = cost; }
QuoteCostOutcome::Unavailable { reason } => { /* crossed / one-sided / no quote */ let _ = reason; }
}
// Multi-leg spread: one round-trip cost across the legs, quantity-aware.
let legs = vec![
QuoteCostLeg::new(Some(1.20), Some(1.24), 1.0)?, // long leg
QuoteCostLeg::new(Some(0.80), Some(0.83), 1.0)?, // short leg
];
if let Some(spread) = spread_round_trip_cost(&legs)?.cost() {
println!("spread round-trip = {}", spread.round_trip_cost());
println!("relative round-trip = {}", spread.relative_round_trip_cost());
}
# Ok::<(), ferro_risk::FerroRiskError>(())Per-quote cost
effective_spread returns a QuoteCostOutcome; when it's Available (or Degraded), the inner QuoteCost exposes:
| QuoteCost | Description |
|---|---|
| bid() / ask() | The validated bid and ask. |
| mid() | Mid price, (bid + ask) / 2. |
| quoted_spread() | Ask − bid. |
| half_spread() | Half the quoted spread — the one-way cost to mid. |
| round_trip_cost() | Full round-trip cost of crossing the spread. |
| relative_spread() | Quoted spread relative to mid. |
| QuoteCostOutcome | Description |
|---|---|
| Available(QuoteCost) | A clean two-sided market. |
| Degraded { cost, degradation } | A usable cost with a flag — e.g. LockedMarket (bid == ask). |
| Unavailable { reason } | No usable cost — Crossed, OneSided, or NoQuote. |
Spread aggregation
spread_round_trip_cost(&[QuoteCostLeg]) returns a SpreadQuoteCostOutcome mirroring the per-quote shape; its SpreadQuoteCost exposes:
| SpreadQuoteCost | Description |
|---|---|
| round_trip_cost() | Aggregate round-trip cost across all legs. |
| half_spread_cost() | Aggregate one-way (to-mid) cost. |
| gross_mid_notional() | Gross mid notional of the spread. |
| relative_round_trip_cost() | Round-trip cost relative to gross mid notional, in [0, 2]. |
| leg_count() | Number of legs aggregated. |
Notes
- Bid and ask are
Option<f64>, so a one-sided market isOneSided, not a quote against zero.effective_spread_from_pricestakes aBidAskPriceswhen you already have that type. - A crossed market (bid > ask) is
Unavailable— the crate refuses to return a negative round-trip cost. - Each
QuoteCostLegcarries a quantity, so the aggregate respects leg sizing rather than summing unit legs. - Aggregate overflow / underflow fails loud with a typed
FerroRiskErrorin thequote_cost.*namespace.
Module
The microstructure primitives live in ferro_risk::microstructure and are re-exported at the crate root, so use ferro_risk::{effective_spread, spread_round_trip_cost} works directly.