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.
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:
| Metric | Formula (BUY; mirrored for SELL) | Units |
|---|---|---|
price_improvement_bps | (mid_at_submit - fill_price) / mid_at_submit * 10000 | bps |
effective_spread_bps | 2 * abs(fill_price - mid_at_submit) / mid_at_submit * 10000 | bps |
slippage_vs_mid_bps | (fill_price - mid_at_submit) / mid_at_submit * 10000 | bps (signed) |
fill_latency_ms | fill_time - submit_time | ms |
liquidity | maker / taker / unknown | enum |
exchange_of_execution | MIC code | string |
- 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:
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
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
import json
with open("best_ex_2026_Q1.json", "w") as f:
json.dump(report.to_dict(), f, indent=2, default=str)
Filters
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.
| Venue | NBBO source | Status |
|---|---|---|
Paper | None | Not captured |
NullLiveFeed + Paper | None | Not captured |
LiveFeed with quotes subscription | Latest quote tick, attached on submit | L1 follow-up |
Real broker Venue (Alpaca, IBKR) | Broker’s fill report includes NBBO at execution | Partially 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
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 (
SQLiteSinkon a read-only file). - Delete
horizon.reportingand 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.