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, InstrumentId, TradeId, VenueOrderId},
reports::FillReport,
types::{Currency, Money, Price, Quantity},
};
use crate::common::consts::{SNAP_OVERFILL_ULPS, SNAP_UNDERFILL_ULPS};
#[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)]
pub(crate) struct OrderFillTrackerMap {
inner: Mutex<FifoCacheMap<VenueOrderId, OrderFillState, 10_000>>,
}
impl OrderFillTrackerMap {
pub fn new() -> Self {
Self {
inner: Mutex::new(FifoCacheMap::default()),
}
}
pub fn register(
&self,
venue_order_id: VenueOrderId,
submitted_qty: Quantity,
order_side: OrderSide,
instrument_id: InstrumentId,
size_precision: u8,
price_precision: u8,
) {
let state = 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,
};
self.inner
.lock()
.expect(MUTEX_POISONED)
.insert(venue_order_id, state);
}
pub fn contains(&self, venue_order_id: &VenueOrderId) -> bool {
self.inner
.lock()
.expect(MUTEX_POISONED)
.get(venue_order_id)
.is_some()
}
pub fn has_fills_or_settled(&self, venue_order_id: &VenueOrderId) -> bool {
let guard = self.inner.lock().expect(MUTEX_POISONED);
match guard.get(venue_order_id) {
Some(s) => s.cumulative_filled > 0.0,
None => true, }
}
pub fn get_cumulative_filled(&self, venue_order_id: &VenueOrderId) -> Option<f64> {
self.inner
.lock()
.expect(MUTEX_POISONED)
.get(venue_order_id)
.map(|s| s.cumulative_filled)
}
pub fn is_fully_filled(&self, venue_order_id: &VenueOrderId) -> bool {
self.inner
.lock()
.expect(MUTEX_POISONED)
.get(venue_order_id)
.is_some_and(|s| {
let leaves = s.submitted_qty.as_f64() - s.cumulative_filled;
leaves < 1e-9
})
}
pub fn record_fill(&self, venue_order_id: &VenueOrderId, qty: f64, px: f64, ts: UnixNanos) {
if let Some(s) = self
.inner
.lock()
.expect(MUTEX_POISONED)
.get_mut(venue_order_id)
{
s.cumulative_filled += qty;
s.last_fill_px = px;
s.last_fill_ts = ts;
}
}
pub fn snap_fill_qty(&self, venue_order_id: &VenueOrderId, fill_qty: Quantity) -> Quantity {
let guard = self.inner.lock().expect(MUTEX_POISONED);
match guard.get(venue_order_id) {
Some(s) => {
let diff = s.submitted_qty.as_f64() - fill_qty.as_f64();
let ulp = 10f64.powi(-(s.size_precision as i32));
let tolerance = if diff > 0.0 {
SNAP_UNDERFILL_ULPS * ulp
} else if diff < 0.0 {
SNAP_OVERFILL_ULPS * ulp
} else {
return fill_qty;
};
if diff.abs() < tolerance {
log::info!(
"Snapping fill qty {fill_qty} -> {} (dust={diff:.6})",
s.submitted_qty,
);
s.submitted_qty
} else {
fill_qty
}
}
None => fill_qty,
}
}
#[expect(clippy::too_many_arguments)]
pub 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.get(venue_order_id)?;
let leaves = s.submitted_qty.as_f64() - s.cumulative_filled;
let ulp = 10f64.powi(-(s.size_precision as i32));
let underfill_tolerance = SNAP_UNDERFILL_ULPS * ulp;
if leaves > 0.0 && leaves < underfill_tolerance {
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.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::new(0.0, 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 >= underfill_tolerance {
log::info!(
"Order {venue_order_id} MATCHED with significant residual \
{leaves:.6} (filled {}/{})",
s.cumulative_filled,
s.submitted_qty,
);
}
None
}
}
}
#[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(23.696681, 23.690000, 23.696681)]
#[case::underfill_near_tolerance(100.000000, 99.990100, 100.000000)]
#[case::underfill_at_tolerance(100.000000, 99.990000, 99.990000)]
#[case::underfill_above_tolerance(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_tolerance(100.000000, 100.000099, 100.000000)]
#[case::overfill_at_tolerance(100.000000, 100.000100, 100.000100)]
#[case::overfill_above_tolerance(100.000000, 100.005000, 100.005000)]
#[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_scaled_within(100.000, 95.000, 100.000)]
#[case::underfill_scaled_above(100.000, 85.000, 85.000)]
#[case::overfill_scaled_within(100.000, 100.050, 100.000)]
#[case::overfill_scaled_above(100.000, 100.200, 100.200)]
fn test_snap_fill_qty_scales_with_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_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));
}
}