4. Commands, market orders, cancel and modify¶
Date: 2026-10-03
Status¶
Accepted
Context¶
S5 adds market orders, cancel and modify (BUILD_PLAN section 5 and 16.3). It also gives the engine its final input shape, because S7 (pipeline) and S8 (journal) need every input to be one replayable command.
Decisions¶
- One sealed
Commandtype and one entry point,MatchingEngine.apply(Command). The commands areNewOrder,CancelOrder,ModifyOrder,ClockTick,SetSessionStateandSetAccountEnabled. Every state change comes from a command, so the input journal (S8) is simply the list of commands. A sealed type makes the compiler check that everyswitchhandles every command. This is the standard event-sourcing shape. - Time enters only through
ClockTick; it never goes backwards. A tick earlier than the current time is ignored rather than rejected. The clock thread is ours, so a backwards tick is a bug, but the engine must never throw on input. Events carry the time of the last tick. - A market order sweeps the opposite side, and any remainder is cancelled with
NO_LIQUIDITY. It is accepted first (with price 0), then trades, then is cancelled. It never rests. Until S6 adds price bands its limit is unbounded; S6 replaces that with the band edge. - Cancel and modify name the order by ID plus symbol. The symbol picks the book without a global index, as FIX cancel requests do. An unknown symbol gives
UNKNOWN_SYMBOL; an unknown order givesUNKNOWN_ORDER. - Another account's order is reported as
UNKNOWN_ORDER, exactly like a missing one. A different error would let anyone probe which order IDs exist. This is a common security practice: don't reveal what you are not allowed to touch. - Modify carries the new total quantity, including what has filled. This is the FIX convention.
leavesQuantityinOrderModifiedis what remains open. - Priority on modify. Reducing quantity at the same price keeps the order's queue position, since it harms nobody behind it. Increasing quantity or changing price removes the order and re-enters it at the back, and it may trade at once if the new price crosses. In that case
OrderModifiedcomes first, then anyTrades. A modify with unchanged values is accepted and keeps priority. - A modify to the filled amount or below cancels the open part with
MODIFIED_TO_ZERO(BUILD_PLAN 16.3). - A rejected modify leaves the order untouched, and the reject carries the order ID.
EXPIREDis added to the cancel reasons now (schema and enum) for DAY expiry in S6, so the contract changes once.
Testing¶
- Unit tests cover each rule:
MarketOrderTestandCancelModifyTest. - The reference matcher models cancel, modify and market orders independently, using an explicit arrival counter for time priority.
- Property tests run random flows of limits, markets, cancels, modifies, ticks and bad inputs. Conservation per order is checked as
ordered = filled + open + cancelled. - Two planted bugs were caught: losing priority on a reduction, and a silent market remainder. The first one exposed a weak generator: prices spanned 21 ticks, so same-price modifies were rare and the property tests missed the bug. Narrowing prices to 7 ticks fixed that. The lesson: check that random tests actually reach each code path, by planting a bug in it.
Trade-offs¶
- An unbounded market order can sweep a thin book to absurd prices until S6 adds bands.
- Modify by remove-and-re-add allocates a new
RestingOrder. That is acceptable until benchmarks say otherwise.