Skip to content

Commit 38205d6

Browse files
committed
Improve execution engine reconciliation robustness
- Add 1-unit precision tolerance for fill/position reconciliation - Skip reconciliation during shutdown to prevent race conditions - Add tests for tolerance and position discrepancy checks
1 parent 9aa0073 commit 38205d6

File tree

4 files changed

+352
-2
lines changed

4 files changed

+352
-2
lines changed

examples/live/okx/okx_spot_swap_quoter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ def on_stop(self) -> None:
408408
trader_id=TraderId("TESTER-001"),
409409
logging=LoggingConfig(
410410
log_level="INFO",
411-
# log_level_file="DEBUG",
411+
log_level_file="DEBUG",
412412
use_pyo3=True,
413413
),
414414
exec_engine=LiveExecEngineConfig(

nautilus_trader/live/execution_engine.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,6 +2011,9 @@ def _reconcile_order_report(
20112011
trades: list[FillReport],
20122012
is_external: bool = True,
20132013
) -> bool:
2014+
if self._is_shutting_down:
2015+
return True # Skip reconciliation during shutdown
2016+
20142017
client_order_id: ClientOrderId = report.client_order_id
20152018

20162019
if client_order_id is None:
@@ -2146,6 +2149,15 @@ def _reconcile_order_report(
21462149
if report.filled_qty > order.filled_qty:
21472150
# Check if order is already closed to avoid duplicate inferred fills
21482151
if order.is_closed:
2152+
# Use the higher precision for tolerance check
2153+
precision = max(report.filled_qty.precision, order.filled_qty.precision)
2154+
if self._is_within_single_unit_tolerance(
2155+
report.filled_qty.as_decimal(),
2156+
order.filled_qty.as_decimal(),
2157+
precision,
2158+
):
2159+
return True
2160+
21492161
self._log.warning( # TODO: Reduce level to debug after initial development phase
21502162
f"{order.instrument_id} {order.client_order_id!r} already {order.status_string()} but "
21512163
f"reported difference in filled_qty: "
@@ -2180,6 +2192,9 @@ def _reconcile_order_report(
21802192
return True # Reconciled
21812193

21822194
def _reconcile_fill_report_single(self, report: FillReport) -> bool:
2195+
if self._is_shutting_down:
2196+
return True # Skip reconciliation during shutdown
2197+
21832198
if not self._consider_for_reconciliation(report.instrument_id):
21842199
self._log_skipping_reconciliation_on_instrument_id(report)
21852200
return True # Filtered
@@ -2379,6 +2394,20 @@ def _fill_reports_equal(self, cached_fill: OrderFilled, report: FillReport) -> b
23792394
and cached_fill.ts_event == report.ts_event
23802395
)
23812396

2397+
def _is_within_single_unit_tolerance(
2398+
self,
2399+
value1: Decimal,
2400+
value2: Decimal,
2401+
precision: int,
2402+
) -> bool:
2403+
# Handles rounding discrepancies from venues (e.g., OKX fillSz vs accFillSz)
2404+
# Only apply tolerance for fractional quantities (precision > 0)
2405+
if precision == 0:
2406+
return value1 == value2 # Integer quantities require exact match
2407+
2408+
tolerance = Decimal(10) ** -precision
2409+
return abs(value1 - value2) <= tolerance
2410+
23822411
def _check_position_discrepancy(
23832412
self,
23842413
cached_positions: list[Position],
@@ -2394,6 +2423,19 @@ def _check_position_discrepancy(
23942423
if venue_report is None:
23952424
# We think we have a position, but venue says flat (or no report)
23962425
if cached_qty != 0:
2426+
instrument = self._cache.instrument(instrument_id)
2427+
if instrument is not None:
2428+
if self._is_within_single_unit_tolerance(
2429+
cached_qty,
2430+
Decimal(0),
2431+
instrument.size_precision,
2432+
):
2433+
return False
2434+
else:
2435+
self._log.debug(
2436+
f"Cannot apply tolerance check for {instrument_id}: instrument not in cache",
2437+
)
2438+
23972439
self._log.warning(
23982440
f"Position discrepancy for {instrument_id}: "
23992441
f"cached_qty={cached_qty}, venue has no position report",
@@ -2409,9 +2451,25 @@ def _check_position_discrepancy(
24092451
if cached_qty == venue_qty:
24102452
return False
24112453

2454+
instrument = self._cache.instrument(instrument_id)
2455+
if instrument is not None:
2456+
if self._is_within_single_unit_tolerance(
2457+
cached_qty,
2458+
venue_qty,
2459+
instrument.size_precision,
2460+
):
2461+
return False
2462+
else:
2463+
self._log.debug(
2464+
f"Cannot apply tolerance check for {instrument_id}: instrument not in cache",
2465+
)
2466+
24122467
return True
24132468

24142469
def _reconcile_position_report(self, report: PositionStatusReport) -> bool:
2470+
if self._is_shutting_down:
2471+
return True # Skip reconciliation during shutdown
2472+
24152473
if not self._consider_for_reconciliation(report.instrument_id):
24162474
self._log_skipping_reconciliation_on_instrument_id(report)
24172475
return True # Filtered

nautilus_trader/test_kit/strategies/tester_exec.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,12 @@ def open_position(self, net_qty: Decimal) -> None:
315315
self.log.warning(f"Open position with {net_qty}, skipping")
316316
return
317317

318+
quantity = self.instrument.make_qty(abs(net_qty))
319+
318320
order: MarketOrder = self.order_factory.market(
319321
instrument_id=self.config.instrument_id,
320322
order_side=OrderSide.BUY if net_qty > 0 else OrderSide.SELL,
321-
quantity=self.instrument.make_qty(self.config.order_qty),
323+
quantity=quantity,
322324
time_in_force=self.config.open_position_time_in_force,
323325
quote_quantity=self.config.use_quote_quantity,
324326
)

0 commit comments

Comments
 (0)