Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,53 @@ snapshot/replay equivalence.
Pass the taker's arrival timestamp (or any deterministic value for
tests/replay), e.g. [`TimestampMs::new`].

### Migration Guide (taker time-in-force / kind semantics — breaking)

[`PriceLevel::match_order`] now **honors the taker's** [`TimeInForce`] and a
new [`TakerKind`]. Two parameters are inserted **between** `taker_order_id`
and `timestamp`:

| Before | After |
|--------|-------|
| `level.match_order(qty, taker_id, ts, &gen)` | `level.match_order(qty, taker_id, tif, kind, ts, &gen)` |

To preserve the previous "fill what you can, report the remainder" behavior,
pass [`TimeInForce::Gtc`] and [`TakerKind::Standard`].

**New single-level semantics.** Let `available` be the quantity this level
can actually fill for the taker, capped at the incoming quantity:

- [`TakerKind::PostOnly`]: rejected if `available > 0` (would take
liquidity) — zero trades, full remainder, queue untouched.
- [`TimeInForce::Fok`]: killed if `available < incoming` — zero trades, full
remainder, queue untouched; otherwise filled completely.
- [`TimeInForce::Ioc`]: fills `available`, discards the remainder (the taker
is never rested by this layer).
- [`TimeInForce::Gtc`] / [`TimeInForce::Gtd`] / [`TimeInForce::Day`] and
[`TakerKind::MarketToLimit`]: fill `available`, report the remainder in
[`MatchResult::remaining_quantity`] for the order book to rest / convert.

**New `MatchResult` signal.** A fill-or-kill *kill* and a post-only
*rejection* both leave zero trades and the full remainder — indistinguishable
through the old fields from "the level had no liquidity". [`MatchResult`]
gains an additive [`MatchOutcome`] (`Filled` / `PartiallyFilled` /
`NotFilled` / `Killed` / `Rejected`), read via
[`MatchResult::outcome`](crate::execution::MatchResult::outcome),
[`MatchResult::was_killed`](crate::execution::MatchResult::was_killed), and
[`MatchResult::was_rejected`](crate::execution::MatchResult::was_rejected).
All existing fields and accessors are unchanged. The field is
`#[serde(default)]` so older JSON deserializes (as `NotFilled`); the text
`Display` / `FromStr` format is unchanged and re-derives the benign outcome
on parse (a `Killed` / `Rejected` signal is not carried by the text format).

Resting-maker time-in-force expiry is still **not** enforced by the match
path — only the *taker's* intent is honored here. Skipping / evicting expired
makers remains the order book's responsibility.

[`TakerKind`]: crate::execution::TakerKind
[`MatchOutcome`]: crate::execution::MatchOutcome
[`MatchResult::remaining_quantity`]: crate::execution::MatchResult::remaining_quantity

### Migration Guide (snapshot format v1 → v2)

The checksum-protected snapshot format now persists per-level statistics
Expand Down
6 changes: 5 additions & 1 deletion benches/concurrent/contention.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::{BenchmarkId, Criterion};
use pricelevel::{
Hash32, Id, OrderType, OrderUpdate, Price, PriceLevel, Quantity, Side, TimeInForce,
Hash32, Id, OrderType, OrderUpdate, Price, PriceLevel, Quantity, Side, TakerKind, TimeInForce,
TimestampMs, UuidGenerator,
};
use std::sync::{Arc, Barrier};
Expand Down Expand Up @@ -110,6 +110,8 @@ fn measure_read_write_contention(
thread_price_level.match_order(
2,
taker_id,
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&thread_transaction_id_gen,
);
Expand Down Expand Up @@ -225,6 +227,8 @@ fn measure_hot_spot_contention(
thread_price_level.match_order(
1,
taker_id,
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&thread_transaction_id_gen,
);
Expand Down
6 changes: 5 additions & 1 deletion benches/concurrent/register.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use criterion::{BenchmarkId, Criterion, criterion_group};
use pricelevel::{
Hash32, Id, OrderType, OrderUpdate, PegReferenceType, Price, PriceLevel, Quantity, Side,
TimeInForce, TimestampMs, UuidGenerator,
TakerKind, TimeInForce, TimestampMs, UuidGenerator,
};
use std::num::NonZeroU64;
use std::sync::{Arc, Barrier};
Expand Down Expand Up @@ -76,6 +76,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
price_level.match_order(
5,
taker_id,
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
);
Expand Down Expand Up @@ -315,6 +317,8 @@ fn measure_concurrent_mixed_operations(thread_count: usize, iterations: u64) ->
thread_price_level.match_order(
5,
taker_id,
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&thread_transaction_id_gen,
);
Expand Down
12 changes: 11 additions & 1 deletion benches/price_level/checked_arithmetic.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::Criterion;
use pricelevel::{
Hash32, Id, MatchResult, OrderType, Price, PriceLevel, Quantity, Side, TimeInForce,
Hash32, Id, MatchResult, OrderType, Price, PriceLevel, Quantity, Side, TakerKind, TimeInForce,
TimestampMs, Trade, UuidGenerator,
};
use std::hint::black_box;
Expand Down Expand Up @@ -28,6 +28,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let result = level.match_order(
500,
Id::from_u64(9999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand All @@ -42,6 +44,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let result = level.match_order(
500,
Id::from_u64(9999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand All @@ -56,6 +60,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let result = level.match_order(
500,
Id::from_u64(9999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand Down Expand Up @@ -89,6 +95,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let result = empty_level.match_order(
100,
Id::from_u64(1),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand All @@ -107,6 +115,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let result = level.match_order(
tc * 10,
Id::from_u64(9999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand Down
10 changes: 9 additions & 1 deletion benches/price_level/lifecycle.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::Criterion;
use pricelevel::{
Hash32, Id, OrderType, OrderUpdate, Price, PriceLevel, Quantity, Side, TimeInForce,
Hash32, Id, OrderType, OrderUpdate, Price, PriceLevel, Quantity, Side, TakerKind, TimeInForce,
TimestampMs, UuidGenerator,
};
use std::hint::black_box;
Expand Down Expand Up @@ -42,6 +42,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let result = level.match_order(
500,
Id::from_u64(9999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand All @@ -67,6 +69,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(level.match_order(
10,
Id::from_u64(10000 + i),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
));
Expand Down Expand Up @@ -115,6 +119,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let result = level.match_order(
1000,
Id::from_u64(9999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand Down Expand Up @@ -180,6 +186,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let result = level.match_order(
100,
Id::from_u64(9999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand Down
18 changes: 17 additions & 1 deletion benches/price_level/match_orders.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::{BenchmarkId, Criterion};
use pricelevel::{
Hash32, Id, OrderType, Price, PriceLevel, Quantity, Side, TimeInForce, TimestampMs,
Hash32, Id, OrderType, Price, PriceLevel, Quantity, Side, TakerKind, TimeInForce, TimestampMs,
UuidGenerator,
};
use std::hint::black_box;
Expand All @@ -21,6 +21,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
50,
Id::from_u64(999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand All @@ -36,6 +38,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
75,
Id::from_u64(999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand All @@ -51,6 +55,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
60,
Id::from_u64(999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand All @@ -66,6 +72,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
100,
Id::from_u64(999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand All @@ -85,6 +93,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
5,
Id::from_u64(999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand All @@ -102,6 +112,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
3,
Id::from_u64(1000 + i),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand All @@ -123,6 +135,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
match_quantity,
Id::from_u64(999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand All @@ -145,6 +159,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
match_quantity,
Id::from_u64(999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand Down
14 changes: 13 additions & 1 deletion benches/price_level/mixed_operations.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::Criterion;
use pricelevel::{
Hash32, Id, OrderType, OrderUpdate, Price, PriceLevel, Quantity, Side, TimeInForce,
Hash32, Id, OrderType, OrderUpdate, Price, PriceLevel, Quantity, Side, TakerKind, TimeInForce,
TimestampMs, UuidGenerator,
};
use std::hint::black_box;
Expand Down Expand Up @@ -33,6 +33,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let _ = black_box(price_level.match_order(
50,
Id::from_u64(999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand Down Expand Up @@ -67,6 +69,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
100,
Id::from_u64(1000),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand All @@ -93,6 +97,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
2,
Id::from_u64(1000 + i),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand Down Expand Up @@ -128,18 +134,24 @@ pub fn register_benchmarks(c: &mut Criterion) {
black_box(price_level.match_order(
300,
Id::from_u64(1001),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
black_box(price_level.match_order(
400,
Id::from_u64(1002),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
black_box(price_level.match_order(
300,
Id::from_u64(1003),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&transaction_id_generator,
));
Expand Down
4 changes: 3 additions & 1 deletion benches/price_level/serialization.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::Criterion;
use pricelevel::{
Hash32, Id, MatchResult, OrderType, Price, PriceLevel, Quantity, Side, TimeInForce,
Hash32, Id, MatchResult, OrderType, Price, PriceLevel, Quantity, Side, TakerKind, TimeInForce,
TimestampMs, Trade, TradeList, UuidGenerator,
};
use std::hint::black_box;
Expand Down Expand Up @@ -91,6 +91,8 @@ pub fn register_benchmarks(c: &mut Criterion) {
let match_result = level.match_order(
200,
Id::from_u64(9999),
TimeInForce::Gtc,
TakerKind::Standard,
TimestampMs::new(1_716_000_000_000),
&id_gen,
);
Expand Down
Loading
Loading