T+1 settlement tracking
Settled vs. traded quantity per asset class. Business-day math. Feeds cash-account buying-power and 1099-B reporting.
US equities and options settle T+1 (changed from T+2 in May 2024). Forex settles T+2. Crypto, perps, and prediction markets are effectively T+0. horizon.state.settlement tracks which trades have actually settled at any point in time.
This matters because:
- Cash accounts. FINRA’s free-rider rule forbids using proceeds of an unsettled sale to buy another marginable security. The settled balance is what counts.
- Reg T margin. Day-trading buying power calculations key off settled cash.
- 1099-B. Trade date drives reporting date and holding-period; settlement date drives when cash is available.
- End-of-year reconciliation. A trade on 2026-12-31 (Thursday) settles 2027-01-02. The wire-side cash balance differs from the trade-date balance across year-end.
The module is additive. It does not change PositionLedger or anything in the hot path.
Import
from horizon.state import (
SettlementEngine,
SettlementRule,
SettlementState,
SettlementStatus,
PendingSettlement,
DEFAULT_SETTLEMENT_RULES,
)
from horizon.asset_classes import AssetClass
Quickstart
from datetime import datetime, timezone
from horizon.calendars import NYSECalendar
from horizon.audit import AuditLog, SQLiteSink
engine = SettlementEngine(
calendar=NYSECalendar(),
audit_log=AuditLog(sink=SQLiteSink("audit.db")),
)
# Feed every fill into the engine. Typical wiring in the run loop:
for fill in venue.drain_fills():
asset_class = markets_by_id[fill.market_id].asset_class
engine.on_fill(fill, asset_class=asset_class, account_id=fill.account_id)
# Once a session (end of day is fine):
engine.advance_to(datetime.now(timezone.utc))
# Queries:
engine.settled_quantity("AAPL", account_id="acc_jane") # signed
engine.unsettled_cash(account_id="acc_jane") # USD
engine.pending(account_id="acc_jane") # list[PendingSettlement]
engine.state() # full snapshot
Default rules
| Asset class | Days | Business days only |
|---|---|---|
| Equity | 1 | yes |
| Option | 1 | yes |
| Future | 0 | yes (daily MTM) |
| Forex | 2 | yes |
| Crypto | 0 | no (24/7) |
| Perp | 0 | no |
| Prediction | 0 | no |
Override per asset class with a custom rule set:
engine = SettlementEngine(rules=(
SettlementRule(AssetClass.Equity, 2), # legacy T+2
# ...
))
Business-day math
With calendar=NYSECalendar() attached, settlement skips NYSE holidays. Without a calendar (the default), only weekends are skipped.
Install the optional pandas_market_calendars (pip install horizon[equity]) for accurate US holiday detection. Without it, NYSECalendar falls back to a Mon-Fri rule and may miss holidays like MLK Day or Good Friday.
Pending vs. settled
p = engine.on_fill(fill, asset_class=AssetClass.Equity)
p.settle_id # internal id
p.trade_date # when the fill occurred
p.settle_date # start of the settle day (00:00 UTC)
p.signed_quantity # +qty for buys, -qty for sells
p.notional_usd # quantity * price
p.net_cashflow_usd # -notional-fee on buys, +notional-fee on sells
advance_to(ts) promotes every pending entry with settle_date <= ts to Settled. It is idempotent and a no-op when going backwards.
Cash-account integration
The module answers the “can I use these proceeds?” question:
# A cash account holder sold AAPL today. The sale won't settle until
# tomorrow. If they try to buy MSFT using those proceeds today, that's
# free-riding.
unsettled = engine.unsettled_cash(account_id="acc_jane_cash") # +$5,000
settled = account.cash_balance # $10,000
# Available for new purchases in a cash account:
buyable = settled
Wire this into a Decision.reject("free-rider", layer="settlement") gate when action.account_id maps to a cash-only account and settled_cash < projected_notional.
The pre-built risk engine does not currently enforce this rule. Add it as a custom gate when you run cash accounts.
Failed settlement (FTD)
engine.mark_failed(pending.settle_id, reason="DTC fail-to-deliver")
Promotes the entry to SettlementStatus.Failed, removes from pending(), and records an AuditCategory.SettlementFailed event. The entry does not contribute to settled_quantity or settled_cash.
Reg SHO close-out obligations and FTD tracking are out of scope here; this module just lets compliance flag a specific settlement as failed and surface the audit trail.
Audit events
| Category | When |
|---|---|
TradeSettled | A pending entry promotes to Settled. |
SettlementAdvanced | advance_to() processed pending entries. |
SettlementFailed | mark_failed() marked an entry as failed. |
Every event carries the settle_id, execution id, account id, market id, quantity, and dates. Examiners can reproduce the settled-vs-traded state at any prior moment by replaying the log.
Snapshot
state = engine.state(at=datetime(2026, 12, 31, tzinfo=timezone.utc))
state.settled_quantity("AAPL", "acc_jane")
state.traded_quantity("AAPL", "acc_jane")
state.settled_cash_delta_by_account["acc_jane"]
state.unsettled_cash_delta_by_account["acc_jane"]
state.pending # tuple[PendingSettlement, ...]
state.to_dict() # JSON-safe
Use state snapshots for point-in-time reconciliation. The 2026-12-31 snapshot above answers “what was the as-of-year-end settled balance?” for tax-packet generation.
1099-B alignment
Settlement date does not change holding-period (trade date does), but it matters for:
- Wire-side cash balance shown on the year-end statement.
- Accrued but unsettled proceeds booked on the 1099-B.
The tax packet builder is not yet wired to the settlement engine; pass the state snapshot through to override wire-side cash totals when you need settlement-date alignment.
Reconstruction from the audit log
Re-feeding fills from the audit log on process restart rebuilds the engine state:
from horizon.audit import AuditCategory
engine = SettlementEngine(calendar=NYSECalendar())
for event in audit_log._sink.read_range():
if event.category == AuditCategory.OrderFilled:
# reconstruct a VenueFill from the event payload
...
engine.on_fill(fill, asset_class=asset_class, account_id=fill.account_id)
engine.advance_to(datetime.now(timezone.utc))
The Recovery module will wire this up automatically in a follow-up; for now feed it yourself.
Out of scope
- Per-security settle rules. All trades in an asset class use the same
SettlementRule. Treasuries (T+1) and mutual funds (T+1 or T+2 depending) often share the equity rule in US markets; add a custom rule when you need finer control. - Fails-to-deliver obligation tracking (Reg SHO close-out).
mark_failedflags; enforcement of close-out timers is a firm-policy concern. - International settlement cycles. Defaults are US-centric. Override rules per asset class.
- Intra-day settlement dynamics. The engine advances at day granularity; a trade at 23:50 UTC on trade-date-1 and a trade at 00:10 UTC on trade-date share the same trade date but may differ in broker bookkeeping by microseconds.