Skip to content

Commit c9eafd8

Browse files
committed
Fix position activity timestamps in live execution engine
1 parent de61cd0 commit c9eafd8

3 files changed

Lines changed: 79 additions & 4 deletions

File tree

RELEASES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,21 @@ releases as feedback arrives, before the final `2.0.0` release.
4747
- Fixed v2 matching engine queue position for per-order deltas in L3 books (#4370), thanks for reporting @warmi024
4848
- Fixed v2 own order book sizes to track remaining quantity after partial fills
4949
- Fixed v2 interval book snapshots blocking order submission from `on_book` handlers
50+
- Fixed v2 position reconciliation grace to measure on the monotonic clock (#4366), thanks @folknor
51+
- Fixed live execution engine position activity to stamp receipt time instead of venue `ts_event`
5052
- Fixed Redis message bus startup with Python v2 configs (#4356), thanks for reporting @davidgreyme
5153
- Fixed Binance Futures order reports omitting external limit order prices (#4346), thanks for reporting @linimin
5254
- Fixed Binance Futures external algo order materialization (#4348), thanks for reporting @linimin
5355
- Fixed Derive perpetual quote and settlement currency to USDC (venue reports quote as `USD`)
5456
- Fixed Derive option `scheduled_activation` parsing as UNIX seconds (was parsed as milliseconds)
5557
- Fixed Polymarket RTDS retained-subscription recovery after reconnects (#4353), thanks @graceyangfan
5658
- Fixed Tardis replay trades directory to `trades/` for catalog compatibility (#4373), thanks @AdvancedUno
59+
- Fixed Tardis replay bars directory to `bars/` for catalog compatibility (#4378), thanks @AdvancedUno
5760
- Fixed Hyperliquid `l2Book` resubscribe options and shared stream teardown (#4298)
5861

5962
### Internal Improvements
6063
- Improved portfolio statistics test coverage with canonical worked examples
64+
- Made portfolio reference-count clones explicit (#4364), thanks @ChrisAB
6165
- Upgraded Rust (MSRV) to 1.96.1
6266
- Upgraded Cython to v3.2.8
6367
- Upgraded `redis` crate to v1.3.0

nautilus_trader/live/execution_engine.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3724,10 +3724,12 @@ def _handle_event_with_tracking(self, event: OrderEvent) -> None:
37243724
self._record_local_activity(event)
37253725

37263726
if isinstance(event, OrderFilled):
3727-
self._recent_fills_cache[event.trade_id] = self._clock.timestamp_ns()
3728-
self._position_local_activity_ns[(event.instrument_id, event.account_id)] = (
3729-
event.ts_event
3730-
)
3727+
ts_now = self._clock.timestamp_ns()
3728+
self._recent_fills_cache[event.trade_id] = ts_now
3729+
3730+
# Stamp receipt time: a venue ts_event ahead of this clock would make
3731+
# the grace delta negative and suppress the position check.
3732+
self._position_local_activity_ns[(event.instrument_id, event.account_id)] = ts_now
37313733

37323734
# Track inferred fill timestamps to prevent duplicate historical fills
37333735
if event.reconciliation:

tests/unit_tests/live/test_execution_recon.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4634,6 +4634,75 @@ async def counting_query(instrument_id, clients):
46344634
assert len(queries) == 1
46354635

46364636

4637+
@pytest.mark.asyncio
4638+
async def test_position_activity_stamped_from_receipt_time_for_venue_ahead_fill(
4639+
live_exec_engine,
4640+
exec_client,
4641+
cache,
4642+
):
4643+
# Arrange
4644+
live_exec_engine.register_client(exec_client)
4645+
live_exec_engine.generate_missing_orders = False
4646+
live_exec_engine._position_check_threshold_ns = 0
4647+
4648+
account = AccountId("SIM-A")
4649+
order = TestExecStubs.limit_order(instrument=AUDUSD_SIM, order_side=OrderSide.BUY)
4650+
order.apply(TestEventStubs.order_submitted(order, account_id=account))
4651+
order.apply(TestEventStubs.order_accepted(order, account_id=account))
4652+
cache.add_order(order, position_id=None)
4653+
4654+
venue_ahead_ns = live_exec_engine._clock.timestamp_ns() + 900 * 86_400 * 1_000_000_000
4655+
fill = TestEventStubs.order_filled(
4656+
order,
4657+
instrument=AUDUSD_SIM,
4658+
account_id=account,
4659+
trade_id=TradeId("T-AXIS-1"),
4660+
last_qty=Quantity.from_int(1000),
4661+
last_px=Price.from_str("1.00000"),
4662+
ts_event=venue_ahead_ns,
4663+
)
4664+
4665+
# Act
4666+
live_exec_engine._handle_event_with_tracking(fill)
4667+
4668+
# Assert - stamp is on the local clock axis
4669+
stamp = live_exec_engine._position_local_activity_ns[(AUDUSD_SIM.id, account)]
4670+
assert stamp <= live_exec_engine._clock.timestamp_ns()
4671+
assert stamp != venue_ahead_ns
4672+
4673+
# A cached position with no venue report: a genuine discrepancy
4674+
order_b = TestExecStubs.limit_order(instrument=AUDUSD_SIM, order_side=OrderSide.BUY)
4675+
fill_b = TestEventStubs.order_filled(
4676+
order_b,
4677+
instrument=AUDUSD_SIM,
4678+
account_id=account,
4679+
trade_id=TradeId("T-AXIS-2"),
4680+
last_qty=Quantity.from_int(1000),
4681+
last_px=Price.from_str("1.00000"),
4682+
position_id=PositionId("P-AXIS-B"),
4683+
)
4684+
position = Position(instrument=AUDUSD_SIM, fill=fill_b)
4685+
cache.add_position(position, OmsType.HEDGING)
4686+
4687+
queries = []
4688+
4689+
async def counting_query(instrument_id, clients):
4690+
queries.append(instrument_id)
4691+
return [], False
4692+
4693+
live_exec_engine._query_and_find_missing_fills = counting_query
4694+
4695+
# Act - zero threshold: a receipt-time stamp is already expired, a venue-axis
4696+
# stamp would make the delta negative and suppress forever
4697+
await live_exec_engine._process_cached_position_discrepancies(
4698+
{(AUDUSD_SIM.id, account): [position]},
4699+
{},
4700+
)
4701+
4702+
# Assert - the grace does not suppress the discrepancy query
4703+
assert len(queries) == 1
4704+
4705+
46374706
@pytest.mark.asyncio
46384707
async def test_check_positions_consistency_processes_only_discrepant_account(
46394708
live_exec_engine,

0 commit comments

Comments
 (0)