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::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)]
pub(crate) struct OrderFillTrackerMap {
inner: Mutex<FifoCacheMap<VenueOrderId, OrderFillState, 10_000>>,
}
impl OrderFillTrackerMap {
pub(crate) fn new() -> Self {
Self {
inner: Mutex::new(FifoCacheMap::default()),
}
}
pub(crate) 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(crate) fn contains(&self, venue_order_id: &VenueOrderId) -> bool {
self.inner
.lock()
.expect(MUTEX_POISONED)
.get(venue_order_id)
.is_some()
}
pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) fn snap_fill_reports(&self, reports: &mut [FillReport]) {
for report in reports {
report.last_qty = self.snap_fill_qty(&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);
match guard.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,
}
}
#[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.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.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 >= DUST_SNAP_THRESHOLD {
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_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));
}
}