use std::sync::Mutex;
use indexmap::IndexMap;
use nautilus_common::cache::fifo::{FifoCache, FifoCacheMap};
use nautilus_core::MUTEX_POISONED;
#[cfg(test)]
use nautilus_model::identifiers::InstrumentId;
use nautilus_model::{
enums::OrderSide,
events::OrderFilled,
identifiers::{ClientOrderId, VenueOrderId},
reports::{FillReport, OrderStatusReport},
types::Quantity,
};
use rust_decimal::Decimal;
use ustr::Ustr;
use crate::common::consts::DUST_SNAP_THRESHOLD_DEC;
#[derive(Debug, Clone, Copy)]
struct OrderFillState {
submitted_qty: Quantity,
cumulative_filled: Quantity,
order_side: OrderSide,
}
#[derive(Clone, Debug)]
pub(crate) struct FillCorrectionMetadata {
pub correction_key: String,
pub info: Option<IndexMap<Ustr, Ustr>>,
pub is_confirmed: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct BufferedFill {
pub report: FillReport,
pub correction: Option<FillCorrectionMetadata>,
}
#[derive(Debug, Default)]
struct TrackerInner {
orders: FifoCacheMap<VenueOrderId, OrderFillState, 10_000>,
pending_fills: FifoCacheMap<VenueOrderId, Vec<BufferedFill>, 1_000>,
pending_reports: FifoCacheMap<VenueOrderId, Vec<OrderStatusReport>, 1_000>,
voided_trades: FifoCache<String, 10_000>,
confirmed_trades: FifoCache<String, 10_000>,
applied_buffered_fills: FifoCacheMap<String, Vec<OrderFilled>, 10_000>,
}
#[derive(Debug)]
pub(crate) struct OrderFillTrackerMap {
inner: Mutex<TrackerInner>,
}
impl OrderFillTrackerMap {
pub(crate) fn new() -> Self {
Self {
inner: Mutex::new(TrackerInner::default()),
}
}
pub(crate) fn restore_order(
&self,
venue_order_id: VenueOrderId,
submitted_qty: Quantity,
filled_qty: Quantity,
order_side: OrderSide,
) {
let mut state = new_order_state(submitted_qty, order_side);
state.cumulative_filled = filled_qty;
self.inner
.lock()
.expect(MUTEX_POISONED)
.orders
.insert(venue_order_id, state);
}
pub(crate) fn contains(&self, venue_order_id: &VenueOrderId) -> bool {
self.inner
.lock()
.expect(MUTEX_POISONED)
.orders
.get(venue_order_id)
.is_some()
}
pub(crate) fn has_fills_or_settled(&self, venue_order_id: &VenueOrderId) -> bool {
match self
.inner
.lock()
.expect(MUTEX_POISONED)
.orders
.get(venue_order_id)
{
Some(s) => !s.cumulative_filled.is_zero(),
None => true, }
}
pub(crate) fn get_cumulative_filled(&self, venue_order_id: &VenueOrderId) -> Option<Quantity> {
self.inner
.lock()
.expect(MUTEX_POISONED)
.orders
.get(venue_order_id)
.map(|s| s.cumulative_filled)
}
pub(crate) fn is_fully_filled(&self, venue_order_id: &VenueOrderId) -> bool {
self.inner
.lock()
.expect(MUTEX_POISONED)
.orders
.get(venue_order_id)
.is_some_and(|s| s.cumulative_filled >= s.submitted_qty)
}
pub(crate) fn accept_or_buffer_fill(
&self,
venue_order_id: VenueOrderId,
report: FillReport,
correction: FillCorrectionMetadata,
) -> Option<FillReport> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
if guard.orders.get(&venue_order_id).is_some() {
record_fill_in(&mut guard.orders, &venue_order_id, report.last_qty);
Some(report)
} else {
push_buffered(
&mut guard.pending_fills,
venue_order_id,
BufferedFill {
report,
correction: Some(correction),
},
);
None
}
}
pub(crate) fn accept_or_buffer_report(
&self,
venue_order_id: VenueOrderId,
report: OrderStatusReport,
) -> Option<OrderStatusReport> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
if guard.orders.get(&venue_order_id).is_some() {
Some(report)
} else {
push_buffered(&mut guard.pending_reports, venue_order_id, report);
None
}
}
pub(crate) fn register_and_take_pending_fills(
&self,
venue_order_id: VenueOrderId,
client_order_id: Option<ClientOrderId>,
submitted_qty: Quantity,
order_side: OrderSide,
) -> Vec<BufferedFill> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
guard
.orders
.insert(venue_order_id, new_order_state(submitted_qty, order_side));
take_and_prepare_fills(&mut guard, venue_order_id, client_order_id)
}
pub(crate) fn register_and_take_pending_fills_if_buffered(
&self,
venue_order_id: VenueOrderId,
client_order_id: Option<ClientOrderId>,
submitted_qty: Quantity,
order_side: OrderSide,
) -> Option<Vec<BufferedFill>> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
if !guard.pending_fills.contains_key(&venue_order_id) {
return None;
}
guard
.orders
.insert(venue_order_id, new_order_state(submitted_qty, order_side));
Some(take_and_prepare_fills(
&mut guard,
venue_order_id,
client_order_id,
))
}
pub(crate) fn take_pending_fills(
&self,
venue_order_id: VenueOrderId,
client_order_id: Option<ClientOrderId>,
) -> Vec<BufferedFill> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
take_and_prepare_fills(&mut guard, venue_order_id, client_order_id)
}
pub(crate) fn take_pending_reports(
&self,
venue_order_id: &VenueOrderId,
) -> Vec<OrderStatusReport> {
self.inner
.lock()
.expect(MUTEX_POISONED)
.pending_reports
.remove(venue_order_id)
.unwrap_or_default()
}
pub(crate) fn emit_buffered_fill<F>(
&self,
fill: OrderFilled,
correction: Option<&FillCorrectionMetadata>,
emit: F,
) -> bool
where
F: FnOnce(OrderFilled, Option<Quantity>),
{
let Some(correction) = correction else {
let new_qty = self.buy_overfill_bump(&fill.venue_order_id);
emit(fill, new_qty);
return true;
};
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
if guard.voided_trades.contains(&correction.correction_key) {
reverse_fill_in(&mut guard.orders, &fill.venue_order_id, fill.last_qty);
return false;
}
let new_qty = buy_overfill_bump_in(&mut guard.orders, &fill.venue_order_id);
emit(fill.clone(), new_qty);
if let Some(fills) = guard
.applied_buffered_fills
.get_mut(&correction.correction_key)
{
fills.push(fill);
} else {
guard
.applied_buffered_fills
.insert(correction.correction_key.clone(), vec![fill]);
}
true
}
pub(crate) fn void_buffered_trade(&self, correction_key: &str) -> Vec<OrderFilled> {
let key = correction_key.to_string();
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
guard.confirmed_trades.remove(&key);
guard.voided_trades.add(key.clone());
let fills = guard
.applied_buffered_fills
.remove(&key)
.unwrap_or_default();
for fill in &fills {
reverse_fill_in(&mut guard.orders, &fill.venue_order_id, fill.last_qty);
}
fills
}
pub(crate) fn mark_trade_confirmed(&self, correction_key: &str) {
self.inner
.lock()
.expect(MUTEX_POISONED)
.confirmed_trades
.add(correction_key.to_string());
}
#[must_use]
pub(crate) fn is_trade_confirmed(&self, correction_key: &str) -> bool {
self.inner
.lock()
.expect(MUTEX_POISONED)
.confirmed_trades
.contains(&correction_key.to_string())
}
pub(crate) fn reverse_fill(&self, venue_order_id: &VenueOrderId, quantity: Quantity) {
reverse_fill_in(
&mut self.inner.lock().expect(MUTEX_POISONED).orders,
venue_order_id,
quantity,
);
}
pub(crate) fn snap_fill_reports(&self, reports: &mut [FillReport]) {
let guard = self.inner.lock().expect(MUTEX_POISONED);
for report in reports {
report.last_qty =
snap_fill_qty_in(&guard.orders, &report.venue_order_id, report.last_qty);
}
}
pub(crate) fn snap_fill_qty(
&self,
venue_order_id: &VenueOrderId,
fill_qty: Quantity,
) -> Quantity {
let guard = self.inner.lock().expect(MUTEX_POISONED);
snap_fill_qty_in(&guard.orders, venue_order_id, fill_qty)
}
pub(crate) fn buy_overfill_bump(&self, venue_order_id: &VenueOrderId) -> Option<Quantity> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
buy_overfill_bump_in(&mut guard.orders, venue_order_id)
}
pub(crate) fn check_terminal_quantity_normalization(
&self,
venue_order_id: &VenueOrderId,
) -> Option<Quantity> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
let s = guard.orders.get(venue_order_id)?;
if s.cumulative_filled >= s.submitted_qty {
return None;
}
let leaves = s.submitted_qty.as_decimal() - s.cumulative_filled.as_decimal();
if leaves > Decimal::ZERO && leaves < DUST_SNAP_THRESHOLD_DEC {
let filled_qty = s.cumulative_filled;
log::debug!(
"Normalizing terminal order {venue_order_id} quantity from {} to {filled_qty} \
(non-economic leaves={leaves})",
s.submitted_qty,
);
guard.orders.remove(venue_order_id);
Some(filled_qty)
} else {
if leaves >= DUST_SNAP_THRESHOLD_DEC {
log::debug!(
"Order {venue_order_id} MATCHED with significant residual \
{leaves} (filled {}/{})",
s.cumulative_filled,
s.submitted_qty,
);
}
None
}
}
pub(crate) fn take_terminal_ioc_remainder(
&self,
venue_order_id: &VenueOrderId,
) -> Option<Quantity> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
let state = guard.orders.get(venue_order_id)?;
if state.cumulative_filled.is_zero() || state.cumulative_filled >= state.submitted_qty {
return None;
}
let remainder = state.submitted_qty - state.cumulative_filled;
guard.orders.remove(venue_order_id);
Some(remainder)
}
}
fn new_order_state(submitted_qty: Quantity, order_side: OrderSide) -> OrderFillState {
OrderFillState {
submitted_qty,
cumulative_filled: Quantity::zero(submitted_qty.precision),
order_side,
}
}
fn buy_overfill_bump_in(
orders: &mut FifoCacheMap<VenueOrderId, OrderFillState, 10_000>,
venue_order_id: &VenueOrderId,
) -> Option<Quantity> {
let state = orders.get_mut(venue_order_id)?;
if state.order_side != OrderSide::Buy {
return None;
}
if state.cumulative_filled > state.submitted_qty {
state.submitted_qty = state.cumulative_filled;
Some(state.cumulative_filled)
} else {
None
}
}
fn take_and_prepare_fills(
inner: &mut TrackerInner,
venue_order_id: VenueOrderId,
client_order_id: Option<ClientOrderId>,
) -> Vec<BufferedFill> {
let Some(buffered) = inner.pending_fills.remove(&venue_order_id) else {
return Vec::new();
};
buffered
.into_iter()
.map(|mut buffered| {
buffered.report.client_order_id = client_order_id;
buffered.report.last_qty =
snap_fill_qty_in(&inner.orders, &venue_order_id, buffered.report.last_qty);
record_fill_in(&mut inner.orders, &venue_order_id, buffered.report.last_qty);
buffered
})
.collect()
}
fn push_buffered<V>(
buffer: &mut FifoCacheMap<VenueOrderId, Vec<V>, 1_000>,
venue_order_id: VenueOrderId,
value: V,
) {
if let Some(values) = buffer.get_mut(&venue_order_id) {
values.push(value);
} else {
buffer.insert(venue_order_id, vec![value]);
}
}
#[cfg(test)]
impl OrderFillTrackerMap {
pub(crate) fn register(
&self,
venue_order_id: VenueOrderId,
submitted_qty: Quantity,
order_side: OrderSide,
_instrument_id: InstrumentId,
_size_precision: u8,
_price_precision: u8,
) {
self.inner
.lock()
.expect(MUTEX_POISONED)
.orders
.insert(venue_order_id, new_order_state(submitted_qty, order_side));
}
pub(crate) fn record_fill(&self, venue_order_id: &VenueOrderId, qty: Quantity) {
record_fill_in(
&mut self.inner.lock().expect(MUTEX_POISONED).orders,
venue_order_id,
qty,
);
}
pub(crate) fn buffer_fill_for_test(&self, venue_order_id: VenueOrderId, report: FillReport) {
push_buffered(
&mut self.inner.lock().expect(MUTEX_POISONED).pending_fills,
venue_order_id,
BufferedFill {
report,
correction: None,
},
);
}
pub(crate) fn buffer_report_for_test(
&self,
venue_order_id: VenueOrderId,
report: OrderStatusReport,
) {
push_buffered(
&mut self.inner.lock().expect(MUTEX_POISONED).pending_reports,
venue_order_id,
report,
);
}
pub(crate) fn has_pending_fill(&self, venue_order_id: &VenueOrderId) -> bool {
self.inner
.lock()
.expect(MUTEX_POISONED)
.pending_fills
.contains_key(venue_order_id)
}
pub(crate) fn pending_fills_for(&self, venue_order_id: &VenueOrderId) -> Vec<FillReport> {
self.inner
.lock()
.expect(MUTEX_POISONED)
.pending_fills
.get(venue_order_id)
.map(|fills| fills.iter().map(|fill| fill.report.clone()).collect())
.unwrap_or_default()
}
pub(crate) fn has_pending_report(&self, venue_order_id: &VenueOrderId) -> bool {
self.inner
.lock()
.expect(MUTEX_POISONED)
.pending_reports
.contains_key(venue_order_id)
}
}
fn record_fill_in(
orders: &mut FifoCacheMap<VenueOrderId, OrderFillState, 10_000>,
venue_order_id: &VenueOrderId,
qty: Quantity,
) {
if let Some(s) = orders.get_mut(venue_order_id) {
s.cumulative_filled = s.cumulative_filled + qty;
}
}
fn reverse_fill_in(
orders: &mut FifoCacheMap<VenueOrderId, OrderFillState, 10_000>,
venue_order_id: &VenueOrderId,
qty: Quantity,
) {
if let Some(state) = orders.get_mut(venue_order_id) {
state.cumulative_filled = if qty >= state.cumulative_filled {
Quantity::zero(state.cumulative_filled.precision)
} else {
state.cumulative_filled - qty
};
}
}
fn snap_fill_qty_in(
orders: &FifoCacheMap<VenueOrderId, OrderFillState, 10_000>,
venue_order_id: &VenueOrderId,
fill_qty: Quantity,
) -> Quantity {
match orders.get(venue_order_id) {
Some(s) => {
let diff = s.submitted_qty.as_decimal() - fill_qty.as_decimal();
if diff < Decimal::ZERO && diff.abs() < DUST_SNAP_THRESHOLD_DEC {
log::debug!(
"Snapping overfill {fill_qty} -> {} (dust={diff})",
s.submitted_qty,
);
s.submitted_qty
} else {
fill_qty
}
}
None => fill_qty,
}
}
#[cfg(test)]
mod tests {
use nautilus_core::{UUID4, UnixNanos};
use nautilus_model::{
enums::LiquiditySide,
identifiers::{AccountId, TradeId},
types::{Currency, Money, Price},
};
use rstest::rstest;
use super::*;
fn pusd() -> Currency {
Currency::pUSD()
}
#[rstest]
fn test_register_and_contains() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
assert!(!tracker.contains(&vid));
tracker.register(
vid,
Quantity::from("100"),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
assert!(tracker.contains(&vid));
}
#[rstest]
fn test_failed_trade_suppresses_buffered_fill_drained_later() {
use std::cell::Cell;
use nautilus_model::{
enums::OrderType,
identifiers::{StrategyId, TraderId},
};
let tracker = OrderFillTrackerMap::new();
let venue_order_id = VenueOrderId::from("order-failed-before-drain");
let instrument_id = InstrumentId::from("TEST.POLYMARKET");
let report = FillReport {
account_id: AccountId::from("POLY-001"),
instrument_id,
venue_order_id,
trade_id: TradeId::from("trade-failed-before-drain"),
order_side: OrderSide::Buy,
last_qty: Quantity::new(5.0, 6),
last_px: Price::new(0.55, 2),
commission: Money::zero(pusd()),
liquidity_side: LiquiditySide::Taker,
avg_px: None,
report_id: UUID4::new(),
ts_event: UnixNanos::default(),
ts_init: UnixNanos::default(),
client_order_id: None,
venue_position_id: None,
};
let correction_key = "trade-failed-before-drain-order-failed-before-drain";
let accepted = tracker.accept_or_buffer_fill(
venue_order_id,
report.clone(),
FillCorrectionMetadata {
correction_key: correction_key.to_string(),
info: None,
is_confirmed: false,
},
);
let prior_fills = tracker.void_buffered_trade(correction_key);
let drained = tracker.register_and_take_pending_fills(
venue_order_id,
Some(ClientOrderId::from("O-FAILED-BEFORE-DRAIN")),
Quantity::new(10.0, 6),
OrderSide::Buy,
);
let buffered = &drained[0];
let fill = OrderFilled::new(
TraderId::from("TESTER-001"),
StrategyId::from("S-001"),
instrument_id,
ClientOrderId::from("O-FAILED-BEFORE-DRAIN"),
venue_order_id,
report.account_id,
report.trade_id,
report.order_side,
OrderType::Limit,
report.last_qty,
report.last_px,
pusd(),
report.liquidity_side,
UUID4::new(),
report.ts_event,
report.ts_init,
false,
None,
Some(report.commission),
None,
);
let was_emitted = Cell::new(false);
let emitted = tracker.emit_buffered_fill(fill, buffered.correction.as_ref(), |_, _| {
was_emitted.set(true);
});
assert!(accepted.is_none());
assert!(prior_fills.is_empty());
assert_eq!(drained.len(), 1);
assert!(!emitted);
assert!(!was_emitted.get());
assert_eq!(
tracker.get_cumulative_filled(&venue_order_id),
Some(Quantity::zero(6))
);
}
#[rstest]
#[case::underfill_dust_preserved(23.696681, 23.690000, 23.690000)]
#[case::underfill_near_band_preserved(100.000000, 99.990100, 99.990100)]
#[case::underfill_at_band(100.000000, 99.990000, 99.990000)]
#[case::underfill_above_band(100.000000, 99.980000, 99.980000)]
#[case::large_underfill(100.000000, 50.000000, 50.000000)]
#[case::overfill_dust(714.285710, 714.285714, 714.285710)]
#[case::overfill_near_band(100.000000, 100.009900, 100.000000)]
#[case::overfill_at_band(100.000000, 100.010000, 100.010000)]
#[case::overfill_above_band(100.000000, 100.020000, 100.020000)]
#[case::large_overfill(100.000000, 150.000000, 150.000000)]
#[case::exact(100.000000, 100.000000, 100.000000)]
fn test_snap_fill_qty(#[case] submitted: f64, #[case] fill: f64, #[case] expected: f64) {
let tracker = OrderFillTrackerMap::new();
let venue_order_id = VenueOrderId::from("order-1");
tracker.register(
venue_order_id,
Quantity::new(submitted, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
let snapped = tracker.snap_fill_qty(&venue_order_id, Quantity::new(fill, 6));
assert_eq!(snapped, Quantity::new(expected, 6));
}
#[rstest]
#[case::underfill_within_band_preserved(100.000, 99.995, 99.995)]
#[case::underfill_above_band(100.000, 95.000, 95.000)]
#[case::overfill_within_band(100.000, 100.005, 100.000)]
#[case::overfill_above_band(100.000, 100.050, 100.050)]
fn test_snap_fill_qty_at_lower_precision(
#[case] submitted: f64,
#[case] fill: f64,
#[case] expected: f64,
) {
let tracker = OrderFillTrackerMap::new();
let venue_order_id = VenueOrderId::from("order-1");
tracker.register(
venue_order_id,
Quantity::new(submitted, 3),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
3,
2,
);
let snapped = tracker.snap_fill_qty(&venue_order_id, Quantity::new(fill, 3));
assert_eq!(snapped, Quantity::new(expected, 3));
}
#[rstest]
fn test_snap_fill_qty_unregistered_order() {
let tracker = OrderFillTrackerMap::new();
let venue_order_id = VenueOrderId::from("unknown");
let fill_qty = Quantity::new(50.0, 6);
let result = tracker.snap_fill_qty(&venue_order_id, fill_qty);
assert_eq!(result, fill_qty);
}
#[rstest]
fn test_snap_fill_reports_snaps_each_in_place() {
use nautilus_model::{
enums::LiquiditySide, identifiers::TradeId, reports::FillReport, types::Money,
};
let tracker = OrderFillTrackerMap::new();
let known_id = VenueOrderId::from("known");
let unknown_id = VenueOrderId::from("unknown");
tracker.register(
known_id,
Quantity::new(714.285710, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
let make_report =
|venue_order_id: VenueOrderId, last_qty: f64, commission: f64| FillReport {
account_id: AccountId::from("POLY-001"),
instrument_id: InstrumentId::from("TEST.POLYMARKET"),
venue_order_id,
trade_id: TradeId::from("trade"),
order_side: OrderSide::Buy,
last_qty: Quantity::new(last_qty, 6),
last_px: Price::new(0.55, 2),
commission: Money::new(commission, pusd()),
liquidity_side: LiquiditySide::Taker,
avg_px: None,
report_id: UUID4::new(),
ts_event: UnixNanos::default(),
ts_init: UnixNanos::default(),
client_order_id: None,
venue_position_id: None,
};
let mut reports = vec![
make_report(known_id, 714.285714, 1.234),
make_report(unknown_id, 999.0, 5.678),
];
tracker.snap_fill_reports(&mut reports);
assert_eq!(reports[0].last_qty, Quantity::new(714.285710, 6));
assert_eq!(reports[0].commission, Money::new(1.234, pusd()));
assert_eq!(reports[1].last_qty, Quantity::new(999.0, 6));
assert_eq!(reports[1].commission, Money::new(5.678, pusd()));
}
#[rstest]
fn test_record_fill_accumulates() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(100.0, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
tracker.record_fill(&vid, Quantity::new(50.0, 6));
tracker.record_fill(&vid, Quantity::new(49.997714, 6));
let normalized = tracker.check_terminal_quantity_normalization(&vid);
assert_eq!(normalized, Some(Quantity::new(99.997714, 6)));
}
#[rstest]
fn test_check_dust_no_residual() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(100.0, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
tracker.record_fill(&vid, Quantity::new(100.0, 6));
assert!(
tracker
.check_terminal_quantity_normalization(&vid)
.is_none()
);
}
#[rstest]
fn test_check_dust_significant_residual() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(100.0, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
tracker.record_fill(&vid, Quantity::new(50.0, 6));
assert!(
tracker
.check_terminal_quantity_normalization(&vid)
.is_none()
);
}
#[rstest]
fn test_take_terminal_ioc_remainder_is_exact_and_idempotent() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-partial-ioc");
tracker.register(
vid,
Quantity::from("30.000000"),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
3,
);
tracker.record_fill(&vid, Quantity::from("20.000000"));
let remainder = tracker.take_terminal_ioc_remainder(&vid);
assert_eq!(remainder, Some(Quantity::from("10.000000")));
assert!(!tracker.contains(&vid));
assert!(tracker.take_terminal_ioc_remainder(&vid).is_none());
}
#[rstest]
fn test_take_terminal_ioc_remainder_requires_a_partial_fill() {
let tracker = OrderFillTrackerMap::new();
let unfilled = VenueOrderId::from("order-unfilled-ioc");
let filled = VenueOrderId::from("order-filled-ioc");
for venue_order_id in [unfilled, filled] {
tracker.register(
venue_order_id,
Quantity::from("20.000000"),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
3,
);
}
tracker.record_fill(&filled, Quantity::from("20.000000"));
assert!(tracker.take_terminal_ioc_remainder(&unfilled).is_none());
assert!(tracker.take_terminal_ioc_remainder(&filled).is_none());
assert!(tracker.contains(&unfilled));
assert!(tracker.contains(&filled));
}
#[rstest]
fn test_check_dust_unregistered() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("unknown");
assert!(
tracker
.check_terminal_quantity_normalization(&vid)
.is_none()
);
}
#[rstest]
fn test_dust_settlement_removes_entry() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(100.0, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
tracker.record_fill(&vid, Quantity::new(99.995, 6));
let normalized = tracker.check_terminal_quantity_normalization(&vid);
assert_eq!(normalized, Some(Quantity::new(99.995, 6)));
assert!(!tracker.contains(&vid));
assert!(
tracker
.check_terminal_quantity_normalization(&vid)
.is_none()
);
}
#[rstest]
fn test_get_cumulative_filled_no_fills() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(100.0, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
let filled = tracker.get_cumulative_filled(&vid);
assert_eq!(filled, Some(Quantity::zero(6)));
}
#[rstest]
fn test_get_cumulative_filled_with_fills() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(100.0, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
tracker.record_fill(&vid, Quantity::new(30.0, 6));
tracker.record_fill(&vid, Quantity::new(20.0, 6));
let filled = tracker.get_cumulative_filled(&vid);
assert_eq!(filled, Some(Quantity::new(50.0, 6)));
}
#[rstest]
fn test_get_cumulative_filled_unregistered() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("unknown");
assert!(tracker.get_cumulative_filled(&vid).is_none());
}
#[rstest]
fn test_is_fully_filled_unregistered() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("unknown");
assert!(!tracker.is_fully_filled(&vid));
}
#[rstest]
fn test_is_fully_filled_partial() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(100.0, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
tracker.record_fill(&vid, Quantity::new(50.0, 6));
assert!(!tracker.is_fully_filled(&vid));
}
#[rstest]
fn test_is_fully_filled_complete() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(100.0, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
tracker.record_fill(&vid, Quantity::new(60.0, 6));
tracker.record_fill(&vid, Quantity::new(40.0, 6));
assert!(tracker.is_fully_filled(&vid));
}
fn register_buy(tracker: &OrderFillTrackerMap, vid: VenueOrderId, submitted: f64) {
tracker.register(
vid,
Quantity::new(submitted, 6),
OrderSide::Buy,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
}
#[rstest]
fn test_buy_overfill_bump_unregistered_is_none() {
let tracker = OrderFillTrackerMap::new();
assert!(
tracker
.buy_overfill_bump(&VenueOrderId::from("unknown"))
.is_none()
);
}
#[rstest]
fn test_buy_overfill_bump_within_qty_is_none() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
register_buy(&tracker, vid, 10.0);
tracker.record_fill(&vid, Quantity::new(10.0, 6));
assert!(tracker.buy_overfill_bump(&vid).is_none());
}
#[rstest]
fn test_buy_overfill_bump_sell_is_none() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
tracker.register(
vid,
Quantity::new(10.0, 6),
OrderSide::Sell,
InstrumentId::from("TEST.POLYMARKET"),
6,
2,
);
tracker.record_fill(&vid, Quantity::new(12.0, 6));
assert!(tracker.buy_overfill_bump(&vid).is_none());
}
#[rstest]
fn test_buy_overfill_bump_raises_to_cumulative_and_is_idempotent() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
register_buy(&tracker, vid, 10.0);
tracker.record_fill(&vid, Quantity::new(12.0, 6));
let bumped = tracker.buy_overfill_bump(&vid).expect("expected a raise");
assert_eq!(bumped, Quantity::new(12.0, 6));
assert!(tracker.buy_overfill_bump(&vid).is_none());
assert!(tracker.is_fully_filled(&vid));
}
#[rstest]
fn test_buy_overfill_bump_tracks_each_crossing_fill() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
register_buy(&tracker, vid, 10.0);
tracker.record_fill(&vid, Quantity::new(6.0, 6));
assert!(tracker.buy_overfill_bump(&vid).is_none());
tracker.record_fill(&vid, Quantity::new(8.0, 6));
assert_eq!(
tracker.buy_overfill_bump(&vid),
Some(Quantity::new(14.0, 6))
);
tracker.record_fill(&vid, Quantity::new(6.0, 6));
assert_eq!(
tracker.buy_overfill_bump(&vid),
Some(Quantity::new(20.0, 6))
);
}
#[rstest]
fn test_buy_overfill_bump_ignores_dust_snapped_fill() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("order-1");
register_buy(&tracker, vid, 100.0);
let raw = Quantity::new(100.005, 6);
let snapped = tracker.snap_fill_qty(&vid, raw);
assert_eq!(snapped, Quantity::new(100.0, 6));
tracker.record_fill(&vid, snapped);
assert!(tracker.buy_overfill_bump(&vid).is_none());
}
}