|
40 | 40 | from nautilus_trader.model.enums import TriggerType
|
41 | 41 | from nautilus_trader.model.identifiers import ClientId
|
42 | 42 | from nautilus_trader.model.identifiers import ClientOrderId
|
| 43 | +from nautilus_trader.model.identifiers import PositionId |
43 | 44 | from nautilus_trader.model.identifiers import StrategyId
|
44 | 45 | from nautilus_trader.model.identifiers import TradeId
|
45 | 46 | from nautilus_trader.model.identifiers import Venue
|
@@ -543,3 +544,113 @@ async def test_reconcile_state_no_cached_with_partially_filled_order_and_cancele
|
543 | 544 | assert order.last_trade_id == TradeId("1")
|
544 | 545 | assert order.quantity == Quantity.from_int(10_000)
|
545 | 546 | assert order.filled_qty == Quantity.from_int(5_000)
|
| 547 | + |
| 548 | + @pytest.mark.asyncio() |
| 549 | + async def test_reconcile_state_with_cached_order_and_different_fill_data(self): |
| 550 | + # Arrange: Create a cached order with a fill |
| 551 | + venue_order_id = VenueOrderId("1") |
| 552 | + client_order_id = ClientOrderId("O-123456") |
| 553 | + |
| 554 | + # Create and cache an order with an initial fill |
| 555 | + order = self.order_factory.market( |
| 556 | + instrument_id=AUDUSD_SIM.id, |
| 557 | + order_side=OrderSide.BUY, |
| 558 | + quantity=Quantity.from_int(10_000), |
| 559 | + client_order_id=client_order_id, |
| 560 | + ) |
| 561 | + |
| 562 | + # Submit and accept the order |
| 563 | + submitted = TestEventStubs.order_submitted(order, account_id=self.account_id) |
| 564 | + order.apply(submitted) |
| 565 | + self.cache.add_order(order, position_id=None) |
| 566 | + |
| 567 | + accepted = TestEventStubs.order_accepted( |
| 568 | + order, |
| 569 | + account_id=self.account_id, |
| 570 | + venue_order_id=venue_order_id, |
| 571 | + ) |
| 572 | + order.apply(accepted) |
| 573 | + self.cache.update_order(order) |
| 574 | + |
| 575 | + # Apply an initial fill with specific data |
| 576 | + initial_fill = TestEventStubs.order_filled( |
| 577 | + order, |
| 578 | + instrument=AUDUSD_SIM, |
| 579 | + position_id=PositionId("P-1"), |
| 580 | + strategy_id=StrategyId("S-1"), |
| 581 | + last_qty=Quantity.from_int(5_000), |
| 582 | + last_px=Price.from_str("1.00000"), |
| 583 | + liquidity_side=LiquiditySide.MAKER, |
| 584 | + trade_id=TradeId("TRADE-1"), |
| 585 | + ) |
| 586 | + order.apply(initial_fill) |
| 587 | + self.cache.update_order(order) |
| 588 | + |
| 589 | + # Now create a broker report with different fill data for the same trade_id |
| 590 | + order_report = OrderStatusReport( |
| 591 | + account_id=self.account_id, |
| 592 | + instrument_id=AUDUSD_SIM.id, |
| 593 | + client_order_id=client_order_id, |
| 594 | + venue_order_id=venue_order_id, |
| 595 | + order_side=OrderSide.BUY, |
| 596 | + order_type=OrderType.MARKET, |
| 597 | + time_in_force=TimeInForce.GTC, |
| 598 | + order_status=OrderStatus.PARTIALLY_FILLED, |
| 599 | + quantity=Quantity.from_int(10_000), |
| 600 | + filled_qty=Quantity.from_int(5_000), |
| 601 | + avg_px=Decimal("1.00000"), |
| 602 | + report_id=UUID4(), |
| 603 | + ts_accepted=0, |
| 604 | + ts_triggered=0, |
| 605 | + ts_last=0, |
| 606 | + ts_init=0, |
| 607 | + ) |
| 608 | + |
| 609 | + # Fill report with DIFFERENT data than cached (different price, commission, liquidity) |
| 610 | + fill_report = FillReport( |
| 611 | + account_id=self.account_id, |
| 612 | + instrument_id=AUDUSD_SIM.id, |
| 613 | + client_order_id=client_order_id, |
| 614 | + venue_order_id=venue_order_id, |
| 615 | + venue_position_id=None, |
| 616 | + trade_id=TradeId("TRADE-1"), # Same trade_id as cached fill |
| 617 | + order_side=OrderSide.BUY, |
| 618 | + last_qty=Quantity.from_int(5_000), |
| 619 | + last_px=Price.from_str("1.00100"), # Different price |
| 620 | + commission=Money(10.0, USD), # Different commission |
| 621 | + liquidity_side=LiquiditySide.TAKER, # Different liquidity side |
| 622 | + report_id=UUID4(), |
| 623 | + ts_event=1000, # Different timestamp |
| 624 | + ts_init=0, |
| 625 | + ) |
| 626 | + |
| 627 | + self.client.add_order_status_report(order_report) |
| 628 | + self.client.add_fill_reports(venue_order_id, [fill_report]) |
| 629 | + |
| 630 | + # Act |
| 631 | + result = await self.exec_engine.reconcile_state() |
| 632 | + |
| 633 | + # Assert: Reconciliation should succeed despite different fill data |
| 634 | + assert result |
| 635 | + |
| 636 | + # The order should still exist and maintain its cached state |
| 637 | + cached_order = self.cache.order(client_order_id) |
| 638 | + assert cached_order is not None |
| 639 | + assert cached_order.status == OrderStatus.PARTIALLY_FILLED |
| 640 | + assert cached_order.filled_qty == Quantity.from_int(5_000) |
| 641 | + |
| 642 | + # The cached fill data should remain unchanged (not updated with broker data) |
| 643 | + # This ensures we don't corrupt the order state |
| 644 | + fill_events = [ |
| 645 | + event |
| 646 | + for event in cached_order.events |
| 647 | + if hasattr(event, "trade_id") and event.trade_id == TradeId("TRADE-1") |
| 648 | + ] |
| 649 | + assert len(fill_events) == 1 |
| 650 | + cached_fill_event = fill_events[0] |
| 651 | + |
| 652 | + # Verify the cached data is preserved (original values, not broker values) |
| 653 | + assert cached_fill_event.last_px == Price.from_str("1.00000") # Original price |
| 654 | + # Note: commission is calculated automatically by TestEventStubs, so we just check it exists |
| 655 | + assert cached_fill_event.commission is not None |
| 656 | + assert cached_fill_event.liquidity_side == LiquiditySide.MAKER # Original liquidity |
0 commit comments