Best execution

SEC Rule 605/606 substrate: per-fill price improvement, effective spread, latency, quarterly routing. Built from the audit log.

Regulated desks answer two questions: did clients get good prices, and where were their orders routed. SEC Rule 605 covers the first (market-center execution-quality disclosure). Rule 606 covers the second (quarterly routing summary). Both start from one ingredient: order and fill events with NBBO snapshots.

The professional tier captures those events in the audit log automatically, so the report is a read layer. No hot-path code is touched.

python
from horizon.reporting import BestExReportBuilder

builder = BestExReportBuilder(audit_log._sink)

report = builder.build(
    start=datetime(2026, 1, 1, tzinfo=UTC),
    end=datetime(2026, 4, 1, tzinfo=UTC),
)

print(report.overall.avg_price_improvement_bps)
print(report.by_venue["XNAS"].avg_effective_spread_bps)
for v in builder.build_rule606().by_venue:
    print(v.venue, v.percent_of_total_shares)

What’s captured

Every AuditedVenue.submit() records an OrderSubmitted event with the NBBO snapshot at submission (when the venue provides it; Paper does not, real brokers do). Every OrderFilled event carries fill price, quantity, fees, rebates, liquidity flag, exchange of execution, and NBBO at fill. The builder walks the log, pairs submits with fills by client_order_id, and computes per-fill metrics.

No opt-in. When hz.run(..., audit_log=log) runs, the events are there.

Per-fill metrics

Each fill produces an OrderExecution:

MetricFormula (BUY; mirrored for SELL)Units
price_improvement_bps(mid_at_submit - fill_price) / mid_at_submit * 10000bps
effective_spread_bps2 * abs(fill_price - mid_at_submit) / mid_at_submit * 10000bps
slippage_vs_mid_bps(fill_price - mid_at_submit) / mid_at_submit * 10000bps (signed)
fill_latency_msfill_time - submit_timems
liquiditymaker / taker / unknownenum
exchange_of_executionMIC codestring
  • Positive price improvement: bought below mid or sold above. Good.
  • Negative: crossed the spread. Expected on market orders. Investigate on limits meant to earn passive fills.
  • Effective spread is the Rule 605 standard metric; lower is better.
  • Latency is submit-to-fill wall clock.

When an order has no NBBO snapshot (Paper without bid/ask), the metric is None and aggregates skip it instead of assuming zero.

Aggregates

BestExReport provides four views:

python
report.overall
report.by_market["AAPL"]
report.by_venue["XNAS"]
report.by_account["acc_jane"]

Each view is a BestExMetrics:

  • n_fills, total_shares, total_notional, total_fees, total_rebates.
  • avg_price_improvement_bps, median_price_improvement_bps, pct_orders_with_price_improvement.
  • avg_effective_spread_bps, median_effective_spread_bps.
  • avg_slippage_vs_mid_bps.
  • avg_fill_latency_ms, median_fill_latency_ms, p95_fill_latency_ms.
  • maker_share_pct, taker_share_pct.

Metrics are None when there is no data to compute them.

Rule 606 routing summary

python
rule606 = builder.build_rule606(
    start=datetime(2026, 1, 1, tzinfo=UTC),
    end=datetime(2026, 4, 1, tzinfo=UTC),
)
print(rule606.total_shares, rule606.total_orders)
for v in rule606.by_venue:
    print(f"{v.venue:8s} {v.percent_of_total_shares:6.2f}% "
          f"maker={v.maker_shares:.0f} taker={v.taker_shares:.0f} "
          f"net=${v.net_rebates_usd:.2f}")

Rule 606(a)(1) requires a quarterly schedule breaking down venues by percent of shares routed, liquidity flag, and net rebates/fees. Rule606VenueRow feeds that schedule.

The official form also requires a per-security-class breakdown (S&P 500, other NMS stocks, options). The builder produces the per-venue totals; a small adapter maps market_id to security class downstream.

JSON export

python
import json
with open("best_ex_2026_Q1.json", "w") as f:
    json.dump(report.to_dict(), f, indent=2, default=str)

Filters

python
report = builder.build(
    start=datetime(2026, 3, 1, tzinfo=UTC),
    end=datetime(2026, 4, 1, tzinfo=UTC),
    market_ids=["AAPL", "MSFT"],
    account_ids=["acc_jane", "acc_bob"],
)

Time filtering uses the event timestamp (not the fill timestamp, though they are usually the same modulo broker clock drift).

NBBO capture

The venue records NBBO on VenueOrder.nbbo_at_submit and VenueFill.nbbo_at_fill. AuditedVenue serializes whatever the venue provides into the audit payload. See Venues and Order lifecycle.

VenueNBBO sourceStatus
PaperNoneNot captured
NullLiveFeed + PaperNoneNot captured
LiveFeed with quotes subscriptionLatest quote tick, attached on submitL1 follow-up
Real broker Venue (Alpaca, IBKR)Broker’s fill report includes NBBO at executionPartially shipped (Alpaca)

Without NBBO the report still has latency, fees, rebates, liquidity, and venue breakdown; enough for a substantial Rule 606 report, not a full Rule 605 price-improvement analysis.

PDF rendering

python
from horizon.reporting.pdf import render_best_ex_pdf
render_best_ex_pdf(report, out_path="reports/2026-Q1/best_ex.pdf")

WeasyPrint wrapper with a lazy import; pip install weasyprint to enable.

Read-only boundary

The best-ex layer never touches orders, fills, positions, ledgers, or lot books. It reads the audit sink.

  • Run mid-session for an intraday snapshot.
  • Run against an archived audit database (SQLiteSink on a read-only file).
  • Delete horizon.reporting and nothing else breaks.
  • Custom reports over other events (RiskDecision, WashSaleDetected, BlockAllocation) follow the same pattern.

The AuditSink Protocol is the API boundary. Any implementation can feed the builder, including a future PostgresSink.