Honor taker TIF/kind in match_order: FOK/IOC/PostOnly/M2L (#65)#98
Conversation
…tOnly, M2L)
match_order now takes taker_tif: TimeInForce and taker_kind: TakerKind and
applies single-level semantics via a mutation-free pre-check:
- FOK: all-or-nothing — killed (zero trades, queue untouched) if depth < incoming.
- PostOnly: rejected if it would cross (matchable depth present).
- IOC / GTC / GTD / Day / MarketToLimit: fill available, report remainder.
New additive MatchOutcome {Filled, PartiallyFilled, NotFilled, Killed, Rejected}
on MatchResult (serde-default) with was_killed()/was_rejected(); existing fields
unchanged.
Also fixes a pre-existing infinite loop: a zero-visible iceberg/reserve with
hidden depth re-queued unchanged (consumed=0) forever, now newly reachable from
the FOK dry-run. Fixed by drawing the full hidden tranche when visible==0, a
shared no-progress set-aside guard (stalled makers reinserted at their original
sequence so makers behind still match and price-time priority holds), and a
unified is_matchable() so PostOnly and FOK agree on depth. Reviewed SOUND by
microstructure-critic.
BREAKING: match_order signature change (pre-0.9). Migration note in lib.rs.
There was a problem hiding this comment.
Pull request overview
This PR updates the single-price-level matching engine to honor taker-side intent (time-in-force and kind) during PriceLevel::match_order, adds an explicit terminal MatchOutcome to disambiguate “no fill” vs “killed/rejected”, and includes a regression fix + tests for a previously possible infinite loop involving zero-visible iceberg/reserve orders.
Changes:
- Breaking API change:
match_ordernow takes(taker_tif: TimeInForce, taker_kind: TakerKind)and enforces FOK/IOC/PostOnly/M2L taker semantics via pre-checks and a dry-run depth computation. - Add
MatchOutcometoMatchResult(serde-defaulted) and addTakerKindtype + exports; update docs/README/examples/benches/tests accordingly. - Fix and regression-test an infinite-loop scenario caused by zero-progress makers (zero-visible iceberg/reserve), including a sweep “set-aside” guard and iceberg refresh handling.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/mod.rs | Updates unit test calls to the new match_order signature. |
| src/price_level/tests/level.rs | Expands/renames tests to cover new taker semantics and loop regressions; updates calls for new signature. |
| src/price_level/level.rs | Implements taker TIF/kind pre-checks, FOK dry-run depth computation, and sweep no-progress guard + docs. |
| src/prelude.rs | Exports MatchOutcome and TakerKind via prelude. |
| src/orders/order_type.rs | Adds OrderType::is_matchable and fixes iceberg refresh behavior for zero-visible tranches. |
| src/lib.rs | Adds migration guide and exports MatchOutcome/TakerKind. |
| src/execution/tests/match_result_trade.rs | Adds tests to keep MatchOutcome in sync and serde-default coverage. |
| src/execution/taker.rs | Introduces TakerKind definition and helpers. |
| src/execution/mod.rs | Wires taker module and re-exports TakerKind + MatchOutcome. |
| src/execution/match_result.rs | Introduces MatchOutcome, threads it through MatchResult lifecycle, and adds killed/rejected helpers. |
| README.md | Mirrors the migration guide and updated semantics documentation. |
| examples/src/bin/simple.rs | Updates example to pass TimeInForce and TakerKind. |
| examples/src/bin/integration_trade_roundtrip.rs | Updates integration example to new match_order signature. |
| examples/src/bin/integration_special_orders.rs | Updates example calls to include taker_tif/taker_kind. |
| examples/src/bin/integration_checked_arithmetic.rs | Updates example calls to include taker_tif/taker_kind. |
| examples/src/bin/integration_basic_lifecycle.rs | Updates example calls to include taker_tif/taker_kind. |
| examples/src/bin/hft_simulation.rs | Updates simulation example calls to include taker_tif/taker_kind. |
| examples/src/bin/contention_test.rs | Updates contention example calls to include taker_tif/taker_kind. |
| benches/price_level/special_orders.rs | Updates benchmarks to new match_order signature. |
| benches/price_level/serialization.rs | Updates benchmarks to new match_order signature. |
| benches/price_level/mixed_operations.rs | Updates benchmarks to new match_order signature. |
| benches/price_level/match_orders.rs | Updates benchmarks to new match_order signature. |
| benches/price_level/lifecycle.rs | Updates benchmarks to new match_order signature. |
| benches/price_level/checked_arithmetic.rs | Updates benchmarks to new match_order signature. |
| benches/concurrent/register.rs | Updates concurrent benchmark calls to new match_order signature. |
| benches/concurrent/contention.rs | Updates concurrent benchmark calls to new match_order signature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
❌ Your project status has failed because the head coverage (73.31%) is below the target coverage (75.00%). You can increase the head coverage or adjust the target coverage.
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
…c comments - MatchResult::new / with_capacity: a zero initial_quantity result is now is_complete=true / outcome=Filled at construction (vacuously complete), matching finalize's remaining==0 => Filled rule, instead of the inconsistent is_complete=false / NotFilled. - matchable_quantity comment no longer overclaims it mirrors the sweep's pure sequence order; clarified it visits (timestamp, sequence) order but the fillable TOTAL is visit-order invariant (which is all FOK depends on). - is_matchable rustdoc: dropped the broken intra-doc links to the private has_matchable_depth / matchable_quantity helpers; use plain code spans.
|
Thanks, all four addressed: zero-quantity MatchResult is now internally consistent at construction, the matchable_quantity comment is accurate about visit-order vs the order-invariant total, and the is_matchable doc links to the private helpers are dropped. |
Summary
match_orderpreviously ignored the taker's time-in-force / order kind andfilled greedily. It now honors them with explicit single-level semantics. The
user approved this as a breaking change for the 0.9 window.
Changes
match_ordergainstaker_tif: TimeInForceandtaker_kind: TakerKind({Standard, PostOnly, MarketToLimit}), applied via a mutation-free pre-check
before the FIFO sweep:
< incoming; else fills fully.MatchOutcome{Filled, PartiallyFilled, NotFilled, Killed,Rejected} on
MatchResult(#[serde(default)]) +outcome()/was_killed()/
was_rejected(). Existing fields/accessors unchanged.NotFilledseparates"empty level" from a FOK
Killed/ PostOnlyRejected.TakerKindtype (src/execution/taker.rs).Blocker fix (found by microstructure-critic review)
A zero-visible iceberg/reserve with hidden depth made
match_againstreturnconsumed=0+ re-queue-same → infinite loop (pre-existing in the GTC sweep;newly reachable from the FOK dry-run this PR adds). Fixed by:
visible == 0(normalvisible > 0pathunchanged);
stalled makers are reinserted at their original sequence, so makers behind
still match and price-time priority holds, with no counter drift;
is_matchable()so PostOnly (has_matchable_depth) and FOK(
matchable_quantity) agree on depth.A second
microstructure-criticpass verified the fix SOUND (FOK dry-runstill exactly predicts the sweep; FIFO/counters/determinism intact).
Public API impact
Breaking:
match_order(qty, taker_id, tif, kind, ts, &gen). Migration default topreserve old behavior:
TimeInForce::Gtc, TakerKind::Standard. Migration note insrc/lib.rs;README.mdregenerated. New exports:MatchOutcome,TakerKind.Testing
iceberg-hidden drain), IOC discard, PostOnly reject/accept/zero-qty, M2L
fill-available/full; the Complete the per-order-type {empty, partial, full, type-branch} test matrix #78/Add TIF boundary, GTD/Day expiry, and average_price edge tests #79 pins corrected to real semantics
behind; update-to-zero) — all terminate
MatchOutcomeserde round-trip + lockstep testsmake pre-pushcleancargo test: 406 (lib) + 1 (integration) + 9 (doctests) passed, 0 failed.cargo clippy --all-targets --all-features -- -D warnings(incl.missing_errors_doc),cargo fmt --all --check,cargo build --release: clean.Checklist
fill-available); reviewed SOUND
.unwrap(); hand-rolledPriceLevelErrorsrc/lib.rsmigration guide +README.mdupdatedCloses #65