1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! Contains the core matching engine logic for the order book.
//!
//! The matching engine supports Self-Trade Prevention (STP) when configured
//! via [`crate::STPMode`]. When STP is disabled (`STPMode::None`, the default),
//! the matching hot path is unchanged with zero overhead.
use crate::orderbook::book_change_event::PriceLevelChangedEvent;
use crate::orderbook::order_state::{CancelReason, OrderStatus};
use crate::orderbook::pool::MatchingPool;
use crate::orderbook::stp::{STPAction, check_stp_at_level};
use crate::{OrderBook, OrderBookError};
use either::Either;
use pricelevel::{Hash32, Id, MatchResult, OrderUpdate, Side};
use std::sync::atomic::Ordering;
impl<T> OrderBook<T>
where
T: Clone + Send + Sync + Default + 'static,
{
/// Highly optimized internal matching function.
///
/// This is the backward-compatible entry point that delegates to
/// [`Self::match_order_with_user`] with `Hash32::zero()` (bypasses STP).
///
/// # Performance Optimization
/// Uses SkipMap which maintains prices in sorted order automatically.
/// This eliminates O(N log N) sorting overhead, reducing time complexity
/// from O(N log N) to O(M log N), where:
/// - N = total number of price levels
/// - M = number of price levels actually matched (typically << N)
///
/// In the happy case (single price level fill), complexity is O(log N).
pub fn match_order(
&self,
order_id: Id,
side: Side,
quantity: u64,
limit_price: Option<u128>,
) -> Result<MatchResult, OrderBookError> {
self.match_order_with_user(order_id, side, quantity, limit_price, Hash32::zero())
}
/// Internal matching function with Self-Trade Prevention support.
///
/// When `taker_user_id` is `Hash32::zero()` or `stp_mode` is `None`,
/// the STP check is skipped entirely (zero overhead fast path).
///
/// # Arguments
/// * `order_id` — The taker (incoming) order's unique identifier.
/// * `side` — The side of the incoming order (`Buy` or `Sell`).
/// * `quantity` — The quantity to match.
/// * `limit_price` — Optional price limit (`None` for market orders).
/// * `taker_user_id` — The user ID of the incoming order for STP checks.
///
/// # Errors
/// Returns [`OrderBookError::InsufficientLiquidity`] for market orders
/// when no liquidity is available, or [`OrderBookError::SelfTradePrevented`]
/// when STP in `CancelTaker` or `CancelBoth` mode cancels the entire taker.
pub fn match_order_with_user(
&self,
order_id: Id,
side: Side,
quantity: u64,
limit_price: Option<u128>,
taker_user_id: Hash32,
) -> Result<MatchResult, OrderBookError> {
self.cache.invalidate();
let mut match_result = MatchResult::new(order_id, quantity);
let mut remaining_quantity = quantity;
// Determine if STP checks are needed for this match
let stp_active = self.stp_mode.is_enabled() && taker_user_id != Hash32::zero();
// Choose the appropriate side for matching
let match_side = match side {
Side::Buy => &self.asks,
Side::Sell => &self.bids,
};
// Early exit if the opposite side is empty
if match_side.is_empty() {
if limit_price.is_none() {
return Err(OrderBookError::InsufficientLiquidity {
side,
requested: quantity,
available: 0,
});
}
// remaining_quantity is managed by add_trade(); no manual update needed
return Ok(match_result);
}
// Use static memory pool for better performance
thread_local! {
static MATCHING_POOL: MatchingPool = MatchingPool::new();
}
// Get reusable vectors from pool
let (mut filled_orders, mut empty_price_levels) = MATCHING_POOL.with(|pool| {
let filled = pool.get_filled_orders_vec();
let empty = pool.get_price_vec();
(filled, empty)
});
// Track whether STP cancelled the taker
let mut stp_taker_cancelled = false;
// Iterate through prices in optimal order (already sorted by SkipMap)
// For buy orders: iterate asks in ascending order (best ask first)
// For sell orders: iterate bids in descending order (best bid first)
let price_iter = match side {
Side::Buy => Either::Left(match_side.iter()),
Side::Sell => Either::Right(match_side.iter().rev()),
};
// Process each price level
for entry in price_iter {
let price = *entry.key();
// Check price limit constraint early
if let Some(limit) = limit_price {
match side {
Side::Buy if price > limit => break,
Side::Sell if price < limit => break,
_ => {}
}
}
// Get price level value from the entry
let price_level = entry.value();
// --- STP pre-processing ---
// When STP is active, check for self-trade conflicts before matching.
// This is done per-price-level to handle partial fills correctly.
if stp_active {
let orders: Vec<_> = price_level.iter_orders().collect();
let action = check_stp_at_level(&orders, taker_user_id, self.stp_mode);
match action {
STPAction::NoConflict => {
// No self-trade at this level; match normally below
}
STPAction::CancelTaker { safe_quantity } => {
// Match up to safe_quantity, then cancel the taker
if safe_quantity > 0 {
let match_qty = remaining_quantity.min(safe_quantity);
let saved_remaining = remaining_quantity;
let price_level_match = price_level.match_order(
match_qty,
order_id,
&self.transaction_id_generator,
);
// Compute actual executed from the sub-match
let executed =
match_qty.saturating_sub(price_level_match.remaining_quantity());
Self::process_level_match(
&mut match_result,
&price_level_match,
&mut filled_orders,
&mut remaining_quantity,
price,
price_level,
side,
&self.last_trade_price,
&self.has_traded,
&self.price_level_changed_listener,
&mut empty_price_levels,
);
// Correct remaining: process_level_match set it to
// sub-match remaining, but we need overall remaining.
remaining_quantity = saved_remaining.saturating_sub(executed);
}
stp_taker_cancelled = true;
break;
}
STPAction::CancelMaker { maker_order_ids } => {
// Cancel same-user resting orders, then match normally.
// Look up each maker's user_id from the snapshot rather
// than assuming it equals taker_user_id.
for maker_id in &maker_order_ids {
let maker_user_id = orders
.iter()
.find(|o| o.id() == *maker_id)
.map(|o| o.user_id())
.unwrap_or(taker_user_id);
let _ = price_level.update_order(OrderUpdate::Cancel {
order_id: *maker_id,
});
self.order_locations.remove(maker_id);
self.untrack_user_order(maker_user_id, maker_id);
}
// If the level is now empty, mark for removal and continue
if price_level.order_count() == 0 {
empty_price_levels.push(price);
continue;
}
// Fall through to normal matching below
}
STPAction::CancelBoth {
safe_quantity,
maker_order_id,
} => {
// Match up to safe_quantity, cancel the maker, then cancel taker
if safe_quantity > 0 {
let match_qty = remaining_quantity.min(safe_quantity);
let saved_remaining = remaining_quantity;
let price_level_match = price_level.match_order(
match_qty,
order_id,
&self.transaction_id_generator,
);
let executed =
match_qty.saturating_sub(price_level_match.remaining_quantity());
Self::process_level_match(
&mut match_result,
&price_level_match,
&mut filled_orders,
&mut remaining_quantity,
price,
price_level,
side,
&self.last_trade_price,
&self.has_traded,
&self.price_level_changed_listener,
&mut empty_price_levels,
);
// Correct remaining: process_level_match set it to
// sub-match remaining, but we need overall remaining.
remaining_quantity = saved_remaining.saturating_sub(executed);
}
// Cancel the maker order — look up its user_id from the
// snapshot rather than assuming it equals taker_user_id.
let maker_user_id = orders
.iter()
.find(|o| o.id() == maker_order_id)
.map(|o| o.user_id())
.unwrap_or(taker_user_id);
let _ = price_level.update_order(OrderUpdate::Cancel {
order_id: maker_order_id,
});
self.order_locations.remove(&maker_order_id);
self.untrack_user_order(maker_user_id, &maker_order_id);
if price_level.order_count() == 0 {
empty_price_levels.push(price);
}
stp_taker_cancelled = true;
break;
}
}
}
// --- Normal matching (no STP conflict or after CancelMaker cleanup) ---
let price_level_match = price_level.match_order(
remaining_quantity,
order_id,
&self.transaction_id_generator,
);
Self::process_level_match(
&mut match_result,
&price_level_match,
&mut filled_orders,
&mut remaining_quantity,
price,
price_level,
side,
&self.last_trade_price,
&self.has_traded,
&self.price_level_changed_listener,
&mut empty_price_levels,
);
// Early exit if order is fully matched
if remaining_quantity == 0 {
break;
}
}
// Batch remove empty price levels
for price in &empty_price_levels {
match_side.remove(price);
}
// Batch remove filled orders from tracking and update state
for filled_id in &filled_orders {
// Track the resting order as Filled (quantity unknown here;
// use 0 as placeholder — the important thing is the terminal state)
self.track_state(*filled_id, OrderStatus::Filled { filled_quantity: 0 });
self.order_locations.remove(filled_id);
self.untrack_order_by_id(filled_id);
}
// Return vectors to pool for reuse
MATCHING_POOL.with(|pool| {
pool.return_filled_orders_vec(filled_orders);
pool.return_price_vec(empty_price_levels);
});
// If STP cancelled the taker and no fills occurred at all, return STP error.
// When partial fills happened (remaining < original quantity), return Ok
// with the partial result so the caller can see what was executed.
if stp_taker_cancelled && remaining_quantity == quantity {
self.track_state(
order_id,
OrderStatus::Cancelled {
filled_quantity: 0,
reason: CancelReason::SelfTradePrevention,
},
);
return Err(OrderBookError::SelfTradePrevented {
mode: self.stp_mode,
taker_order_id: order_id,
user_id: taker_user_id,
});
}
// Check for insufficient liquidity in market orders
if limit_price.is_none() && remaining_quantity == quantity {
return Err(OrderBookError::InsufficientLiquidity {
side,
requested: quantity,
available: 0,
});
}
// remaining_quantity is managed by add_trade(); no manual update needed
Ok(match_result)
}
/// Processes match results from a single price level, updating the
/// aggregate match result and bookkeeping vectors.
///
/// Extracted to avoid code duplication between the normal path and
/// the STP safe-quantity pre-match path.
#[allow(clippy::too_many_arguments)]
fn process_level_match(
match_result: &mut MatchResult,
price_level_match: &MatchResult,
filled_orders: &mut Vec<Id>,
remaining_quantity: &mut u64,
price: u128,
price_level: &std::sync::Arc<pricelevel::PriceLevel>,
side: Side,
last_trade_price: &crossbeam::atomic::AtomicCell<u128>,
has_traded: &std::sync::atomic::AtomicBool,
price_level_changed_listener: &Option<
crate::orderbook::book_change_event::PriceLevelChangedListener,
>,
empty_price_levels: &mut Vec<u128>,
) {
// Process trades if any occurred
if !price_level_match.trades().as_vec().is_empty() {
// Update last trade price atomically
last_trade_price.store(price);
has_traded.store(true, Ordering::Relaxed);
// Add trades to result
for trade in price_level_match.trades().as_vec() {
// add_trade returns Result in v0.7; ignore error since
// pricelevel already validated the quantities during matching
let _ = match_result.add_trade(*trade);
}
// Notify price level changes
if let Some(listener) = price_level_changed_listener {
listener(PriceLevelChangedEvent {
side: side.opposite(),
price: price_level.price(),
quantity: price_level.visible_quantity(),
});
}
}
// Collect filled orders for batch removal
for &filled_order_id in price_level_match.filled_order_ids() {
match_result.add_filled_order_id(filled_order_id);
filled_orders.push(filled_order_id);
}
// Update remaining quantity
*remaining_quantity = price_level_match.remaining_quantity();
// Check if price level is empty and mark for removal
if price_level.order_count() == 0 {
empty_price_levels.push(price);
}
}
/// Optimized peek match without memory pooling or sorting
///
/// # Performance Optimization
/// Uses SkipMap's natural ordering to eliminate sorting overhead.
/// Time complexity: O(M log N) where M = price levels inspected.
pub fn peek_match(&self, side: Side, quantity: u64, price_limit: Option<u128>) -> u64 {
let price_levels = match side {
Side::Buy => &self.asks,
Side::Sell => &self.bids,
};
if price_levels.is_empty() {
return 0;
}
let mut matched_quantity = 0u64;
// Iterate through prices in optimal order (already sorted by SkipMap)
let price_iter = match side {
Side::Buy => Either::Left(price_levels.iter()),
Side::Sell => Either::Right(price_levels.iter().rev()),
};
// Process each price level
for entry in price_iter {
// Early termination when we have enough quantity
if matched_quantity >= quantity {
break;
}
let price = *entry.key();
// Check price limit
if let Some(limit) = price_limit {
match side {
Side::Buy if price > limit => break,
Side::Sell if price < limit => break,
_ => {}
}
}
// Get available quantity at this level
let price_level = entry.value();
let available_quantity = price_level.total_quantity().unwrap_or(0);
let needed_quantity = quantity.saturating_sub(matched_quantity);
let quantity_to_match = needed_quantity.min(available_quantity);
matched_quantity = matched_quantity.saturating_add(quantity_to_match);
}
matched_quantity
}
/// Batch operation for multiple order matches (additional optimization)
pub fn match_orders_batch(
&self,
orders: &[(Id, Side, u64, Option<u128>)],
) -> Vec<Result<MatchResult, OrderBookError>> {
let mut results = Vec::with_capacity(orders.len());
for &(order_id, side, quantity, limit_price) in orders {
let result = OrderBook::<T>::match_order(self, order_id, side, quantity, limit_price);
results.push(result);
}
results
}
}