ADR-0014: US equities¶
Status: accepted (2026-09); not implemented (the symbols and files below are the plan, not the code); market data amended by ADR-0015
FastMM trades US-listed stocks on IEX data: first in backtests from IEX HIST, then in paper trading through Alpaca to validate the connector, then two-sided market making through Interactive Brokers with directed routing.
Context¶
- FastMM has no equity venue. ITCH 5.0, MoldUDP64, SoupBinTCP, OUCH and FIX codecs (
include/fastmm/codecs/) are used only by tests and benchmarks. The engine drops L3 events (engine.hpp);L3Bookis fed only in tests andbench/bench_book.cpp, rejects off-tick prices, drops orders outside its 65 536-tick window and takes about 75 MB at its default capacity. - An instrument has one tick.
Instrument::valid_priceandround_priceare called by risk, the built-in strategies, the hot adapter, the Python binding and the Deribit encoder;uncross,keep_passiveand the QuoteManager's hysteresis takeinst.tick. Deribit'sTickSchedule(withTickStepandkMaxTickSteps = 4) lives in the venue. - There are no sessions, halts, short-sale controls or buying power.
FeeModelcharges centi-bps on notional. The risk token bucket counts new orders per second only. - Data:
- Nasdaq TotalView-ITCH on Databento needs a business entity and a Nasdaq licence. Individuals can buy only other datasets, such as EQUS.MINI and IEX top of book (Databento).
- Nasdaq's ITCH sample files (5–18 GB per day) state no terms.
- IEX HIST DEEP (price-level book, trading status, operational halts, short-sale price test) is free and may be redistributed with attribution (IEX). IEX is the venue of Alpaca's free feed.
- Alpaca:
- REST order entry at 200 requests per minute per account. A paper account needs only an email address (paper).
- Paper orders fill against the NBBO once marketable, with no queue position, random partial fills 10% of the time and no regulatory fees.
- Alpaca rejects a buy limit priced at or above the account's open sell limit in the same symbol, in paper too (protection).
- Extended-hours orders are Day or GTC limits with
extended_hours=true. Overnight trading (20:00–04:00 ET) runs on Blue Ocean ATS. - The SIP stream ($99/month) has trading status and LULD messages; the free IEX feed is not documented to carry them.
- Regulatory fees are summed per account per day, rounded up to the cent and posted at the end of the day (fees).
- IBKR: the TWS API goes through TWS or IB Gateway, 50 messages/s, at most 20 active orders per contract per side (IBKR).
- Rules:
- Section 31: $20.60 per $1M of sales from 2026-04-04, until 60 days after the FY2027 appropriation (SEC).
- FINRA TAF: $0.000195 per share sold, capped at $9.79, in 2026; $0.000232, capped at $11.61, from 2027-01-01. FINRA's rounding does not apply (TAF).
- CAT fee: $0.000003 per share, buys and sells (Alpaca schedule).
- PDT rule replaced by intraday margin from 2026-06-04 (FINRA RN 26-10).
- Rule 612 ticks: $0.01 at or above $1.00, $0.0001 below. The half-penny tick is deferred to November 2027 (SEC).
- Round lots (100, 40, 10 or 1 share) are set twice a year: the March average sets May's, the September average sets November's.
- Rule 201: when triggered, a short sale must be priced above the national best bid.
- LULD bands and pauses apply 09:30–16:00 ET.
- The SEC approved Nasdaq 23/5 on 2026-04-10; the night session awaits a further filing and SIP readiness.
Decision¶
1. Scope and stages¶
IexDeepSourcereplays IEX HIST DEEP pcap files into backtests (pcap reader, IEX-TP and DEEP decoder). A trimmed real day is a test fixture with the IEX attribution.- Alpaca connector on Alpaca's free IEX feed. Alpaca paper validates the connector, sessions, halts, reconciliation and fees; it is not where market making is judged. Credentials come only from
ALPACA_PAPER_API_KEYandALPACA_PAPER_API_SECRET, referenced as${ALPACA_PAPER_API_KEY}and${ALPACA_PAPER_API_SECRET}inconfigs/alpaca-paper.tomlas the Binance configs do, never inline. Until the account exists, the connector is developed and tested against recorded fixtures and the in-process fake server (tests/venues/fake_venue_util.hpp). - IBKR connector, for two-sided market making with routing to a named exchange and per-share tiered pricing.
- Optional, later:
ItchSourcereplays ITCH 5.0 for users with licensed data. Nasdaq sample files are for manual runs only, never CI.
Out of scope: Nasdaq data licensing, a SIP subscription, registered market making, direct exchange access (live ITCH, OUCH, sponsored access), options on equities, fractional shares, hard-to-borrow locates, auction and ISO orders, overnight positions, corporate-action adjustment, overnight quoting on Alpaca, universes over kMaxInstruments (256), other brokers.
2. Tick schedule and lots¶
TickScheduleandTickStepmove toinclude/fastmm/core/tick_schedule.hpp; a step's tick applies at prices >= itsabove.InstrumentTableholds up to 16 schedules. Instrument flagkBandedTick(bit 3) and cold-line bytetick_scheduleselect one.InstrumentTable::valid_price(inst, px),round_price(inst, px, side)andtick_at(inst, px)use the schedule whenkBandedTickis set andinst.tickotherwise. Callers:- risk uses them, and the QuoteManager's hysteresis uses
tick_atof the resting price; - the built-in strategies, the hot adapter and the Python binding round with them and pass
tick_at(mid)touncross,keep_passiveand tick thresholds; ctx.tick_atandctx.round_priceforward to the table.- For a banded instrument
Instrument::tickis a tick valid at every price of the schedule: $0.01 for Rule 612. User strategies that useinst.tickstay valid, on a coarser grid below $1.00. - Deribit keeps its schedules in the venue and does not set
kBandedTick, so crypto journals do not change. The 2027 half-penny tick is a new schedule in reference data. - Equities have
lot = 1. Cold-linestd::uint16_t round_lotcomes from DEEP's security directory or ITCH R in backtests and from configuration live, because Alpaca's assets carry no round lot. Withquote_round_lots = true(default for equities) the QuoteManager rounds quote quantities down to whole round lots; a quote below one round lot is not sent and is counted. ItchSourcegives an instrument anL3Bookat $0.0001 when the prior close is below $5.00 and at $0.01 otherwise. Off-tick and out-of-window orders are counted and excluded, and the top 10 levels are exact. Books exist only for the run's instruments, anditch_max_orders(216–220) sets their size. Sources run on the backtest runner thread.
3. Calendar and sessions¶
enum class SessionState : std::uint8_t { Closed, Pre, Regular, Post, Overnight, Open }. An instrument without a calendar isOpen.configs/calendars/us-equities.tomlholds holidays, early closes and session times as UTC nanoseconds.tools/update_calendar.pywrites it from the NYSE and Nasdaq holiday pages (dates) and AlpacaGET /v2/calendar(times); a person reviews the diff.- A scheduled CI job fails when it covers fewer than 90 days ahead.
- The loader builds
SessionSchedule(up to 4096 transitions), stores it in the journal header and supplies ITCH and DEEP midnight in ET. - Each transition, and each
pull_before_close_sandflatten_before_close_spoint, is an engine timer; firing journals aTimerMsgwithengine = 2, so quiet instruments change state on time and replay is exact. [[instruments]] sessions = ["regular"]lists where an instrument quotes; outside them the QuoteManager cancels its quotes.on_session(ctx, inst, state)joinshooks.hppwith near misseson_sessionsandon_session_state.- A live session compares the host clock with Alpaca
GET /v2/clockat start and refuses to start beyondmax_clock_skew_ms(default 500) with exit code 3. The engine clock followsCLOCK_REALTIME, which steps on WSL2. OutNewOrderMsggains asessionbyte from its padding. Alpaca sendsextended_hours=truefor every order of an instrument whose sessions include an extended session, so an order sent near 16:00 is not rejected. IBKR setsoutsideRth. Equities default to Day TIF; the simulated venue expires Day orders at the session end.
4. Trading status¶
EventType::TradingStatus = 27,TradingStatusMsg(128 bytes): bitskHalted,kLuldPause,kSsrActive,kShortable,kEasyToBorrow,kStatusUnknown;luld_lo,luld_hi;reason. The engine keeps one status per instrument and journals each message.- Mapping:
- ITCH H: H to
kHalted, P tokLuldPause(Nasdaq-listed only; others arrive as H), Q tokHalted, T clears. - ITCH Y: 1 or 2 sets
kSsrActive. ITCH h setskHaltedfor Nasdaq. - ITCH W: level 3 halts every equity for the rest of the day; levels 1–2 halt for 15 minutes when before 15:25.
- DEEP trading status, operational halt and short-sale price test map the same way.
- After a halt, quoting stays off until the first T or, at the open, the first trade after 09:30.
- Live on the IEX feed, LULD bands and trading-status messages may be unavailable. The connector runs
LuldBandCalculatoron IEX trades and maps Alpaca's status and LULD messages when they arrive, the asset endpoint'stradableflag at start and on each account poll, andGET /v2/clock. An equity starts withkStatusUnknown, which pulls quotes likekHalted. It clears when a status message arrives, or when the clock says open and an IEX trade or quote for the symbol arrives. It is set again afterstatus_stale_s(default 30) without an update in the regular session. LuldBandCalculatorruns inside the source or connector:- Tier comes from ITCH R or DEEP's directory. The first reference price is the listing exchange's opening price, and bands apply 09:30–16:00.
- Every 30 s it takes the mean of the source venue's eligible trades over the last 5 minutes and republishes the reference price only on a move of 1% or more.
- Percentages: 5% (Tier 1) or 10% (Tier 2) above $3.00; 20% from $0.75 to $3.00; below $0.75, the lesser of $0.15 or 75%. Doubled 15:35–16:00 for Tier 1 and for Tier 2 at or below $3.00.
- Bands are rounded to the penny.
- Halted, paused or unknown: quotes are cancelled, orders rejected with
RejectReason::Halted = 17,on_status(ctx, inst)fires (near misseson_trading_status,on_halt). LULD: bids aboveluld_hiand asks belowluld_loare clamped before risk. - A sell is short when it would take position plus open sells below zero; a sell never takes a long position below zero in one order. Short sales are allowed by default and rejected with
NotShortable = 18whenkShortableis clear or[risk] long_only = true. Live,kShortableis clear until Alpaca's asset endpoint says shortable; backtests set it fromassume_shortable. - With
kSsrActive, a short sale priced at or below the best bid is rejected withShortSaleRestricted = 19. The engine holds a venue bid (IEX, Nasdaq or the feed), not always the national best bid; the broker's check is final.
5. Account and request budget¶
EventType::AccountUpdate = 28,AccountUpdateMsg: venue,cash,buying_power,equity, andnets_open_orders(whether the venue's figure already subtracts open orders). Connectors send it after reconciliation, after fills and from a poll; it is journaled.- For a venue with equity instruments, orders are rejected with
InsufficientBuyingPower = 20before its firstAccountUpdate, while the last is older thanmax_account_age_s, and when order notional (plus open buy notional unless netted) exceedsbuying_power - buying_power_reserve. [venues.<name>] requests_per_minuteandrequest_burstform a budget thatrest_channelcharges for every REST call. 20% is reserved for cancels, the account poll backs off when the budget is low, and refused orders getVenueRateLimit.
6. Fees¶
FeeModelbecomesFeeScheduleininclude/fastmm/core/fee_schedule.hpp, with every new field zero by default:maker_cbps,taker_cbps,maker_per_share,taker_per_share;commission_per_share,commission_min,commission_max_cbps;sec_fee_per_million,taf_per_share,taf_max,cat_per_share;fee_rounding = none | execution | daily.[[venues.<name>.fees.periods]]carry afromdate; the engine switches rates at the first session timer on or after it. The config comments note the Section 31 expiry rule.- Alpaca uses
daily: fills carry unrounded estimates. The connector compares the day's estimate with Alpaca's fee activities the next session and reports the difference in the log and status file; PnL keeps the estimates. PositionTrackeris unchanged; crypto fees and hashes do not change.
7. Flat by the close¶
- From
flatten_before_close_s(default 60) before the end of the last allowed session, the engine sends flattening orders every second until flat. They are IOC in the regular session and Day limits in extended sessions, pricedflatten_slippage_bpsthrough the best price and never beyond the LULD band. - A position left at the end raises an alert, sets a status-file flag and blocks the next start unless
allow_start_with_position = true. There is no split or dividend adjustment.
8. Alpaca connector¶
- Base URLs
paper-api.alpaca.marketsandapi.alpaca.markets, order updates fromtrade_updates, market data fromv2/iex. VenueCaps::rejects_self_cross:stpis forced on and the OMS also counts the pending price of a replace in flight, so no buy is sent at or above an open sell of the symbol or the reverse; the QuoteManager cancels first.- A paper test records whether Alpaca accepts a short sale while a buy is open. If not, quoting from flat is one-sided on Alpaca, and the venue reference says so.
- Replace is
PATCH, which creates a new order; the connector followsreplaced_byand keeps client order ids within Alpaca's length limit. Symbols use the dotted form (BRK.B); sources and connectors map theirs throughvenues/symbology.hpp(IBKRBRK B).
9. Python and invariants¶
FASTMM_HOT_ABI_VERSION = 2appends afterask_is_raw:session(int32);halted,luld_paused,ssr,shortable,status_unknown,luld_clamped(uint8);luld_lo,luld_hi,buying_power,round_lot(double and_raw).ctx.tickistick_at(mid).on_sessionandon_statusjoin the hot hooks;fastmm.SESSION_*constants work in hot code. Hooks compile at import, so strategies recompile.- No allocation or virtual call on the hot path: schedules, statuses and account state are fixed arrays;
tests/hotpath/noalloc_test.cppcovers each new path. kJournalVersion = 4adds a CRC-checked reference section (tick schedules, session schedule). Readers accept 1–4; existing golden journals andsample_1000.sha256replay unchanged.- Strategies that do not opt in compile and behave as before; crypto instruments set none of the new flags.
Consequences¶
- Equities run in backtests, replay and paper trading with the same strategy classes.
- IEX's book is thin, and LULD bands computed from IEX trades differ from the published ones. Live without SIP, a halt may be seen only as missing updates, so quotes are pulled late or conservatively while status is unknown.
- Alpaca paper tests the connector, sessions, halts, reconciliation and fees. Its PnL is no evidence of spread capture: fills ignore queue position.
- At 200 requests per minute for every REST call, requotes are measured in seconds, and engine latency makes no difference on Alpaca.
min_requote_interval_msandmin_requote_ticksneed values to match. - Retail commissions and regulatory fees exceed exchange rebates. IBKR's tiered pricing and directed routing narrow the gap without closing it.
- Alpaca's self-cross rule may force one-sided quoting from flat there; two-sided market making is judged on IBKR.
- The calendar and fee periods are maintained by hand.
Implementation order¶
Steps 1–3 can start in parallel; step 4 needs 1–3; steps 5–7 follow in order; step 8 needs step 4. Each step ships its docs.
- Tick schedule: core types,
InstrumentTablefunctions, callers,[tick_schedules]. Tests: both sides at $0.9999, $1.00, $1.0001; Deribit tests unchanged; golden hashes unchanged; BasicMM tick-to-order p50 within 2% of main. - Engine inputs: session timers,
TradingStatus,AccountUpdate, journal v4, rejects 17–20, clamps, short-sale checks, flattening, hooks, calendar file, tool and scheduled job. Tests: a synthetic journal crossing Pre, Regular and Post with a halt, SSR, band move and account update replays to the same hash on a quiet instrument; v1–3 journals replay; orders fail closed without an account update. - Fees:
FeeSchedule, periods, rounding modes. Tests: hand-computed fees for a 100-share sell at $50.00, a TAF-capped sell, a 2026–2027 period switch and a daily rounding total; crypto results identical. IexDeepSourcewithLuldBandCalculatorand statuses. Tests: the IEX fixture gives the expected book and statuses; LULD against published examples; two runs give the same hash.- Alpaca: request budget, self-cross handling, replace chains, symbology, clock and calendar checks, fee reconciliation,
configs/alpaca-paper.toml. Tests against the fake server: lifecycle, replace, rejects, reconnection, budget refusal, extended-hours flag, halt pull, replay hash. Tests for unknown status: no status messages and no updates pull quotes. Once the owner's paper account exists: the paper test of short with an open buy, and a paper session. - Python ABI 2. Tests: a hot BasicMM port matches C++ on the IEX fixture; ADR-0013 tests pass.
- IBKR over IB Gateway: routing,
outsideRth, halt and shortable ticks, 50 messages/s, 20 orders per side. Tests against a fake gateway from recorded fixtures; then a two-sided quoting paper session. - Optional:
ItchSourcewith theL3Bookrules. Tests: an ITCH fixture fromgenerate_fixtures.pywith H, Y, W and prices across $1.00 gives the expected book and statuses; two runs give the same hash.
Open questions¶
- IBKR account, commission plan (fixed or tiered) and market data subscriptions, before step 7.