use std::sync::Mutex;
use nautilus_common::cache::fifo::FifoCacheMap;
use nautilus_core::{MUTEX_POISONED, UUID4, UnixNanos};
use nautilus_model::{
enums::{LiquiditySide, OrderSide},
identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
reports::{FillReport, OrderStatusReport},
types::{Currency, Money, Price, Quantity},
};
use crate::common::consts::DUST_SNAP_THRESHOLD;
#[derive(Debug, Clone, Copy)]
struct OrderFillState {
submitted_qty: Quantity,
cumulative_filled: f64,
last_fill_px: f64,
last_fill_ts: UnixNanos,
order_side: OrderSide,
instrument_id: InstrumentId,
size_precision: u8,
price_precision: u8,
}
#[derive(Debug, Default)]
struct TrackerInner {
orders: FifoCacheMap<VenueOrderId, OrderFillState, 10_000>,
pending_fills: FifoCacheMap<VenueOrderId, Vec<FillReport>, 1_000>,
pending_reports: FifoCacheMap<VenueOrderId, Vec<OrderStatusReport>, 1_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 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 > 0.0,
None => true, }
}
pub(crate) fn get_cumulative_filled(&self, venue_order_id: &VenueOrderId) -> Option<f64> {
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| {
let leaves = s.submitted_qty.as_f64() - s.cumulative_filled;
leaves < 1e-9
})
}
pub(crate) fn accept_or_buffer_fill(
&self,
venue_order_id: VenueOrderId,
report: FillReport,
) -> 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.as_f64(),
report.last_px.as_f64(),
report.ts_event,
);
Some(report)
} else {
push_buffered(&mut guard.pending_fills, venue_order_id, report);
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
}
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn register_and_take_pending_fills(
&self,
venue_order_id: VenueOrderId,
client_order_id: Option<ClientOrderId>,
submitted_qty: Quantity,
order_side: OrderSide,
instrument_id: InstrumentId,
size_precision: u8,
price_precision: u8,
) -> Vec<FillReport> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
guard.orders.insert(
venue_order_id,
new_order_state(
submitted_qty,
order_side,
instrument_id,
size_precision,
price_precision,
),
);
take_and_prepare_fills(&mut guard, venue_order_id, client_order_id)
}
#[expect(clippy::too_many_arguments)]
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,
instrument_id: InstrumentId,
size_precision: u8,
price_precision: u8,
) -> Option<Vec<FillReport>> {
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,
instrument_id,
size_precision,
price_precision,
),
);
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<FillReport> {
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 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);
let state = guard.orders.get_mut(venue_order_id)?;
if state.order_side != OrderSide::Buy {
return None;
}
let filled = Quantity::new(state.cumulative_filled, state.size_precision);
if filled > state.submitted_qty {
state.submitted_qty = filled;
Some(filled)
} else {
None
}
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn check_dust_and_build_fill(
&self,
venue_order_id: &VenueOrderId,
account_id: AccountId,
order_id: &str,
fallback_px: f64,
currency: Currency,
ts_event: UnixNanos,
ts_init: UnixNanos,
) -> Option<FillReport> {
let mut guard = self.inner.lock().expect(MUTEX_POISONED);
let s = guard.orders.get(venue_order_id)?;
let leaves = s.submitted_qty.as_f64() - s.cumulative_filled;
if leaves > 0.0 && leaves < DUST_SNAP_THRESHOLD {
let size_precision = s.size_precision;
let price_precision = s.price_precision;
let last_fill_px = s.last_fill_px;
let order_side = s.order_side;
let instrument_id = s.instrument_id;
log::info!(
"Order {venue_order_id} MATCHED with dust residual {leaves:.6} -- \
emitting synthetic fill to reach FILLED"
);
let dust_qty = Quantity::new(leaves, size_precision);
let px = if last_fill_px > 0.0 {
last_fill_px
} else {
fallback_px
};
let fill_px = Price::new(px, price_precision);
let trade_id = TradeId::from(format!("{order_id:.27}-dust").as_str());
guard.orders.remove(venue_order_id);
Some(FillReport {
account_id,
instrument_id,
venue_order_id: *venue_order_id,
trade_id,
order_side,
last_qty: dust_qty,
last_px: fill_px,
commission: Money::zero(currency),
liquidity_side: LiquiditySide::NoLiquiditySide,
avg_px: None,
report_id: UUID4::new(),
ts_event,
ts_init,
client_order_id: None,
venue_position_id: None,
})
} else {
if leaves >= DUST_SNAP_THRESHOLD {
log::info!(
"Order {venue_order_id} MATCHED with significant residual \
{leaves:.6} (filled {}/{})",
s.cumulative_filled,
s.submitted_qty,
);
}
None
}
}
}
fn new_order_state(
submitted_qty: Quantity,
order_side: OrderSide,
instrument_id: InstrumentId,
size_precision: u8,
price_precision: u8,
) -> OrderFillState {
OrderFillState {
submitted_qty,
cumulative_filled: 0.0,
last_fill_px: 0.0,
last_fill_ts: UnixNanos::default(),
order_side,
instrument_id,
size_precision,
price_precision,
}
}
fn take_and_prepare_fills(
inner: &mut TrackerInner,
venue_order_id: VenueOrderId,
client_order_id: Option<ClientOrderId>,
) -> Vec<FillReport> {
let Some(buffered) = inner.pending_fills.remove(&venue_order_id) else {
return Vec::new();
};
buffered
.into_iter()
.map(|mut fill| {
fill.client_order_id = client_order_id;
fill.last_qty = snap_fill_qty_in(&inner.orders, &venue_order_id, fill.last_qty);
record_fill_in(
&mut inner.orders,
&venue_order_id,
fill.last_qty.as_f64(),
fill.last_px.as_f64(),
fill.ts_event,
);
fill
})
.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,
instrument_id,
size_precision,
price_precision,
),
);
}
pub(crate) fn record_fill(
&self,
venue_order_id: &VenueOrderId,
qty: f64,
px: f64,
ts: UnixNanos,
) {
record_fill_in(
&mut self.inner.lock().expect(MUTEX_POISONED).orders,
venue_order_id,
qty,
px,
ts,
);
}
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,
report,
);
}
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)
.cloned()
.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: f64,
px: f64,
ts: UnixNanos,
) {
if let Some(s) = orders.get_mut(venue_order_id) {
s.cumulative_filled += qty;
s.last_fill_px = px;
s.last_fill_ts = ts;
}
}
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_f64() - fill_qty.as_f64();
if diff < 0.0 && diff.abs() < DUST_SNAP_THRESHOLD {
log::info!(
"Snapping overfill {fill_qty} -> {} (dust={diff:+.6})",
s.submitted_qty,
);
s.submitted_qty
} else {
fill_qty
}
}
None => fill_qty,
}
}
#[cfg(test)]
mod tests {
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]
#[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, 50.0, 0.55, UnixNanos::from(1_000u64));
tracker.record_fill(&vid, 49.997714, 0.55, UnixNanos::from(2_000u64));
let dust_fill = tracker.check_dust_and_build_fill(
&vid,
AccountId::from("POLY-001"),
"order-1",
0.55,
pusd(),
UnixNanos::from(3_000u64),
UnixNanos::from(4_000u64),
);
assert!(dust_fill.is_some());
let fill = dust_fill.unwrap();
assert!((fill.last_qty.as_f64() - 0.002286).abs() < 1e-9);
assert_eq!(fill.order_side, OrderSide::Buy);
assert_eq!(fill.liquidity_side, LiquiditySide::NoLiquiditySide);
assert_eq!(fill.ts_event, UnixNanos::from(3_000u64));
assert_eq!(fill.ts_init, UnixNanos::from(4_000u64));
}
#[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, 100.0, 0.55, UnixNanos::from(1_000u64));
let dust_fill = tracker.check_dust_and_build_fill(
&vid,
AccountId::from("POLY-001"),
"order-1",
0.55,
pusd(),
UnixNanos::from(2_000u64),
UnixNanos::from(2_000u64),
);
assert!(dust_fill.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, 50.0, 0.55, UnixNanos::from(1_000u64));
let dust_fill = tracker.check_dust_and_build_fill(
&vid,
AccountId::from("POLY-001"),
"order-1",
0.55,
pusd(),
UnixNanos::from(2_000u64),
UnixNanos::from(2_000u64),
);
assert!(dust_fill.is_none());
}
#[rstest]
fn test_check_dust_unregistered() {
let tracker = OrderFillTrackerMap::new();
let vid = VenueOrderId::from("unknown");
let dust_fill = tracker.check_dust_and_build_fill(
&vid,
AccountId::from("POLY-001"),
"unknown",
0.55,
pusd(),
UnixNanos::from(1_000u64),
UnixNanos::from(1_000u64),
);
assert!(dust_fill.is_none());
}
#[rstest]
fn test_dust_fill_uses_last_fill_price() {
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, 99.995, 0.60, UnixNanos::from(1_000u64));
let dust_fill = tracker
.check_dust_and_build_fill(
&vid,
AccountId::from("POLY-001"),
"order-1",
0.50, pusd(),
UnixNanos::from(2_000u64),
UnixNanos::from(2_000u64),
)
.unwrap();
assert_eq!(dust_fill.last_px, Price::new(0.60, 2));
}
#[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, 99.995, 0.55, UnixNanos::from(1_000u64));
let dust_fill = tracker.check_dust_and_build_fill(
&vid,
AccountId::from("POLY-001"),
"order-1",
0.55,
pusd(),
UnixNanos::from(2_000u64),
UnixNanos::from(2_000u64),
);
assert!(dust_fill.is_some());
assert!(!tracker.contains(&vid));
let dust_fill2 = tracker.check_dust_and_build_fill(
&vid,
AccountId::from("POLY-001"),
"order-1",
0.55,
pusd(),
UnixNanos::from(3_000u64),
UnixNanos::from(3_000u64),
);
assert!(dust_fill2.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(0.0));
}
#[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, 30.0, 0.5, UnixNanos::from(1_000u64));
tracker.record_fill(&vid, 20.0, 0.5, UnixNanos::from(2_000u64));
let filled = tracker.get_cumulative_filled(&vid);
assert_eq!(filled, Some(50.0));
}
#[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, 50.0, 0.5, UnixNanos::from(1_000u64));
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, 60.0, 0.5, UnixNanos::from(1_000u64));
tracker.record_fill(&vid, 40.0, 0.5, UnixNanos::from(2_000u64));
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, 10.0, 0.5, UnixNanos::from(1_000u64));
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, 12.0, 0.5, UnixNanos::from(1_000u64));
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, 12.0, 0.5, UnixNanos::from(1_000u64));
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, 6.0, 0.5, UnixNanos::from(1_000u64));
assert!(tracker.buy_overfill_bump(&vid).is_none());
tracker.record_fill(&vid, 8.0, 0.5, UnixNanos::from(2_000u64));
assert_eq!(
tracker.buy_overfill_bump(&vid),
Some(Quantity::new(14.0, 6))
);
tracker.record_fill(&vid, 6.0, 0.5, UnixNanos::from(3_000u64));
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.as_f64(), 0.5, UnixNanos::from(1_000u64));
assert!(tracker.buy_overfill_bump(&vid).is_none());
}
}