Library/Market Microstructure/Liquidity and Spreads

D11-F02-A01 / Complete engineering topic

Quoted Spread

A production-minded guide to Quoted Spread.

D11 · MARKET MICROSTRUCTURE
D11-F02-A01Canonical / Tested / Open
D11 / D11-F02

How far apart are the best displayed prices available to an immediate seller and buyer? The answer matters because a visible quote, a signed execution, a later midpoint, and a low-frequency proxy describe different objects. This tutorial gives practitioners a usable measurement and gives builders the data, clock, and validation contract needed to reproduce it.

Start with the decision

The spread is the visible round-trip price gap at the top of book, not the cost of a particular execution. The canonical package selects Observation-level spread from one valid bid and ask; midpoint normalization; no depth weighting or time aggregation. The primary SEC Rule 605 adopting release supports that definition. The numbers below are synthetic and the arithmetic is author-derived.

Choose the right variant

VariantDefinitionBest useMain limitation
Observation-level midpoint-relativeOne synchronized two-sided quoteTop-of-book monitoringNo execution or depth information
Time-weighted quoted spreadWeight valid quote durationsSession summariesRequires condition, halt, and stale-quote policy
Share-weighted regulatory statisticWeight eligible executions by sharesRule-specific reportingNot reproduced by a lone snapshot

Evidence and applicability boundary

This package teaches one observation-level calculation. The 2024 Rule 605 Amendments and the SEC staff FAQ dated April 1, 2026 govern a broader U.S. reporting workflow; the FAQ is effective August 1, 2026 and has no legal force by itself. It identifies realized-spread horizons of 50 milliseconds, 1 second, 15 seconds, 1 minute, and 5 minutes.

Define the measurement

m=(a+b)/2,Sq=ab,Sqrel=Sq/mm=(a+b)/2,\qquad S_q=a-b,\qquad S_q^{rel}=S_q/m

Every price must share one instrument, currency, adjustment basis, and declared quote scope. Intraday measures also need synchronized clocks and correction policy. Daily proxies need one trading calendar and corporate-action basis.

<!-- ENHANCEMENT:F02-TOPIC-SPECIFIC -->

Choose the correct measurement variant

ConventionUse whenWhat changes
Venue-local BBOOne venue is the measurement scopeVenue-specific displayed cost
Consolidated NBBOA consolidated feed and eligibility policy existCross-venue displayed benchmark
Time-weighted spreadA period summary is requiredWeights quote states by duration

Nearest comparison: Effective Spread adds an execution; Queue Imbalance adds displayed size.

Do not substitute: It is not the price a trade actually received and says nothing about hidden depth.

Work the canonical example

Input: {"bid": 100.0, "ask": 100.08}.

  1. Midpoint = (100.00 + 100.08) / 2 = 100.04.
  2. Absolute spread = 100.08 − 100.00 = 0.08.
  3. Relative spread = 0.08 / 100.04 = 0.000799680127949; basis points = 7.99680127949.

See the state before the code

The annotated visual identifies the measurement anchors, active relationship, and failure boundary. Read the labels before comparing the highlighted output.

Quoted Spread annotated measurement

Open the annotated visual at full size · Open the guided playground

Implement the contract

  1. validate bid and ask
  2. midpoint ← (bid + ask) / 2
  3. spread ← ask − bid
  4. return spread, spread / midpoint, state

The Python and TypeScript references share the same synthetic JSON fixtures and error behavior. They keep raw precision until presentation. Production systems should add fixed-point/decimal prices, feed sequence handling, timestamp normalization, corrections, market-status filters, and licensed source lineage.

<!-- ENHANCEMENT:F02-INTERPRETATION -->

Interpret before aggregating

Read the state before ranking or averaging the value:

CheckQuestion
EligibilityAre both sides positive, uncrossed, and from the declared scope?
ClockWhich quote was knowable at the measurement timestamp?
UnitAre dollar, relative, and basis-point results labeled separately?

A useful report names the measurement question, variant, scope, clock, unit, weighting rule, valid observation count, and unsupported states. A single headline value is not enough audit evidence.

Validate what the result means

The checks cover canonical arithmetic, a property, a boundary, an invalid state, and cross-language parity. A crossed quote is rejected; a locked quote is reported as zero with a locked state. A successful calculation does not prove a profitable strategy, fair execution, or compliance.

StateDecision boundaryRequired response
Locked quoteask = bidReturn zero with locked; do not infer abundant liquidity
Crossed quoteask < bidReject; reconstruct or classify the market state upstream
Stale sideone quote is not causally currentUnsupported even when arithmetic is finite

Compare the neighboring methods

Unlike effective spread, quoted spread does not use a trade price or reveal price improvement. Quoted, effective, and realized spreads are direct intraday measurements when synchronized quote/trade data exist. Roll and Corwin–Schultz are low-frequency spread estimators with model assumptions. Amihud is a return-to-volume illiquidity proxy rather than a spread.

<!-- ENHANCEMENT:F02-VISUAL-SEQUENCE -->

Use four additional audit views

Quoted Spread calculation map

The map separates eligible evidence, transformation, and reason-bearing output.

Quoted Spread exact decision boundary

The boundary view shows when the method returns a supported value and when it must return an explicit unsupported or bounded state.

Quoted Spread scenario coverage

The matrix confirms that the playground retains five distinct scenarios and 61 deterministic states per scenario rather than relying on a tiny toy example.

Quoted Spread interpretation boundary

The final view separates what the calculation measures from causal, performance, identity, best-execution, and compliance claims it cannot prove.

<!-- ENHANCEMENT:SEC-605-TRANSITION -->

Rule 605 temporal applicability

The SEC published its replacement Rule 605 staff FAQ on April 1, 2026. It states that the new FAQ becomes effective on August 1, 2026, the compliance date for the 2024 amendments. This package was reviewed on July 29, 2026. Accordingly, the amended definitions are presented as an imminent reporting context, not silently treated as already effective. The educational per-record formula remains separate from a complete regulatory aggregation engine.

Evidence boundary

A historical case is deferred until point-in-time identity, licensed source, conditions/corrections, session policy, corporate-action basis, and redistribution permission are archived. NYSE describes Daily TAQ as containing trades, quotes, NBBO, and administrative messages, but its data remain licensed.

Summary and next topic

You can now calculate absolute, relative, and basis-point quoted spread from one synchronized two-sided quote, audit its assumptions, and distinguish it from nearby liquidity measures. Continue with Effective Spread.

Primary references

Quoted Spread validation flow

Rendering system map…
ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

Source roles are narrow. Primary sources support definitions and data requirements; all numerical fixtures in this package are visibly synthetic and author-derived.

SRC-01 — SEC Rule 605 adopting release

  • Organization or authors: U.S. Securities and Exchange Commission
  • Source type: Official rule or original peer-reviewed paper
  • Publication or effective date: 2000-11-17
  • Version: Source page or paper version at access
  • URL or DOI: SEC Rule 605 adopting release
  • Accessed: 2026-07-29
  • Jurisdiction: U.S. equities / research model
  • Supports: official quoted-spread and midpoint execution-quality convention
  • Limitations: Does not validate the repository's synthetic numbers or make the selected convention universal.

SRC-02 — NYSE Daily TAQ product description

  • Organization or authors: New York Stock Exchange
  • Source type: Official market-data product documentation
  • Publication or effective date: Current product page; accessed 2026-07-29
  • Version: Source page or paper version at access
  • URL or DOI: NYSE Daily TAQ product description
  • Accessed: 2026-07-29
  • Jurisdiction: U.S. equities
  • Supports: availability of consolidated trades, quotes, NBBO, and administrative messages needed for reproducible intraday measurement
  • Limitations: Product access and redistribution are licensed; the page does not supply a free historical fixture.

SRC-03 — 2024 Rule 605 Amendments adopting release

  • Organization or authors: U.S. Securities and Exchange Commission
  • Source type: Official final rule release
  • Publication or effective date: 2024-03-06; Federal Register publication 2024-04-15
  • Version: Source page or paper version at access
  • URL or DOI: 2024 Rule 605 Amendments adopting release
  • Accessed: 2026-07-29
  • Jurisdiction: U.S. NMS securities
  • Supports: amended execution-quality scope, statistics, timing, and realized-spread horizons
  • Limitations: A regulatory reporting framework is broader than this observation-level educational calculator.

SRC-04 — Rule 605 staff frequently asked questions

  • Organization or authors: U.S. Securities and Exchange Commission, Division of Trading and Markets staff
  • Source type: Official staff guidance without legal force or effect
  • Publication or effective date: 2026-04-01; effective 2026-08-01
  • Version: Source page or paper version at access
  • URL or DOI: Rule 605 staff frequently asked questions
  • Accessed: 2026-07-29
  • Jurisdiction: U.S. NMS securities
  • Supports: five realized-spread horizons, share-weighting, quote timing, partial execution, halt, and correction context
  • Limitations: Staff views do not alter applicable law; applicability is date- and rule-specific.
<!-- ENHANCEMENT:SEC-605-TRANSITION -->

SEC-2026 — Rule 605 staff FAQ transition

  • Organization or authors: U.S. Securities and Exchange Commission, Division of Trading and Markets staff
  • Source type: Official staff guidance; not a Commission rule or statement
  • Publication or effective date: Published 2026-04-01; effective 2026-08-01
  • Version: Frequently Asked Questions: Rule 605 of Regulation NMS (April 1, 2026)
  • URL or DOI: https://www.sec.gov/rules-regulations/staff-guidance/trading-markets-frequently-asked-questions/frequently-asked-questions-rule-605-regulation-nms
  • Accessed: 2026-07-29
  • Jurisdiction: U.S. NMS stocks and the Rule 605 scope described by the source
  • Supports: the amended reporting context, share weighting, and five realized-spread horizons
  • Limitations: The FAQ states it has no legal force and becomes effective after this package’s access date. This package is educational, not a Rule 605 reporting engine.
liquidity-spreads.ts
export type TradeSide = "buy" | "sell";

function numberValue(name: string, value: unknown, positive = false): number {
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new Error(`${name} must be a finite number`);
  }
  if (positive && value <= 0) throw new Error(`${name} must be greater than zero`);
  return value;
}

function sideDirection(side: unknown): number {
  if (side === "buy") return 1;
  if (side === "sell") return -1;
  throw new Error("side must be 'buy' or 'sell'");
}

function quoteValues(bid: unknown, ask: unknown): [number, number, number] {
  const bidValue = numberValue("bid", bid, true);
  const askValue = numberValue("ask", ask, true);
  if (askValue < bidValue) throw new Error("ask must be greater than or equal to bid");
  return [bidValue, askValue, (bidValue + askValue) / 2];
}

export function quotedSpread(bid: unknown, ask: unknown) {
  const [bidValue, askValue, midpoint] = quoteValues(bid, ask);
  const spread = askValue - bidValue;
  const relative = spread / midpoint;
  return {
    model: "quoted-spread",
    bid: bidValue,
    ask: askValue,
    midpoint,
    quoted_spread: spread,
    quoted_spread_relative: relative,
    quoted_spread_bps: relative * 10_000,
    state: spread === 0 ? "locked" : "two-sided",
  };
}

export function effectiveSpread(
  bid: unknown,
  ask: unknown,
  tradePrice: unknown,
  side: TradeSide,
) {
  const quote = quotedSpread(bid, ask);
  const trade = numberValue("trade_price", tradePrice, true);
  const direction = sideDirection(side);
  const effective = 2 * direction * (trade - quote.midpoint);
  const relative = effective / quote.midpoint;
  const quoteSidePrice = direction === 1 ? quote.ask : quote.bid;
  const priceImprovementPerShare = direction * (quoteSidePrice - trade);
  const roundTripSpreadReduction = quote.quoted_spread - effective;
  const tolerance = 1e-12;
  let state = "outside-quote";
  if (effective < -tolerance) state = "through-midpoint-improvement";
  else if (roundTripSpreadReduction > tolerance) state = "inside-quote";
  else if (Math.abs(roundTripSpreadReduction) <= tolerance) state = "at-quote";
  return {
    ...quote,
    model: "effective-spread",
    trade_price: trade,
    side,
    direction,
    effective_spread: effective,
    effective_spread_relative: relative,
    effective_spread_bps: relative * 10_000,
    quote_side_price: quoteSidePrice,
    price_improvement_per_share: priceImprovementPerShare,
    round_trip_spread_reduction: roundTripSpreadReduction,
    state,
  };
}

export function realizedSpread(
  bidAtTrade: unknown,
  askAtTrade: unknown,
  tradePrice: unknown,
  side: TradeSide,
  bidAfter: unknown,
  askAfter: unknown,
  horizonSeconds: unknown = 300,
) {
  const initial = effectiveSpread(bidAtTrade, askAtTrade, tradePrice, side);
  const [futureBid, futureAsk, futureMidpoint] = quoteValues(bidAfter, askAfter);
  const horizon = numberValue("horizon_seconds", horizonSeconds, true);
  const realized = 2 * initial.direction * (initial.trade_price - futureMidpoint);
  const priceImpact = 2 * initial.direction * (futureMidpoint - initial.midpoint);
  const relative = realized / initial.midpoint;
  const impactRelative = priceImpact / initial.midpoint;
  return {
    model: "realized-spread",
    side,
    direction: initial.direction,
    trade_price: initial.trade_price,
    midpoint_at_trade: initial.midpoint,
    midpoint_after: futureMidpoint,
    bid_after: futureBid,
    ask_after: futureAsk,
    horizon_seconds: horizon,
    effective_spread: initial.effective_spread,
    realized_spread: realized,
    realized_spread_relative: relative,
    realized_spread_bps: relative * 10_000,
    price_impact: priceImpact,
    price_impact_relative: impactRelative,
    price_impact_bps: impactRelative * 10_000,
    decomposition_error: initial.effective_spread - realized - priceImpact,
    state: realized < 0 ? "negative-realized" : "nonnegative-realized",
  };
}

function finiteSeries(name: string, values: unknown, minimum: number): number[] {
  if (!Array.isArray(values)) throw new Error(`${name} must be an array`);
  const result = values.map((value, index) => numberValue(`${name}[${index}]`, value, true));
  if (result.length < minimum) throw new Error(`${name} must contain at least ${minimum} observations`);
  return result;
}

function mean(values: number[]): number {
  return values.reduce((total, value) => total + value, 0) / values.length;
}

export function rollSpread(prices: unknown) {
  const priceValues = finiteSeries("prices", prices, 4);
  const changes = priceValues.slice(1).map((value, index) => value - priceValues[index]);
  const lagged = changes.slice(0, -1);
  const current = changes.slice(1);
  if (current.length < 2) throw new Error("prices must create at least two covariance pairs");
  const laggedMean = mean(lagged);
  const currentMean = mean(current);
  const covariance = lagged.reduce(
    (total, value, index) => total + (value - laggedMean) * (current[index] - currentMean),
    0,
  ) / (current.length - 1);
  const meanPrice = mean(priceValues);
  const covarianceTolerance = 1e-15;
  const spread = covariance < -covarianceTolerance ? 2 * Math.sqrt(-covariance) : null;
  const relative = spread === null ? null : spread / meanPrice;
  return {
    model: "roll-spread",
    observation_count: priceValues.length,
    change_count: changes.length,
    covariance_pair_count: current.length,
    lag1_price_change_covariance: covariance,
    covariance_tolerance: covarianceTolerance,
    mean_price: meanPrice,
    spread_estimate: spread,
    spread_estimate_relative: relative,
    spread_estimate_bps: relative === null ? null : relative * 10_000,
    state: spread === null ? "not-identified" : "estimated",
  };
}

export function amihudIlliquidity(
  closes: unknown,
  dollarVolumes: unknown,
  scale: unknown = 1_000_000,
) {
  const closeValues = finiteSeries("closes", closes, 2);
  const volumeValues = finiteSeries("dollar_volumes", dollarVolumes, 2);
  if (closeValues.length !== volumeValues.length) {
    throw new Error("closes and dollar_volumes must have equal length");
  }
  const scaleValue = numberValue("scale", scale, true);
  const returns = closeValues.slice(1).map((value, index) => value / closeValues[index] - 1);
  const ratios = returns.map((value, index) => Math.abs(value) / volumeValues[index + 1]);
  const raw = mean(ratios);
  return {
    model: "amihud-illiquidity",
    observation_count: closeValues.length,
    return_count: returns.length,
    returns,
    daily_illiquidity: ratios,
    illiquidity_per_currency_unit: raw,
    scale: scaleValue,
    illiquidity_per_scaled_volume: raw * scaleValue,
    state: "estimated",
  };
}

export function corwinSchultzSpread(
  highDay1: unknown,
  lowDay1: unknown,
  highDay2: unknown,
  lowDay2: unknown,
  clipNegative = true,
) {
  const h1 = numberValue("high_day_1", highDay1, true);
  const l1 = numberValue("low_day_1", lowDay1, true);
  const h2 = numberValue("high_day_2", highDay2, true);
  const l2 = numberValue("low_day_2", lowDay2, true);
  if (h1 < l1 || h2 < l2) throw new Error("each daily high must be greater than or equal to its low");
  const beta = Math.log(h1 / l1) ** 2 + Math.log(h2 / l2) ** 2;
  const gamma = Math.log(Math.max(h1, h2) / Math.min(l1, l2)) ** 2;
  const denominator = 3 - 2 * Math.sqrt(2);
  const alphaRaw = (
    (Math.sqrt(2 * beta) - Math.sqrt(beta)) / denominator
    - Math.sqrt(gamma / denominator)
  );
  const alpha = clipNegative ? Math.max(0, alphaRaw) : alphaRaw;
  const expAlpha = Math.exp(alpha);
  const spread = 2 * (expAlpha - 1) / (1 + expAlpha);
  return {
    model: "corwin-schultz-spread",
    beta,
    gamma,
    alpha_raw: alphaRaw,
    alpha_used: alpha,
    spread_estimate_relative: spread,
    spread_estimate_bps: spread * 10_000,
    clip_negative: clipNegative,
    state: clipNegative && alphaRaw < 0 ? "clipped-to-zero" : "estimated",
  };
}

export function calculate(topicId: string, inputs: Record<string, any>) {
  if (topicId === "D11-F02-A01") return quotedSpread(inputs.bid, inputs.ask);
  if (topicId === "D11-F02-A02") return effectiveSpread(inputs.bid, inputs.ask, inputs.trade_price, inputs.side);
  if (topicId === "D11-F02-A03") return realizedSpread(
    inputs.bid_at_trade,
    inputs.ask_at_trade,
    inputs.trade_price,
    inputs.side,
    inputs.bid_after,
    inputs.ask_after,
    inputs.horizon_seconds ?? 300,
  );
  if (topicId === "D11-F02-A04") return rollSpread(inputs.prices);
  if (topicId === "D11-F02-A05") return amihudIlliquidity(
    inputs.closes,
    inputs.dollar_volumes,
    inputs.scale ?? 1_000_000,
  );
  if (topicId === "D11-F02-A06") return corwinSchultzSpread(
    inputs.high_day_1,
    inputs.low_day_1,
    inputs.high_day_2,
    inputs.low_day_2,
    inputs.clip_negative ?? true,
  );
  throw new Error(`unsupported topic_id: ${topicId}`);
}
Full-height labplaygroundOpen full screen