@@ -1157,56 +1157,15 @@ impl LiveNode {
11571157 residual_events += 1 ;
11581158 }
11591159
1160- let mut close_ids: Vec <ClientOrderId > = Vec :: new( ) ;
1161-
1162- match & evt {
1163- ExecutionEvent :: Order ( order_evt) => {
1164- self . exec_manager. observe_order_event( order_evt) ;
1165- close_ids. push( order_evt. client_order_id( ) ) ;
1166- }
1167- ExecutionEvent :: OrderSubmittedBatch ( batch) => {
1168- for submitted in & batch. events {
1169- self . exec_manager. record_local_activity( submitted. client_order_id) ;
1170- }
1171- }
1172- ExecutionEvent :: OrderAcceptedBatch ( batch) => {
1173- for accepted in & batch. events {
1174- // Stamp after clearing: `clear_recon_tracking` drops the
1175- // local-activity mark, the missing-order grace gate.
1176- self . exec_manager. clear_recon_tracking(
1177- & accepted. client_order_id, true ,
1178- ) ;
1179- self . exec_manager. record_local_activity( accepted. client_order_id) ;
1180- }
1181- }
1182- ExecutionEvent :: OrderCanceledBatch ( batch) => {
1183- for canceled in & batch. events {
1184- self . exec_manager. clear_recon_tracking(
1185- & canceled. client_order_id, true ,
1186- ) ;
1187- self . exec_manager. record_local_activity( canceled. client_order_id) ;
1188- close_ids. push( canceled. client_order_id) ;
1189- }
1190- }
1191- ExecutionEvent :: Report ( report) => {
1192- if let ExecutionReport :: Fill ( fill_report) = report
1193- && self . exec_manager. is_fill_recently_processed( & fill_report. trade_id) {
1194- log:: debug!(
1195- "Skipping recently processed fill report: {}" ,
1196- fill_report. trade_id,
1197- ) ;
1198- record_runner_dispatch(
1199- & metrics,
1200- RunnerMetricChannel :: ExecEvents ,
1201- dispatch_start,
1202- metrics_start,
1203- ) ;
1204- continue ;
1205- }
1206- self . exec_manager. observe_execution_report( report) ;
1207- }
1208- ExecutionEvent :: Account ( _) => { }
1209- }
1160+ let Some ( close_ids) = self . observe_exec_event_before_dispatch( & evt) else {
1161+ record_runner_dispatch(
1162+ & metrics,
1163+ RunnerMetricChannel :: ExecEvents ,
1164+ dispatch_start,
1165+ metrics_start,
1166+ ) ;
1167+ continue ;
1168+ } ;
12101169
12111170 AsyncRunner :: handle_exec_event( evt) ;
12121171
@@ -1504,6 +1463,60 @@ impl LiveNode {
15041463 }
15051464 }
15061465
1466+ fn observe_exec_event_before_dispatch (
1467+ & mut self ,
1468+ evt : & ExecutionEvent ,
1469+ ) -> Option < Vec < ClientOrderId > > {
1470+ let mut close_ids = Vec :: new ( ) ;
1471+
1472+ match evt {
1473+ ExecutionEvent :: Order ( order_evt) => {
1474+ self . exec_manager . observe_order_event ( order_evt) ;
1475+ close_ids. push ( order_evt. client_order_id ( ) ) ;
1476+ }
1477+ ExecutionEvent :: OrderSubmittedBatch ( batch) => {
1478+ for submitted in & batch. events {
1479+ self . exec_manager
1480+ . record_local_activity ( submitted. client_order_id ) ;
1481+ }
1482+ }
1483+ ExecutionEvent :: OrderAcceptedBatch ( batch) => {
1484+ for accepted in & batch. events {
1485+ self . exec_manager
1486+ . clear_recon_tracking ( & accepted. client_order_id , true ) ;
1487+ self . exec_manager
1488+ . record_local_activity ( accepted. client_order_id ) ;
1489+ }
1490+ }
1491+ ExecutionEvent :: OrderCanceledBatch ( batch) => {
1492+ for canceled in & batch. events {
1493+ self . exec_manager
1494+ . clear_recon_tracking ( & canceled. client_order_id , true ) ;
1495+ self . exec_manager
1496+ . record_local_activity ( canceled. client_order_id ) ;
1497+ close_ids. push ( canceled. client_order_id ) ;
1498+ }
1499+ }
1500+ ExecutionEvent :: Report ( report) => {
1501+ if let ExecutionReport :: Fill ( fill_report) = report
1502+ && self
1503+ . exec_manager
1504+ . is_fill_recently_processed ( & fill_report. trade_id )
1505+ {
1506+ log:: debug!(
1507+ "Skipping recently processed fill report: {}" ,
1508+ fill_report. trade_id,
1509+ ) ;
1510+ return None ;
1511+ }
1512+ self . exec_manager . observe_execution_report ( report) ;
1513+ }
1514+ ExecutionEvent :: Account ( _) => { }
1515+ }
1516+
1517+ Some ( close_ids)
1518+ }
1519+
15071520 /// Gets the node's environment.
15081521 #[ must_use]
15091522 pub fn environment ( & self ) -> Environment {
@@ -2305,7 +2318,7 @@ mod tests {
23052318 use nautilus_common:: {
23062319 actor:: DataActor ,
23072320 cache:: Cache ,
2308- clock:: Clock ,
2321+ clock:: { Clock , TestClock } ,
23092322 enums:: SerializationEncoding ,
23102323 messages:: execution:: { SubmitOrder , TradingCommand } ,
23112324 msgbus:: {
@@ -2321,8 +2334,10 @@ mod tests {
23212334 use nautilus_model:: {
23222335 data:: QuoteTick ,
23232336 enums:: { OmsType , OrderStatus , OrderType } ,
2337+ events:: { OrderAcceptedBatch , order:: spec:: OrderAcceptedSpec } ,
23242338 identifiers:: {
2325- AccountId , ClientId , InstrumentId , PositionId , StrategyId , TraderId , VenueOrderId ,
2339+ AccountId , ClientId , InstrumentId , PositionId , StrategyId , TradeId , TraderId ,
2340+ VenueOrderId ,
23262341 } ,
23272342 instruments:: { Instrument , InstrumentAny , stubs:: crypto_perpetual_ethusdt} ,
23282343 orders:: { OrderTestBuilder , stubs:: TestOrderEventStubs } ,
@@ -2363,6 +2378,100 @@ mod tests {
23632378 assert_eq ! ( output, expected) ;
23642379 }
23652380
2381+ #[ rstest]
2382+ fn test_observe_exec_event_before_dispatch_skips_recent_fill_report ( ) {
2383+ let config = LiveNodeConfig {
2384+ exec_engine : crate :: config:: LiveExecEngineConfig {
2385+ reconciliation : false ,
2386+ ..Default :: default ( )
2387+ } ,
2388+ ..Default :: default ( )
2389+ } ;
2390+ let mut node = LiveNode :: build ( "FillSkipNode" . to_string ( ) , Some ( config) ) . unwrap ( ) ;
2391+ let event = stub_exec_event ( ) ;
2392+ let trade_id = TradeId :: from ( "T-001" ) ;
2393+
2394+ let close_ids = node. observe_exec_event_before_dispatch ( & event) ;
2395+ assert_eq ! ( close_ids, Some ( Vec :: new( ) ) ) ;
2396+ assert ! ( !node. exec_manager. is_fill_recently_processed( & trade_id) ) ;
2397+
2398+ node. exec_manager . mark_fill_processed ( trade_id) ;
2399+
2400+ let close_ids = node. observe_exec_event_before_dispatch ( & event) ;
2401+ assert_eq ! ( close_ids, None ) ;
2402+ }
2403+
2404+ #[ rstest]
2405+ fn test_observe_exec_event_before_dispatch_accepted_batch_stamps_local_activity ( ) {
2406+ let config = LiveNodeConfig {
2407+ exec_engine : crate :: config:: LiveExecEngineConfig {
2408+ reconciliation : true ,
2409+ open_check_threshold_ms : 5_000 ,
2410+ single_order_query_delay_ms : 0 ,
2411+ ..Default :: default ( )
2412+ } ,
2413+ ..Default :: default ( )
2414+ } ;
2415+ let mut node = LiveNode :: build ( "AcceptedBatchNode" . to_string ( ) , Some ( config) ) . unwrap ( ) ;
2416+ let account_id = AccountId :: from ( "TEST-ACCEPTED-BATCH-001" ) ;
2417+ let client_id = ClientId :: from ( "TEST-ACCEPTED-BATCH" ) ;
2418+ let instrument = crypto_perpetual_ethusdt ( ) ;
2419+ let instrument_id = instrument. id ( ) ;
2420+ let client_order_id = ClientOrderId :: from ( "O-ACCEPTED-BATCH" ) ;
2421+ let venue_order_id = VenueOrderId :: from ( "V-ACCEPTED-BATCH" ) ;
2422+
2423+ node. kernel
2424+ . cache
2425+ . borrow_mut ( )
2426+ . add_instrument ( InstrumentAny :: CryptoPerpetual ( instrument) )
2427+ . unwrap ( ) ;
2428+ insert_accepted_limit_order_in_node (
2429+ & node,
2430+ account_id,
2431+ client_id,
2432+ instrument_id,
2433+ client_order_id,
2434+ venue_order_id,
2435+ ) ;
2436+
2437+ assert_eq ! ( node. exec_manager. check_open_order_queries( ) . len( ) , 1 ) ;
2438+
2439+ let accepted = OrderAcceptedSpec :: builder ( )
2440+ . instrument_id ( instrument_id)
2441+ . client_order_id ( client_order_id)
2442+ . venue_order_id ( venue_order_id)
2443+ . account_id ( account_id)
2444+ . build ( ) ;
2445+ let event = ExecutionEvent :: OrderAcceptedBatch ( OrderAcceptedBatch :: new ( vec ! [ accepted] ) ) ;
2446+
2447+ let close_ids = node. observe_exec_event_before_dispatch ( & event) ;
2448+
2449+ assert_eq ! ( close_ids, Some ( Vec :: new( ) ) ) ;
2450+ assert ! ( node. exec_manager. check_open_order_queries( ) . is_empty( ) ) ;
2451+ }
2452+
2453+ #[ rstest]
2454+ fn test_live_node_builder_clock_factory_drives_kernel_clock ( ) {
2455+ let calls = Rc :: new ( Cell :: new ( 0usize ) ) ;
2456+ let calls_in_factory = calls. clone ( ) ;
2457+ let sentinel = UnixNanos :: from ( 123_456_789_u64 ) ;
2458+
2459+ let node = LiveNode :: builder ( TraderId :: from ( "TESTER-001" ) , Environment :: Sandbox )
2460+ . unwrap ( )
2461+ . with_reconciliation ( false )
2462+ . with_clock_factory ( move || {
2463+ calls_in_factory. set ( calls_in_factory. get ( ) + 1 ) ;
2464+ let mut clock = TestClock :: new ( ) ;
2465+ clock. advance_time ( sentinel, true ) ;
2466+ Rc :: new ( RefCell :: new ( clock) ) as Rc < RefCell < dyn Clock > >
2467+ } )
2468+ . build ( )
2469+ . unwrap ( ) ;
2470+
2471+ assert_eq ! ( node. kernel( ) . clock( ) . borrow( ) . timestamp_ns( ) , sentinel) ;
2472+ assert_eq ! ( calls. get( ) , 1 ) ;
2473+ }
2474+
23662475 #[ derive( Debug ) ]
23672476 struct ReplayKernelEventStore {
23682477 fail_restore : bool ,
0 commit comments