Skip to main content

nautilus_live/execution/
manager.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Execution reconciliation and state tracking for live hosts.
17//!
18//! [`ExecutionManager`] owns activity windows, retry state, fill identities, and cache-dependent
19//! decisions. It prepares queries and events, validates reports, and verifies applied fills.
20//! External-order registration updates the cache and publishes its initialization notification.
21//!
22//! The manager requests reports for individual checks and applies mass-status events to the engine.
23//! The live node owns recurring tasks, deadlines, and continuous event dispatch. Preparation snapshots
24//! retain activity revisions so decisions can be rechecked after requests or event callbacks.
25//! Activity revision counters are retained for the manager's lifetime so an older snapshot cannot
26//! mistake a reset counter for unchanged activity.
27
28use std::{
29    cell::{Ref, RefCell},
30    fmt::Debug,
31    rc::Rc,
32    sync::LazyLock,
33    time::Duration,
34};
35
36use anyhow::Context;
37use indexmap::{IndexMap, IndexSet};
38use nautilus_common::{
39    cache::{Cache, refs::PositionRef},
40    clients::{DEFAULT_POSITION_RECONCILIATION_TOLERANCE, ExecutionClient},
41    clock::Clock,
42    config::ConfigResult,
43    enums::{LogColor, LogLevel},
44    live::dst,
45    log_info,
46    messages::{
47        ExecutionReport,
48        execution::{
49            QueryOrder, TradingCommand,
50            report::{
51                GenerateFillReports, GenerateOrderStatusReport, GenerateOrderStatusReports,
52                GeneratePositionStatusReports,
53            },
54        },
55    },
56    msgbus::{self, MessagingSwitchboard, switchboard},
57};
58use nautilus_core::{DurationNanos, UUID4, UnixNanos, datetime::mins_to_secs};
59use nautilus_execution::{
60    engine::ExecutionEngine,
61    reconciliation::{
62        RECONCILIATION_ORDER_TAG, ReconciliationResult as AdjustedReports,
63        calculate_reconciliation_price, create_inferred_fill_for_qty,
64        create_reconciliation_rejected, create_reconciliation_triggered,
65        fill_precedes_snapshot_reconciled_position,
66        generate_external_order_status_events_with_commission,
67        generate_reconciliation_order_pre_fill_events,
68        generate_reconciliation_order_snapshot_events_with_commission,
69        incremental_inferred_fill_price_and_liquidity, inferred_fill_price_and_liquidity,
70        position_prices_match, process_mass_status_for_reconciliation,
71        process_mass_status_for_reconciliation_without_synthetic_reports,
72        reconcile_order_report_with_commission, should_reconciliation_update,
73    },
74};
75use nautilus_model::{
76    enums::{AvgPxReconciliation, OmsType, OrderSide, OrderStatus},
77    events::{OrderEventAny, OrderFilled, OrderInitialized, OrderUpdated},
78    identifiers::{
79        AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId,
80        TraderId, VenueOrderId,
81    },
82    instruments::{Instrument, InstrumentAny},
83    orders::{Order, OrderAny, TRIGGERABLE_ORDER_TYPES},
84    position::{Position, PositionReplayEvent},
85    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
86    types::{Money, Quantity},
87};
88use rust_decimal::Decimal;
89use ustr::Ustr;
90
91#[cfg(feature = "node")]
92pub(crate) use super::reconciliation::collect_targeted_order_reports;
93pub(crate) use super::reconciliation::{
94    OpenOrderReconciliationResult, OpenOrderReportCheck, SourcedOrderStatusReport,
95    TargetedOrderQuery, TargetedOrderReportResult, request_targeted_order_reports,
96    resolve_position_report_client_coverage,
97};
98pub use super::{
99    config::ExecutionManagerConfig,
100    reconciliation::{
101        ExternalOrderMetadata, InflightCheckResult, InstrumentAccountKey, PositionFillReportPlan,
102        PositionFillReportPreparation, PositionFillReportQuery, PositionReportCheck,
103        ReconciliationResult, ReportClientCoverage,
104    },
105};
106use super::{
107    recency::RecencyMap,
108    reconciliation::{
109        FillKey, InflightCheck, PositionQuantityComparison, PositionReconciliationState,
110        PositionReportShape, ReconciliationFillQueue, RetainedFillState,
111        create_orphan_fill_order_report, create_position_reconciliation_report,
112        create_reconciliation_canceled, distinct_position_reports, has_active_inferred_fill,
113        is_exact_order_match, position_avg_px, position_qty_aggregates,
114        resolve_inferred_fill_commission, responsible_client_ids, should_project_fill,
115        sort_reconciliation_events, terminal_report_has_missing_fills,
116    },
117    submission::{SubmissionRecoveryExhausted, SubmissionRecoveryPolicy, SubmissionRecoverySource},
118};
119
120/// Tag for orders originating from venue (external orders).
121static TAG_VENUE: LazyLock<Ustr> = LazyLock::new(|| Ustr::from("VENUE"));
122
123/// Tag for orders generated by reconciliation logic (synthetic orders).
124static TAG_RECONCILIATION: LazyLock<Ustr> = LazyLock::new(|| Ustr::from(RECONCILIATION_ORDER_TAG));
125
126const LOG_SAMPLES_MAX: usize = 5;
127
128#[derive(Clone, Debug)]
129struct SubmissionRecoveryState {
130    trader_id: TraderId,
131    client_id: Option<ClientId>,
132    strategy_id: StrategyId,
133    instrument_id: InstrumentId,
134    exhausted: bool,
135    venue_confirmed: bool,
136}
137
138/// Manager for execution state.
139///
140/// The `ExecutionManager` handles:
141/// - Startup reconciliation to align state on system start.
142/// - Continuous reconciliation of inflight orders.
143/// - External order discovery and claiming.
144/// - Fill report processing and validation.
145/// - Purging of old orders, positions, and account events.
146///
147/// # Thread safety
148///
149/// The manager shares its clock and cache through `Rc<RefCell<_>>` and stays on one thread.
150/// Hosts must release cache and engine borrows before dispatching callbacks that may reenter them.
151#[derive(Clone)]
152pub struct ExecutionManager {
153    clock: Rc<RefCell<dyn Clock>>,
154    cache: Rc<RefCell<Cache>>,
155    config: ExecutionManagerConfig,
156
157    order_activity: RecencyMap<ClientOrderId>,
158    order_inflight_checks: IndexMap<ClientOrderId, InflightCheck>,
159    submissions: IndexMap<ClientOrderId, SubmissionRecoveryState>,
160    submission_exhaustions: Vec<SubmissionRecoveryExhausted>,
161    submission_retention: IndexMap<ClientId, AccountId>,
162    order_query_recency: RecencyMap<ClientOrderId>,
163    order_query_pending: IndexSet<ClientOrderId>,
164    order_recon_retries: IndexMap<ClientOrderId, u32>,
165    order_coverage_unresolved: IndexSet<ClientOrderId>,
166    order_coverage_warnings: IndexSet<ClientOrderId>,
167    order_lookback_warnings: IndexSet<ClientOrderId>,
168
169    fills_processed: RecencyMap<FillKey>,
170    fills_recent: RecencyMap<FillKey>,
171    fills_unapplied: RecencyMap<FillKey>,
172
173    position_activity: RecencyMap<InstrumentAccountKey>,
174    position_activity_revisions: IndexMap<InstrumentAccountKey, u64>,
175    position_recon: IndexMap<InstrumentAccountKey, PositionReconciliationState>,
176    position_recon_tolerances: IndexMap<AccountId, Decimal>,
177}
178
179impl Debug for ExecutionManager {
180    #[rustfmt::skip]
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        f.debug_struct(stringify!(ExecutionManager))
183            .field("clock", &self.clock)
184            .field("cache", &self.cache)
185            .field("config", &self.config)
186            .field("order_activity", &self.order_activity)
187            .field("order_inflight_checks", &self.order_inflight_checks)
188            .field("submissions", &self.submissions)
189            .field("submission_exhaustions", &self.submission_exhaustions)
190            .field("submission_retention", &self.submission_retention)
191            .field("order_query_recency", &self.order_query_recency)
192            .field("order_query_pending", &self.order_query_pending)
193            .field("order_recon_retries", &self.order_recon_retries)
194            .field("order_coverage_unresolved", &self.order_coverage_unresolved)
195            .field("order_coverage_warnings", &self.order_coverage_warnings)
196            .field("order_lookback_warnings", &self.order_lookback_warnings)
197            .field("fills_processed", &self.fills_processed)
198            .field("fills_recent", &self.fills_recent)
199            .field("fills_unapplied", &self.fills_unapplied)
200            .field("position_activity", &self.position_activity)
201            .field("position_activity_revisions", &self.position_activity_revisions)
202            .field("position_recon", &self.position_recon)
203            .field("position_recon_tolerances", &self.position_recon_tolerances)
204            .finish()
205    }
206}
207
208impl ExecutionManager {
209    /// Creates a new [`ExecutionManager`] instance.
210    ///
211    /// # Errors
212    ///
213    /// Returns an error if `config` fails validation.
214    pub fn new(
215        clock: Rc<RefCell<dyn Clock>>,
216        cache: Rc<RefCell<Cache>>,
217        config: ExecutionManagerConfig,
218    ) -> ConfigResult<Self> {
219        config.validate()?;
220
221        Ok(Self {
222            clock,
223            cache,
224            config,
225            order_activity: RecencyMap::default(),
226            order_inflight_checks: IndexMap::new(),
227            submissions: IndexMap::new(),
228            submission_exhaustions: Vec::new(),
229            submission_retention: IndexMap::new(),
230            order_query_recency: RecencyMap::default(),
231            order_query_pending: IndexSet::new(),
232            order_recon_retries: IndexMap::new(),
233            order_coverage_unresolved: IndexSet::new(),
234            order_coverage_warnings: IndexSet::new(),
235            order_lookback_warnings: IndexSet::new(),
236            fills_processed: RecencyMap::default(),
237            fills_recent: RecencyMap::default(),
238            fills_unapplied: RecencyMap::default(),
239            position_activity: RecencyMap::default(),
240            position_activity_revisions: IndexMap::new(),
241            position_recon: IndexMap::new(),
242            position_recon_tolerances: IndexMap::new(),
243        })
244    }
245
246    /// Returns the execution manager configuration.
247    pub(crate) const fn config(&self) -> &ExecutionManagerConfig {
248        &self.config
249    }
250
251    /// Returns the trading clock timestamp in nanoseconds.
252    pub(crate) fn timestamp_ns(&self) -> UnixNanos {
253        self.clock.borrow().timestamp_ns()
254    }
255
256    /// Borrows the execution cache for reading.
257    pub(crate) fn cache(&self) -> Ref<'_, Cache> {
258        self.cache.borrow()
259    }
260
261    /// Registers an inflight command at dispatch, starting its recovery budget.
262    ///
263    /// Skips filtered orders and preserves an active submission's remaining budget.
264    /// Submission identity and duplicate suppression require [`Self::register_submission`].
265    pub fn register_inflight(&mut self, client_order_id: ClientOrderId) {
266        self.confirm_submission_outcome(&client_order_id);
267
268        if self.submission_recovery_pending(client_order_id)
269            && (self.order_inflight_checks.contains_key(&client_order_id)
270                || self.submission_is_retained(client_order_id))
271        {
272            return;
273        }
274
275        self.start_inflight_check(client_order_id);
276    }
277
278    /// Registers a submission's identity before execution dispatch or cache insertion.
279    ///
280    /// With submission tracking enabled, duplicate registration preserves the current
281    /// recovery budget until acknowledgement or exhaustion. After exhaustion, duplicate
282    /// registration preserves the unresolved identity without restarting recovery.
283    pub fn register_submission(
284        &mut self,
285        initialized: &OrderInitialized,
286        client_id: Option<ClientId>,
287    ) {
288        let client_order_id = initialized.client_order_id;
289
290        if self.submissions.contains_key(&client_order_id)
291            || self.submission_retention_enabled(client_order_id, client_id)
292        {
293            self.confirm_submission_outcome(&client_order_id);
294
295            if self.submissions.contains_key(&client_order_id)
296                || self.submission_is_acknowledged(client_order_id)
297            {
298                return;
299            }
300
301            self.track_submission(initialized, client_id);
302        }
303
304        self.start_inflight_check(client_order_id);
305    }
306
307    fn start_inflight_check(&mut self, client_order_id: ClientOrderId) {
308        if self
309            .config
310            .filtered_client_order_ids
311            .contains(&client_order_id)
312        {
313            return;
314        }
315
316        self.order_inflight_checks.insert(
317            client_order_id,
318            InflightCheck {
319                submitted_at: dst::time::Instant::now(),
320                retry_count: 0,
321                last_query_at: None,
322            },
323        );
324
325        self.order_recon_retries.insert(client_order_id, 0);
326        self.order_query_recency.remove(&client_order_id);
327        self.order_activity.remove(&client_order_id);
328    }
329
330    fn track_submission(&mut self, initialized: &OrderInitialized, client_id: Option<ClientId>) {
331        if !self.submission_retention_enabled(initialized.client_order_id, client_id)
332            || self
333                .config
334                .filtered_client_order_ids
335                .contains(&initialized.client_order_id)
336        {
337            return;
338        }
339
340        self.submissions
341            .entry(initialized.client_order_id)
342            .or_insert(SubmissionRecoveryState {
343                trader_id: initialized.trader_id,
344                client_id,
345                strategy_id: initialized.strategy_id,
346                instrument_id: initialized.instrument_id,
347                exhausted: false,
348                venue_confirmed: false,
349            });
350    }
351
352    fn submission_is_unacknowledged(order: &OrderAny) -> bool {
353        !order.events().iter().any(|event| {
354            matches!(
355                event,
356                OrderEventAny::Accepted(_)
357                    | OrderEventAny::Triggered(_)
358                    | OrderEventAny::Filled(_)
359                    | OrderEventAny::FillVoided(_)
360                    | OrderEventAny::Rejected(_)
361                    | OrderEventAny::Denied(_)
362                    | OrderEventAny::Canceled(_)
363                    | OrderEventAny::Expired(_)
364            ) || matches!(event, OrderEventAny::Updated(updated) if updated.venue_order_id.is_some())
365        })
366    }
367
368    fn submission_is_acknowledged(&self, client_order_id: ClientOrderId) -> bool {
369        self.cache
370            .borrow()
371            .order_ref(&client_order_id)
372            .is_some_and(|order| !Self::submission_is_unacknowledged(&order))
373    }
374
375    fn submission_recovery_pending(&self, client_order_id: ClientOrderId) -> bool {
376        self.submissions
377            .get(&client_order_id)
378            .is_some_and(|submission| !submission.venue_confirmed)
379            && !self.submission_is_acknowledged(client_order_id)
380    }
381
382    /// Returns retained submissions which still lack a native outcome or venue confirmation.
383    ///
384    /// Only the node's shutdown path reads this, so it is gated with its callers: without the
385    /// `node` feature the method has none, and a dependent crate that leaves the feature off would
386    /// see it as dead code.
387    #[cfg(feature = "node")]
388    pub(crate) fn unresolved_submission_ids(&self) -> Vec<ClientOrderId> {
389        self.submissions
390            .keys()
391            .copied()
392            .filter(|client_order_id| self.submission_recovery_pending(*client_order_id))
393            .collect()
394    }
395
396    /// Retires submission recovery after a native outcome has been applied.
397    ///
398    /// An acknowledgement ends the submission budget, including recovery for commands
399    /// dispatched before acknowledgement. A later command starts its own inflight check.
400    pub fn confirm_submission_outcome(&mut self, client_order_id: &ClientOrderId) {
401        let Some(submission) = self.submissions.get(client_order_id) else {
402            return;
403        };
404
405        let venue_confirmed = submission.venue_confirmed;
406
407        if !self.submission_is_acknowledged(*client_order_id) {
408            return;
409        }
410
411        if venue_confirmed {
412            // Native history replaces the report-only marker without clearing a later command
413            self.submissions.shift_remove(client_order_id);
414            return;
415        }
416
417        self.clear_recon_tracking(client_order_id, true);
418        self.record_local_activity(*client_order_id);
419    }
420
421    fn exhaust_submission(
422        &mut self,
423        client_order_id: ClientOrderId,
424        source: SubmissionRecoverySource,
425        retry_count: u32,
426    ) -> bool {
427        if !self.submissions.contains_key(&client_order_id)
428            && !self.submission_retention_enabled(client_order_id, None)
429        {
430            return false;
431        }
432
433        let budget = match source {
434            SubmissionRecoverySource::Inflight => self.config.inflight_max_retries,
435            SubmissionRecoverySource::MissingOrder => self.config.open_check_missing_retries,
436        };
437
438        if retry_count < budget {
439            return false;
440        }
441
442        if let Some(order) = self.get_order(client_order_id) {
443            if !Self::submission_is_unacknowledged(&order) {
444                return false;
445            }
446
447            let client_id = self.cache.borrow().client_id(&client_order_id).copied();
448            self.track_submission(order.init_event(), client_id);
449        }
450
451        let Some(submission) = self.submissions.get_mut(&client_order_id) else {
452            return false;
453        };
454
455        if submission.venue_confirmed {
456            return false;
457        }
458
459        if !submission.exhausted {
460            submission.exhausted = true;
461            self.submission_exhaustions
462                .push(SubmissionRecoveryExhausted {
463                    trader_id: submission.trader_id,
464                    client_id: submission
465                        .client_id
466                        .or_else(|| self.cache.borrow().client_id(&client_order_id).copied()),
467                    strategy_id: submission.strategy_id,
468                    instrument_id: submission.instrument_id,
469                    client_order_id,
470                    source,
471                    retry_count,
472                    ts_event: self.clock.borrow().timestamp_ns(),
473                });
474
475            log::warn!(
476                "Submission recovery exhausted for {client_order_id}, retaining unacknowledged order pending venue evidence"
477            );
478        }
479
480        self.clear_recon_tracking(&client_order_id, true);
481        true
482    }
483
484    /// Takes exhaustion diagnostics produced by recovery checks since the previous call.
485    ///
486    /// Hosts apply the returned reconciliation events before publishing these diagnostics,
487    /// so subscribers cannot invalidate a prepared local resolution through reentrancy.
488    /// Diagnostics describe query exhaustion for retained submissions,
489    /// not an authoritative venue outcome.
490    pub fn take_submission_recovery_exhaustions(&mut self) -> Vec<SubmissionRecoveryExhausted> {
491        std::mem::take(&mut self.submission_exhaustions)
492    }
493
494    /// Records local activity for the specified order.
495    ///
496    /// Uses a monotonic receipt instant, not venue or domain time, to accurately
497    /// track when we last processed activity for this order. This avoids race
498    /// conditions where network/queue latency makes events appear "old" even
499    /// though they just arrived.
500    pub fn record_local_activity(&mut self, client_order_id: ClientOrderId) {
501        self.order_activity.mark(client_order_id);
502    }
503
504    /// Returns the order's reconciliation retry count, or zero if no entry exists.
505    ///
506    /// Inflight checks and missing-order queries share this counter.
507    #[must_use]
508    pub fn recon_check_retry_count(&self, client_order_id: &ClientOrderId) -> u32 {
509        self.order_recon_retries
510            .get(client_order_id)
511            .copied()
512            .unwrap_or(0)
513    }
514
515    /// Clears pending targeted queries for the supplied orders.
516    pub(crate) fn remove_targeted_order_queries(&mut self, client_order_ids: &[ClientOrderId]) {
517        for client_order_id in client_order_ids {
518            self.order_query_pending.shift_remove(client_order_id);
519        }
520    }
521
522    /// Clears reconciliation tracking state for an order.
523    ///
524    /// An active submission's identity and budget survive pre-dispatch cleanup until
525    /// native state or a matching venue report acknowledges it. Exhaustion removes timers
526    /// and query bookkeeping but preserves the unresolved submission identity.
527    pub fn clear_recon_tracking(&mut self, client_order_id: &ClientOrderId, drop_last_query: bool) {
528        let pending = self.submission_recovery_pending(*client_order_id);
529        if pending && !self.submission_is_retained(*client_order_id) {
530            return;
531        }
532
533        self.order_inflight_checks.shift_remove(client_order_id);
534
535        let keep_confirmation = self
536            .submissions
537            .get(client_order_id)
538            .is_some_and(|submission| submission.venue_confirmed)
539            && self
540                .cache
541                .borrow()
542                .order_ref(client_order_id)
543                .is_some_and(|order| Self::submission_is_unacknowledged(&order));
544
545        if !pending && !keep_confirmation {
546            self.submissions.shift_remove(client_order_id);
547        }
548
549        self.order_recon_retries.shift_remove(client_order_id);
550        self.order_coverage_warnings.shift_remove(client_order_id);
551        self.order_lookback_warnings.shift_remove(client_order_id);
552        self.order_coverage_unresolved.shift_remove(client_order_id);
553        self.remove_targeted_order_queries(&[*client_order_id]);
554
555        if drop_last_query {
556            self.order_query_recency.remove(client_order_id);
557        }
558
559        self.order_activity.remove(client_order_id);
560    }
561
562    /// Prunes order activity outside the continuous reconciliation settling window.
563    pub fn prune_order_local_activity(&mut self) {
564        self.order_activity
565            .prune_older_than(Duration::from(self.config.open_check_threshold_ns));
566    }
567
568    /// Checks if a fill has been recently processed (for deduplication).
569    #[must_use]
570    pub fn is_fill_recently_processed(
571        &self,
572        account_id: AccountId,
573        instrument_id: InstrumentId,
574        trade_id: TradeId,
575    ) -> bool {
576        self.fills_recent
577            .contains_key(&(account_id, instrument_id, trade_id))
578    }
579
580    /// Marks a fill as recently processed when it is present on its canonical order.
581    pub fn commit_recent_fill_if_applied(&mut self, fill: &OrderFilled) {
582        let fill_key = (fill.account_id, fill.instrument_id, fill.trade_id);
583        if self.is_fill_applied(fill, fill_key) {
584            self.mark_fill_processed(fill_key.0, fill_key.1, fill_key.2);
585        }
586    }
587
588    /// Marks a fill as recently processed with the current monotonic instant.
589    pub fn mark_fill_processed(
590        &mut self,
591        account_id: AccountId,
592        instrument_id: InstrumentId,
593        trade_id: TradeId,
594    ) {
595        self.fills_recent
596            .mark((account_id, instrument_id, trade_id));
597    }
598
599    /// Prunes expired fills from the recent fills cache.
600    ///
601    /// Default TTL is 60 seconds.
602    pub fn prune_recent_fills_cache(&mut self, ttl_secs: f64) {
603        // Map the f64 TTL to a Duration, reproducing the old
604        // (ttl_secs * NANOSECONDS_IN_SECOND) as u64 cast at the boundaries
605        // rather than panicking on this pub fn. The as cast saturated:
606        //   - negative / NaN            -> 0        (prune everything)
607        //   - positive overflow / +inf  -> u64::MAX (keep everything)
608        // try_from_secs_f64 returns Err for all three, so branch on the sign
609        // to keep the two behaviors distinct.
610        let ttl = match Duration::try_from_secs_f64(ttl_secs) {
611            Ok(ttl) => ttl,
612            Err(_) if ttl_secs > 0.0 => Duration::MAX,
613            Err(_) => Duration::ZERO,
614        };
615
616        self.fills_recent.prune_older_than(ttl);
617    }
618
619    /// Prunes committed mass-reconciliation fills outside the startup report window.
620    ///
621    /// An unbounded startup lookback requires indefinite retention because no finite
622    /// horizon can safely exclude a replayed fill report.
623    pub fn prune_processed_fills(&mut self) {
624        let Some(lookback_mins) = self.config.lookback_mins else {
625            return;
626        };
627
628        let ttl = Duration::from_mins(lookback_mins).max(Duration::from_mins(1));
629        self.fills_processed.prune_older_than(ttl);
630    }
631
632    /// Sets the account tolerance, substituting the default for negative values.
633    pub(crate) fn set_position_reconciliation_tolerance(
634        &mut self,
635        account_id: AccountId,
636        tolerance: Decimal,
637    ) {
638        let tolerance = if tolerance < Decimal::ZERO {
639            log::error!(
640                "Invalid negative position reconciliation tolerance {tolerance} for \
641                 {account_id}; using the default"
642            );
643            DEFAULT_POSITION_RECONCILIATION_TOLERANCE
644        } else {
645            tolerance
646        };
647
648        self.position_recon_tolerances.insert(account_id, tolerance);
649    }
650
651    /// Returns the account tolerance, falling back to the default.
652    pub(crate) fn position_reconciliation_tolerance(&self, account_id: AccountId) -> Decimal {
653        self.position_recon_tolerances
654            .get(&account_id)
655            .copied()
656            .unwrap_or(DEFAULT_POSITION_RECONCILIATION_TOLERANCE)
657    }
658
659    /// Records position activity for the instrument and account.
660    ///
661    /// Restarts the reconciliation grace window and advances the activity revision, so position
662    /// report checks prepared earlier are no longer current. Uses monotonic `dst::time` so the
663    /// grace window is unaffected by trading-clock acceleration or venue timestamps.
664    pub fn record_position_activity(&mut self, instrument_id: InstrumentId, account_id: AccountId) {
665        let key = (instrument_id, account_id);
666        self.position_activity.mark(key);
667        let revision = self.position_activity_revisions.entry(key).or_default();
668        *revision = revision.saturating_add(1);
669    }
670
671    /// Checks whether position activity falls within the reconciliation grace window.
672    pub(crate) fn position_activity_is_recent(&self, key: &InstrumentAccountKey) -> bool {
673        self.position_activity
674            .within(key, Duration::from(self.config.position_check_threshold_ns))
675    }
676
677    /// Returns the position activity revision, or zero if no activity is recorded.
678    pub(crate) fn position_activity_revision(&self, key: &InstrumentAccountKey) -> u64 {
679        self.position_activity_revisions
680            .get(key)
681            .copied()
682            .unwrap_or_default()
683    }
684
685    fn set_position_reconciliation_retries(
686        &mut self,
687        key: InstrumentAccountKey,
688        report_shape: PositionReportShape,
689        retries: u32,
690    ) {
691        self.position_recon.insert(
692            key,
693            PositionReconciliationState {
694                report_shape,
695                retries,
696            },
697        );
698    }
699
700    /// Returns retries for the matching report shape, or zero if none match.
701    fn position_reconciliation_retries(
702        &self,
703        key: &InstrumentAccountKey,
704        report_shape: PositionReportShape,
705    ) -> u32 {
706        self.position_recon
707            .get(key)
708            .filter(|state| state.report_shape == report_shape)
709            .map_or(0, |state| state.retries)
710    }
711
712    /// Returns the current position-reconciliation retry count for the given
713    /// `(instrument, account)` key, or zero if no entry exists.
714    #[must_use]
715    pub fn position_recon_retry_count(&self, key: &InstrumentAccountKey) -> u32 {
716        self.position_recon
717            .get(key)
718            .map_or(0, |state| state.retries)
719    }
720
721    /// Clears reconciliation retry state for an instrument and account.
722    pub(crate) fn clear_position_reconciliation(&mut self, key: &InstrumentAccountKey) {
723        self.position_recon.shift_remove(key);
724    }
725
726    /// Retains reconciliation retry state only for active position keys.
727    pub(crate) fn retain_position_reconciliation(
728        &mut self,
729        active_keys: &IndexSet<InstrumentAccountKey>,
730    ) {
731        self.position_recon
732            .retain(|key, _| active_keys.contains(key));
733    }
734
735    /// Reconciles a mass snapshot, applying order events before evaluating positions.
736    ///
737    /// Publishes raw reports before cache mutation and verifies each historical fill after dispatch.
738    /// Returns processed events, external orders for client registration, and diagnostics for
739    /// in-scope nonzero venue positions that remain inconsistent with the cache.
740    pub fn reconcile_execution_mass_status(
741        &mut self,
742        mass_status: &ExecutionMassStatus,
743        exec_engine: &RefCell<ExecutionEngine>,
744    ) -> ReconciliationResult {
745        if exec_engine
746            .borrow()
747            .get_client(&mass_status.client_id)
748            .is_none()
749        {
750            log::error!(
751                "Cannot reconcile ExecutionMassStatus from unknown client {}",
752                mass_status.client_id
753            );
754            return ReconciliationResult::default();
755        }
756
757        self.validate_mass_status_order_sources(mass_status);
758
759        // Publish raw reports before any state mutation (including fill adjustment
760        // below, which can synthesize replacement order/fill reports). The
761        // execution engine's per-report `reconcile_*` entry points are bypassed by
762        // this path, so the capture seam lives here.
763        let raw_order_status_topic =
764            MessagingSwitchboard::reconciliation_raw_order_status_report_topic();
765
766        for report in mass_status.order_reports().values() {
767            msgbus::publish_any(raw_order_status_topic, report);
768        }
769
770        let raw_fill_topic = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
771
772        for fills in mass_status.fill_reports().values() {
773            for fill in fills {
774                msgbus::publish_any(raw_fill_topic, fill);
775            }
776        }
777
778        let raw_position_topic =
779            MessagingSwitchboard::reconciliation_raw_position_status_report_topic();
780
781        for reports in mass_status.position_reports().values() {
782            for report in reports {
783                msgbus::publish_any(raw_position_topic, report);
784            }
785        }
786
787        if exec_engine
788            .borrow()
789            .get_client(&mass_status.client_id)
790            .is_none()
791        {
792            log::error!(
793                "Execution client {} disappeared while publishing raw mass status reports",
794                mass_status.client_id
795            );
796            return ReconciliationResult::default();
797        }
798
799        let venue = mass_status.venue;
800        let order_count = mass_status.order_reports().len();
801        let fill_count: usize = mass_status.fill_reports().values().map(Vec::len).sum();
802        let position_count: usize = mass_status.position_reports().values().map(Vec::len).sum();
803
804        log_info!(
805            "Reconciling ExecutionMassStatus for {venue}",
806            color = LogColor::Blue
807        );
808        log_info!(
809            "Received {order_count} order(s), {fill_count} fill(s), {position_count} position(s)",
810            color = LogColor::Blue
811        );
812
813        if mass_status.lookback_start().is_some() && !mass_status.reports_complete() {
814            log::warn!("Received incomplete bounded reconciliation reports for {venue}");
815        }
816
817        let retained_fill_state = self.retained_fill_state();
818        let reported_fill_keys: IndexSet<FillKey> = mass_status
819            .fill_reports()
820            .values()
821            .flatten()
822            .filter(|fill| !fill.last_qty.is_zero())
823            .map(|fill| (fill.account_id, fill.instrument_id, fill.trade_id))
824            .collect();
825
826        let order_venue_ids = exec_engine
827            .borrow()
828            .get_client(&mass_status.client_id)
829            .and_then(|client| client.venue_order_ids(mass_status));
830        let mut adjusted_reports = self.adjust_mass_status_fills(mass_status);
831        Self::preserve_external_order_history(
832            mass_status,
833            order_venue_ids.as_ref(),
834            &mut adjusted_reports,
835        );
836        let AdjustedReports {
837            orders: adjusted_order_reports,
838            fills: adjusted_fill_reports,
839            mut order_only_ids,
840            order_only_fill_keys,
841        } = adjusted_reports;
842
843        order_only_ids.extend(self.order_only_ids(
844            mass_status,
845            &adjusted_order_reports,
846            &adjusted_fill_reports,
847        ));
848
849        let mut events = Vec::new();
850        let mut external_orders = Vec::new();
851        let mut orders_reconciled = 0usize;
852        let mut external_orders_created = 0usize;
853        let mut open_orders_initialized = 0usize;
854        let mut orders_skipped_no_instrument = 0usize;
855        let mut orders_skipped_duplicate = 0usize;
856        let mut fills_applied = 0usize;
857        let mut fill_queue = ReconciliationFillQueue::default();
858
859        let fill_reports = &adjusted_fill_reports;
860        let mut seen_fill_keys: IndexSet<FillKey> = IndexSet::new();
861
862        for fills in fill_reports.values() {
863            for fill in fills {
864                let fill_key = (fill.account_id, fill.instrument_id, fill.trade_id);
865                if !seen_fill_keys.insert(fill_key) {
866                    log::warn!(
867                        "Duplicate trade_id {} for {} in mass status",
868                        fill.trade_id,
869                        fill.instrument_id
870                    );
871                }
872            }
873        }
874
875        let order_reports = &adjusted_order_reports;
876        let mut orders_skipped_filtered = 0usize;
877        let mut orders_projected: IndexMap<ClientOrderId, OrderAny> = IndexMap::new();
878
879        for report in order_reports.values().filter(|report| {
880            order_venue_ids
881                .as_ref()
882                .and_then(|histories| report.client_order_id.and_then(|id| histories.get(&id)))
883                .is_none_or(|ids| ids.last() == Some(&report.venue_order_id))
884        }) {
885            if self.should_skip_order_report(report) {
886                orders_skipped_filtered += 1;
887                continue;
888            }
889
890            let order = report
891                .client_order_id
892                .and_then(|id| {
893                    orders_projected
894                        .get(&id)
895                        .cloned()
896                        .or_else(|| self.get_order(id))
897                })
898                .or_else(|| {
899                    self.get_order_by_venue_order_id(report.venue_order_id)
900                        .map(|order| {
901                            orders_projected
902                                .get(&order.client_order_id())
903                                .cloned()
904                                .unwrap_or(order)
905                        })
906                });
907            let order_client = order
908                .as_ref()
909                .filter(|order| Some(order.client_order_id()) == report.client_order_id);
910            let events_start = events.len();
911
912            if let Some(client_order_id) = &report.client_order_id {
913                if let Some(cached_order) = order_client
914                    && is_exact_order_match(cached_order, report)
915                {
916                    log::debug!("Skipping order {client_order_id}: already in sync with venue");
917                    orders_skipped_duplicate += 1;
918
919                    // Still ensure venue_order_id is indexed even when skipping
920                    if let Err(e) = self
921                        .cache
922                        .borrow_mut()
923                        .index_venue_order_id(client_order_id, &report.venue_order_id)
924                    {
925                        log::warn!("Failed to index venue order ID: {e}");
926                    }
927
928                    continue;
929                }
930
931                // Skip closed reconciliation orders to prevent duplicate inferred fills on restart
932                if let Some(cached_order) = order_client
933                    && cached_order.is_closed()
934                    && cached_order
935                        .tags()
936                        .is_some_and(|tags| tags.contains(&*TAG_RECONCILIATION))
937                {
938                    log::debug!(
939                        "Skipping closed reconciliation order {client_order_id}: \
940                         synthetic position adjustment from previous session",
941                    );
942                    orders_skipped_duplicate += 1;
943                    continue;
944                }
945            }
946
947            if let Some(order) = order.as_ref() {
948                let instrument = self.get_instrument(&report.instrument_id);
949
950                if order_client.is_some() {
951                    log::info!(
952                        color = LogColor::Blue as u8;
953                        "Reconciling {} {} {} [{}] -> [{}]",
954                        order.client_order_id(),
955                        report.venue_order_id,
956                        report.instrument_id,
957                        order.status(),
958                        report.order_status,
959                    );
960                } else {
961                    // Fallback: match by venue_order_id
962                    log::info!(
963                        color = LogColor::Blue as u8;
964                        "Reconciling {} (matched by venue_order_id {}) {} [{}] -> [{}]",
965                        order.client_order_id(),
966                        report.venue_order_id,
967                        report.instrument_id,
968                        order.status(),
969                        report.order_status,
970                    );
971                }
972
973                let order_fills: Vec<&FillReport> = fill_reports
974                    .get(&report.venue_order_id)
975                    .map(|f| f.iter().collect())
976                    .unwrap_or_default();
977                let engine_ref = exec_engine.borrow();
978                let commission_client = engine_ref.get_client(&mass_status.client_id);
979
980                let order_events = self.reconcile_order_with_fills(
981                    true,
982                    order,
983                    report,
984                    &order_fills,
985                    instrument.as_ref(),
986                    &mut fill_queue,
987                    commission_client,
988                );
989
990                drop(engine_ref);
991
992                orders_reconciled += usize::from(!order_events.is_empty());
993                fills_applied += order_events
994                    .iter()
995                    .filter(|e| matches!(e, OrderEventAny::Filled(_)))
996                    .count();
997                events.extend(order_events);
998
999                // Always ensure venue_order_id is indexed after reconciliation
1000                if let Err(e) = self
1001                    .cache
1002                    .borrow_mut()
1003                    .index_venue_order_id(&order.client_order_id(), &report.venue_order_id)
1004                {
1005                    log::warn!("Failed to index venue order ID: {e}");
1006                }
1007            } else if let Some(client_order_id) = &report.client_order_id {
1008                if let Some(instrument) = self.get_instrument(&report.instrument_id) {
1009                    let venue_order_ids = order_venue_ids
1010                        .as_ref()
1011                        .and_then(|histories| histories.get(client_order_id));
1012                    let order_fills: Vec<&FillReport> = venue_order_ids
1013                        .map_or(std::slice::from_ref(&report.venue_order_id), Vec::as_slice)
1014                        .iter()
1015                        .filter_map(|id| fill_reports.get(id))
1016                        .flatten()
1017                        .collect();
1018                    let engine_ref = exec_engine.borrow();
1019                    let commission_client = engine_ref.get_client(&mass_status.client_id);
1020
1021                    let (mut external_events, metadata) = self.handle_external_order(
1022                        report,
1023                        mass_status.account_id,
1024                        &instrument,
1025                        &order_fills,
1026                        false, // Not synthetic (venue order)
1027                        Some(&mut fill_queue),
1028                        commission_client,
1029                    );
1030
1031                    drop(engine_ref);
1032
1033                    self.restore_external_order_venue_ids(
1034                        report,
1035                        venue_order_ids.map(Vec::as_slice),
1036                        &mut external_events,
1037                    );
1038
1039                    if !external_events.is_empty() {
1040                        external_orders_created += 1;
1041                        fills_applied += external_events
1042                            .iter()
1043                            .filter(|e| matches!(e, OrderEventAny::Filled(_)))
1044                            .count();
1045
1046                        if report.order_status.is_open() {
1047                            open_orders_initialized += 1;
1048                        }
1049
1050                        events.extend(external_events);
1051
1052                        if let Some(m) = metadata {
1053                            external_orders.push(m);
1054                        }
1055                    }
1056                } else {
1057                    orders_skipped_no_instrument += 1;
1058                }
1059            } else if let Some(instrument) = self.get_instrument(&report.instrument_id) {
1060                // Synthetic orders (S- prefix) are generated by reconciliation logic
1061                let is_synthetic = report.venue_order_id.as_str().starts_with("S-");
1062
1063                let order_fills: Vec<&FillReport> = fill_reports
1064                    .get(&report.venue_order_id)
1065                    .map(|f| f.iter().collect())
1066                    .unwrap_or_default();
1067                let engine_ref = exec_engine.borrow();
1068                let commission_client = engine_ref.get_client(&mass_status.client_id);
1069
1070                let (external_events, metadata) = self.handle_external_order(
1071                    report,
1072                    mass_status.account_id,
1073                    &instrument,
1074                    &order_fills,
1075                    is_synthetic,
1076                    Some(&mut fill_queue),
1077                    commission_client,
1078                );
1079
1080                drop(engine_ref);
1081
1082                if !external_events.is_empty() {
1083                    external_orders_created += 1;
1084                    fills_applied += external_events
1085                        .iter()
1086                        .filter(|e| matches!(e, OrderEventAny::Filled(_)))
1087                        .count();
1088
1089                    if report.order_status.is_open() {
1090                        open_orders_initialized += 1;
1091                    }
1092
1093                    events.extend(external_events);
1094
1095                    if let Some(m) = metadata {
1096                        external_orders.push(m);
1097                    }
1098                }
1099            } else {
1100                orders_skipped_no_instrument += 1;
1101            }
1102
1103            if events.len() > events_start
1104                && let Some(mut order) = order
1105            {
1106                for event in &events[events_start..] {
1107                    if event.client_order_id() == order.client_order_id()
1108                        && let Err(e) = order.apply(event.clone())
1109                    {
1110                        log::warn!("Cannot project queued reconciliation event: {e}");
1111                    }
1112                }
1113                orders_projected.insert(order.client_order_id(), order);
1114            }
1115        }
1116
1117        // Process orphan fills (fills without matching order reports)
1118        let mut dropped_fill_groups: Vec<(VenueOrderId, InstrumentId, usize)> = Vec::new();
1119
1120        for (venue_order_id, fills) in fill_reports {
1121            if order_reports.contains_key(venue_order_id) {
1122                continue;
1123            }
1124
1125            let Some(first_fill) = fills.first() else {
1126                continue;
1127            };
1128
1129            if !self.should_reconcile_instrument(&first_fill.instrument_id) {
1130                log::debug!(
1131                    "Skipping orphan fills for {}: not in reconciliation_instrument_ids",
1132                    first_fill.instrument_id
1133                );
1134                continue;
1135            }
1136
1137            // Skip if fill's client_order_id is in filtered list
1138            if let Some(client_order_id) = &first_fill.client_order_id
1139                && self
1140                    .config
1141                    .filtered_client_order_ids
1142                    .contains(client_order_id)
1143            {
1144                log::debug!(
1145                    "Skipping orphan fills for {client_order_id}: in filtered_client_order_ids"
1146                );
1147                continue;
1148            }
1149
1150            let order = first_fill
1151                .client_order_id
1152                .as_ref()
1153                .and_then(|id| self.get_order(*id))
1154                .or_else(|| self.get_order_by_venue_order_id(*venue_order_id));
1155
1156            // Skip if resolved order's client_order_id is filtered (venue_order_id lookup path)
1157            if let Some(ref order) = order
1158                && self
1159                    .config
1160                    .filtered_client_order_ids
1161                    .contains(&order.client_order_id())
1162            {
1163                log::debug!(
1164                    "Skipping orphan fills for {}: in filtered_client_order_ids",
1165                    order.client_order_id()
1166                );
1167                continue;
1168            }
1169
1170            if let Some(order) = order {
1171                let instrument_id = order.instrument_id();
1172                if let Some(instrument) = self.get_instrument(&instrument_id) {
1173                    let mut sorted_fills: Vec<&FillReport> = fills.iter().collect();
1174                    sorted_fills.sort_by_key(|f| f.ts_event);
1175
1176                    for fill in sorted_fills {
1177                        if let Some((event, fill_key)) = self.create_order_fill(
1178                            &order,
1179                            fill,
1180                            &instrument,
1181                            &fill_queue.pending_fill_keys,
1182                        ) {
1183                            fills_applied += 1;
1184                            fill_queue.push(&mut events, event, fill_key);
1185                        }
1186                    }
1187                } else {
1188                    orders_skipped_no_instrument += 1;
1189                }
1190            } else if fills.iter().any(FillReport::has_venue_position_id) {
1191                if !self.config.generate_missing_orders {
1192                    log::debug!(
1193                        "Skipping orphan fills for venue order {venue_order_id}: \
1194                         `generate_missing_orders` is disabled"
1195                    );
1196                    orders_skipped_filtered += 1;
1197                    continue;
1198                }
1199
1200                let Some(instrument) = self.get_instrument(&first_fill.instrument_id) else {
1201                    orders_skipped_no_instrument += 1;
1202                    continue;
1203                };
1204
1205                let mut sorted_fills: Vec<&FillReport> = fills.iter().collect();
1206                sorted_fills.sort_by_key(|fill| fill.ts_event);
1207
1208                let report = match create_orphan_fill_order_report(&sorted_fills, &instrument) {
1209                    Ok(report) => report,
1210                    Err(e) => {
1211                        log::error!(
1212                            "Cannot materialize orphan fills for venue order {venue_order_id}: {e}"
1213                        );
1214
1215                        continue;
1216                    }
1217                };
1218
1219                let engine_ref = exec_engine.borrow();
1220                let commission_client = engine_ref.get_client(&mass_status.client_id);
1221
1222                let (external_events, metadata) = self.handle_external_order(
1223                    &report,
1224                    mass_status.account_id,
1225                    &instrument,
1226                    &sorted_fills,
1227                    false,
1228                    Some(&mut fill_queue),
1229                    commission_client,
1230                );
1231
1232                drop(engine_ref);
1233
1234                if !external_events.is_empty() {
1235                    external_orders_created += 1;
1236                    fills_applied += external_events
1237                        .iter()
1238                        .filter(|event| matches!(event, OrderEventAny::Filled(_)))
1239                        .count();
1240
1241                    events.extend(external_events);
1242
1243                    if let Some(metadata) = metadata {
1244                        external_orders.push(metadata);
1245                    }
1246                }
1247            } else {
1248                log::debug!(
1249                    "Dropping {} fill(s) for venue order {venue_order_id} ({}): \
1250                     no order report or cached order",
1251                    fills.len(),
1252                    first_fill.instrument_id,
1253                );
1254                dropped_fill_groups.push((*venue_order_id, first_fill.instrument_id, fills.len()));
1255            }
1256        }
1257
1258        sort_reconciliation_events(&mut events);
1259
1260        let mut unapplied_fill_position_ids = IndexSet::new();
1261
1262        for event in &events {
1263            if let OrderEventAny::Filled(fill) = event
1264                && should_project_fill(
1265                    fill,
1266                    &retained_fill_state,
1267                    &reported_fill_keys,
1268                    &order_only_ids,
1269                    &order_only_fill_keys,
1270                )
1271            {
1272                exec_engine.borrow_mut().project_reconciliation_fill(fill);
1273            } else {
1274                exec_engine.borrow_mut().process(event);
1275            }
1276
1277            if let OrderEventAny::Filled(fill) = event
1278                && let Some(fill_key) = fill_queue.event_fill_keys.get(&fill.event_id).copied()
1279            {
1280                if self.is_fill_applied(fill, fill_key) {
1281                    self.fills_processed.mark(fill_key);
1282                } else if let Some(venue_position_id) = fill.position_id {
1283                    log::error!(
1284                        "Skipping reconciliation for venue position {venue_position_id}: historical fill {} was not applied",
1285                        fill.trade_id,
1286                    );
1287
1288                    unapplied_fill_position_ids.insert(venue_position_id);
1289                }
1290            }
1291        }
1292
1293        let mut positions_created = 0usize;
1294
1295        if !self.config.filter_position_reports {
1296            // Collect instruments with fills that lack venue_position_id (can't attribute to
1297            // specific hedge position, so must skip all hedge reports for that instrument)
1298            let instruments_with_unattributed_fills: IndexSet<InstrumentId> = mass_status
1299                .fill_reports()
1300                .values()
1301                .flatten()
1302                .filter(|f| !f.last_qty.is_zero() && f.venue_position_id.is_none())
1303                .map(|f| f.instrument_id)
1304                .chain(
1305                    mass_status
1306                        .order_reports()
1307                        .values()
1308                        .filter(|r| !r.filled_qty.is_zero() && r.venue_position_id.is_none())
1309                        .map(|r| r.instrument_id),
1310                )
1311                .collect();
1312
1313            for (instrument_id, reports) in mass_status.position_reports() {
1314                if !self.should_reconcile_instrument(&instrument_id) {
1315                    log::debug!(
1316                        "Skipping position reports for {instrument_id}: not in reconciliation_instrument_ids"
1317                    );
1318                    continue;
1319                }
1320
1321                for report in reports {
1322                    if report.venue_position_id.is_some_and(|venue_position_id| {
1323                        unapplied_fill_position_ids.contains(&venue_position_id)
1324                    }) {
1325                        continue;
1326                    }
1327
1328                    if let Some(position_events) = self.reconcile_position_report(
1329                        &report,
1330                        mass_status.account_id,
1331                        &instruments_with_unattributed_fills,
1332                    ) {
1333                        for event in position_events {
1334                            exec_engine.borrow_mut().process(&event);
1335                            events.push(event);
1336                        }
1337
1338                        positions_created += 1;
1339                    }
1340                }
1341            }
1342        }
1343
1344        if orders_skipped_no_instrument > 0 {
1345            log::warn!("{orders_skipped_no_instrument} orders skipped (instrument not in cache)");
1346        }
1347
1348        if !dropped_fill_groups.is_empty() {
1349            let dropped_fill_count: usize =
1350                dropped_fill_groups.iter().map(|(_, _, count)| count).sum();
1351
1352            let samples = dropped_fill_groups
1353                .iter()
1354                .take(LOG_SAMPLES_MAX)
1355                .map(|(venue_order_id, instrument_id, _)| {
1356                    format!("{venue_order_id} ({instrument_id})")
1357                })
1358                .collect::<Vec<_>>()
1359                .join(", ");
1360
1361            log::warn!(
1362                "Dropped {dropped_fill_count} fill(s) in {} fill group(s) without an order report \
1363                 or cached order ({samples})",
1364                dropped_fill_groups.len(),
1365            );
1366        }
1367
1368        if orders_skipped_duplicate > 0 {
1369            log::debug!("{orders_skipped_duplicate} orders skipped (already in sync)");
1370        }
1371
1372        if orders_skipped_filtered > 0 {
1373            log::debug!("{orders_skipped_filtered} orders skipped (filtered by config)");
1374        }
1375
1376        log::info!(
1377            color = LogColor::Blue as u8;
1378            "Reconciliation complete for {venue}: reconciled={orders_reconciled}, external={external_orders_created}, open={open_orders_initialized}, fills={fills_applied}, positions={positions_created}, skipped={orders_skipped_duplicate}, filtered={orders_skipped_filtered}",
1379        );
1380
1381        ReconciliationResult {
1382            events,
1383            external_orders,
1384            unresolved_positions: self.unresolved_mass_status_positions(mass_status),
1385        }
1386    }
1387
1388    fn preserve_external_order_history(
1389        mass_status: &ExecutionMassStatus,
1390        order_venue_ids: Option<&IndexMap<ClientOrderId, Vec<VenueOrderId>>>,
1391        adjusted: &mut AdjustedReports,
1392    ) {
1393        let Some(order_venue_ids) = order_venue_ids else {
1394            return;
1395        };
1396
1397        let reports = mass_status.order_reports();
1398        let fills = mass_status.fill_reports();
1399
1400        for id in order_venue_ids.values().flatten() {
1401            if let Some(report) = reports.get(id) {
1402                adjusted.orders.entry(*id).or_insert_with(|| report.clone());
1403            }
1404
1405            let retained = adjusted.fills.entry(*id).or_default();
1406            for fill in fills.get(id).into_iter().flatten() {
1407                if !fill.last_qty.is_zero()
1408                    && !retained
1409                        .iter()
1410                        .any(|existing| existing.trade_id == fill.trade_id)
1411                {
1412                    // Restore filtered native history to the order without reopening old exposure
1413                    adjusted.order_only_fill_keys.insert((
1414                        fill.account_id,
1415                        fill.instrument_id,
1416                        fill.trade_id,
1417                    ));
1418                    retained.push(fill.clone());
1419                }
1420            }
1421        }
1422    }
1423
1424    fn restore_external_order_venue_ids(
1425        &self,
1426        report: &OrderStatusReport,
1427        venue_order_ids: Option<&[VenueOrderId]>,
1428        events: &mut Vec<OrderEventAny>,
1429    ) {
1430        let Some((first, remaining)) = venue_order_ids.and_then(|ids| ids.split_first()) else {
1431            return;
1432        };
1433
1434        let Some(OrderEventAny::Accepted(mut accepted)) = events.first().cloned() else {
1435            return;
1436        };
1437
1438        let Some(order) = report.client_order_id.and_then(|id| self.get_order(id)) else {
1439            return;
1440        };
1441
1442        accepted.venue_order_id = *first;
1443        let mut lifecycle = vec![OrderEventAny::Accepted(accepted)];
1444
1445        // Restore ID history at acceptance time, before native fills are applied
1446        for venue_order_id in remaining {
1447            lifecycle.push(OrderEventAny::Updated(OrderUpdated::new(
1448                order.trader_id(),
1449                order.strategy_id(),
1450                order.instrument_id(),
1451                order.client_order_id(),
1452                order.quantity(),
1453                UUID4::new(),
1454                report.ts_accepted,
1455                self.clock.borrow().timestamp_ns(),
1456                true,
1457                Some(*venue_order_id),
1458                Some(accepted.account_id),
1459                order.price(),
1460                order.trigger_price(),
1461                None,
1462                order.is_quote_quantity(),
1463            )));
1464        }
1465
1466        if let Err(e) =
1467            self.cache
1468                .borrow_mut()
1469                .add_venue_order_id(&order.client_order_id(), first, true)
1470        {
1471            log::error!("Cannot initialize venue order ID history: {e}");
1472            return;
1473        }
1474
1475        events.splice(..1, lifecycle);
1476    }
1477
1478    fn unresolved_mass_status_positions(&self, mass_status: &ExecutionMassStatus) -> Vec<String> {
1479        if self.config.filter_position_reports {
1480            return Vec::new();
1481        }
1482
1483        let mut unresolved = Vec::new();
1484
1485        for (instrument_id, reports) in mass_status.position_reports() {
1486            if !self.should_reconcile_instrument(&instrument_id) {
1487                continue;
1488            }
1489
1490            let reports = distinct_position_reports(reports);
1491
1492            if self
1493                .netting_position_reports_match((instrument_id, mass_status.account_id), &reports)
1494            {
1495                continue;
1496            }
1497
1498            for report in &reports {
1499                if let Some(reason) =
1500                    self.unresolved_position_report(report, mass_status.account_id, &reports)
1501                {
1502                    unresolved.push(reason);
1503                }
1504            }
1505        }
1506
1507        unresolved
1508    }
1509
1510    fn netting_position_reports_match(
1511        &self,
1512        key: InstrumentAccountKey,
1513        reports: &[PositionStatusReport],
1514    ) -> bool {
1515        let comparison = self.position_quantity_comparison(key, reports);
1516        let cache = self.cache.borrow();
1517
1518        !comparison.cached_positions.is_empty()
1519            && comparison
1520                .cached_positions
1521                .iter()
1522                .all(|position| cache.oms_type(&position.id) == Some(OmsType::Netting))
1523            && comparison.quantities_match(self.position_reconciliation_tolerance(key.1))
1524            && self.position_report_prices_match(&comparison.cached_positions, reports)
1525    }
1526
1527    fn unresolved_position_report(
1528        &self,
1529        report: &PositionStatusReport,
1530        account_id: AccountId,
1531        reports: &[PositionStatusReport],
1532    ) -> Option<String> {
1533        let cache = self.cache.borrow();
1534        let instrument_id = report.instrument_id;
1535
1536        let open_positions =
1537            cache.positions_open(None, Some(&instrument_id), None, Some(&account_id), None);
1538        let is_netting = !open_positions.is_empty()
1539            && open_positions
1540                .iter()
1541                .all(|position| cache.oms_type(&position.id) == Some(OmsType::Netting));
1542
1543        let venue_position_id = report.venue_position_id.filter(|_| !is_netting);
1544        let venue_qty = report.signed_decimal_qty;
1545
1546        let cached_positions: Vec<Position> = if let Some(position_id) = venue_position_id {
1547            cache
1548                .position(&position_id)
1549                .filter(|p| p.account_id == account_id && p.instrument_id == instrument_id)
1550                .into_iter()
1551                .map(|p| (*p).clone())
1552                .collect()
1553        } else {
1554            open_positions.into_iter().map(|p| (*p).clone()).collect()
1555        };
1556
1557        let cached_qty: Decimal = cached_positions
1558            .iter()
1559            .map(Position::signed_decimal_qty)
1560            .sum();
1561
1562        let quantities_match = if is_netting {
1563            self.position_quantity_comparison((instrument_id, account_id), reports)
1564                .quantities_match(self.position_reconciliation_tolerance(account_id))
1565        } else if venue_position_id.is_some() {
1566            cached_qty == venue_qty
1567        } else {
1568            (cached_qty - venue_qty).abs() <= self.position_reconciliation_tolerance(account_id)
1569        };
1570
1571        let price_reports = if is_netting {
1572            reports
1573        } else {
1574            std::slice::from_ref(report)
1575        };
1576
1577        let prices_match = self.position_report_prices_match(&cached_positions, price_reports);
1578
1579        if quantities_match && prices_match {
1580            return None;
1581        }
1582
1583        let reason = if cache.instrument(&instrument_id).is_none() {
1584            "instrument missing from cache"
1585        } else if cache.account(&account_id).is_none() {
1586            "account missing from cache"
1587        } else if !self.config.generate_missing_orders {
1588            "generate_missing_orders is disabled"
1589        } else if report.avg_px_open.is_none() && cached_qty == Decimal::ZERO {
1590            "missing avg_px_open for position recovery"
1591        } else if quantities_match {
1592            "position recovery did not restore the reported average entry price"
1593        } else {
1594            "position recovery did not restore the reported quantity"
1595        };
1596
1597        Some(format!(
1598            "account={account_id}, instrument={instrument_id}, venue_position_id={:?}, venue_quantity={venue_qty}: {reason}",
1599            report.venue_position_id,
1600        ))
1601    }
1602
1603    fn position_report_prices_match(
1604        &self,
1605        cached_positions: &[Position],
1606        reports: &[PositionStatusReport],
1607    ) -> bool {
1608        for negative in [false, true] {
1609            let reports: Vec<_> = reports
1610                .iter()
1611                .filter(|report| {
1612                    !report.signed_decimal_qty.is_zero()
1613                        && report.signed_decimal_qty.is_sign_negative() == negative
1614                })
1615                .collect();
1616
1617            if !reports.iter().any(|report| report.avg_px_open.is_some())
1618                || reports.iter().any(|report| {
1619                    report.avg_px_open_reconciliation == AvgPxReconciliation::OpeningOnly
1620                })
1621            {
1622                continue;
1623            }
1624
1625            let Some((venue_value, venue_qty)) = reports.iter().try_fold(
1626                (Decimal::ZERO, Decimal::ZERO),
1627                |(value, quantity), report| {
1628                    let price = report.avg_px_open.filter(|price| *price > Decimal::ZERO)?;
1629                    let qty = report.signed_decimal_qty.abs();
1630                    Some((value + price * qty, quantity + qty))
1631                },
1632            ) else {
1633                return false;
1634            };
1635
1636            let positions: Vec<&Position> = cached_positions
1637                .iter()
1638                .filter(|position| {
1639                    !position.quantity.is_zero()
1640                        && position.signed_decimal_qty().is_sign_negative() == negative
1641                })
1642                .collect();
1643
1644            if positions
1645                .iter()
1646                .any(|position| position.avg_px_open <= Decimal::ZERO)
1647            {
1648                return false;
1649            }
1650
1651            if positions.is_empty()
1652                && venue_qty <= self.position_reconciliation_tolerance(reports[0].account_id)
1653            {
1654                continue;
1655            }
1656
1657            let Some(cached_avg_px) = position_avg_px(positions) else {
1658                return false;
1659            };
1660
1661            // The coarsest reported precision bounds the rounding in the side's weighted average
1662            let venue_precision = reports
1663                .iter()
1664                .filter_map(|report| report.avg_px_open_precision)
1665                .min();
1666
1667            if !position_prices_match(
1668                cached_avg_px,
1669                venue_value / venue_qty,
1670                None,
1671                venue_precision,
1672            ) {
1673                return false;
1674            }
1675        }
1676
1677        true
1678    }
1679
1680    fn retained_fill_state(&self) -> RetainedFillState {
1681        let cache = self.cache.borrow();
1682        let positions = cache.positions(None, None, None, None, None);
1683        let mut fill_keys = IndexSet::new();
1684        let mut missing_order_ids = IndexSet::new();
1685        let mut missing_venue_order_ids = IndexSet::new();
1686        let mut netting_lifecycle_starts = IndexMap::new();
1687
1688        for position in positions {
1689            for fill in &position.events {
1690                fill_keys.insert((position.account_id, position.instrument_id, fill.trade_id));
1691                if cache.order(&fill.client_order_id).is_none() {
1692                    missing_order_ids.insert((
1693                        position.account_id,
1694                        position.instrument_id,
1695                        fill.client_order_id,
1696                    ));
1697                    missing_venue_order_ids.insert((
1698                        position.account_id,
1699                        position.instrument_id,
1700                        fill.venue_order_id,
1701                    ));
1702                }
1703            }
1704
1705            if cache.oms_type(&position.id) == Some(OmsType::Netting) {
1706                netting_lifecycle_starts.insert(
1707                    (
1708                        position.account_id,
1709                        position.instrument_id,
1710                        position.strategy_id,
1711                    ),
1712                    position.ts_opened,
1713                );
1714            }
1715        }
1716
1717        RetainedFillState {
1718            fill_keys,
1719            missing_order_ids,
1720            missing_venue_order_ids,
1721            netting_lifecycle_starts,
1722        }
1723    }
1724
1725    fn order_only_ids(
1726        &self,
1727        mass_status: &ExecutionMassStatus,
1728        order_reports: &IndexMap<VenueOrderId, OrderStatusReport>,
1729        fill_reports: &IndexMap<VenueOrderId, Vec<FillReport>>,
1730    ) -> IndexSet<VenueOrderId> {
1731        if mass_status.lookback_start().is_none() {
1732            return IndexSet::new();
1733        }
1734
1735        let reported_instruments: IndexSet<InstrumentId> = if self.config.filter_position_reports {
1736            IndexSet::new()
1737        } else {
1738            mass_status
1739                .position_reports()
1740                .into_iter()
1741                .filter(|(instrument_id, reports)| {
1742                    !reports.is_empty() && self.should_reconcile_instrument(instrument_id)
1743                })
1744                .map(|(instrument_id, _)| instrument_id)
1745                .collect()
1746        };
1747
1748        let mut venue_order_ids = IndexSet::new();
1749
1750        for (venue_order_id, report) in order_reports {
1751            if report.filled_qty.is_zero() || reported_instruments.contains(&report.instrument_id) {
1752                continue;
1753            }
1754
1755            venue_order_ids.insert(*venue_order_id);
1756        }
1757
1758        for (venue_order_id, fills) in fill_reports {
1759            let Some(fill) = fills.first() else {
1760                continue;
1761            };
1762
1763            if reported_instruments.contains(&fill.instrument_id) {
1764                continue;
1765            }
1766
1767            venue_order_ids.insert(*venue_order_id);
1768        }
1769
1770        venue_order_ids
1771    }
1772
1773    /// Validates cached order origins against the mass status client, logging a warning for each
1774    /// kind of violation. Never fails: orders persisted before origin tracking or materialized at
1775    /// runtime lack origins legitimately, so reconciliation proceeds regardless.
1776    fn validate_mass_status_order_sources(&self, mass_status: &ExecutionMassStatus) {
1777        let cache = self.cache.borrow();
1778        let mut checked_client_order_ids = IndexSet::new();
1779        let mut missing_origins: Vec<ClientOrderId> = Vec::new();
1780        let mut mismatched_origins: Vec<(ClientOrderId, ClientId)> = Vec::new();
1781
1782        let mut validate_report_source =
1783            |direct_client_order_id: Option<ClientOrderId>, venue_order_id: VenueOrderId| {
1784                let direct_client_order_id = direct_client_order_id
1785                    .filter(|client_order_id| cache.order_exists(client_order_id));
1786                let indexed_client_order_id = cache
1787                    .client_order_id(&venue_order_id)
1788                    .copied()
1789                    .filter(|client_order_id| cache.order_exists(client_order_id));
1790
1791                for client_order_id in [direct_client_order_id, indexed_client_order_id]
1792                    .into_iter()
1793                    .flatten()
1794                    .filter(|client_order_id| checked_client_order_ids.insert(*client_order_id))
1795                {
1796                    match cache.client_id(&client_order_id) {
1797                        Some(cached_client_id) if *cached_client_id == mass_status.client_id => {}
1798                        Some(cached_client_id) => {
1799                            mismatched_origins.push((client_order_id, *cached_client_id));
1800                        }
1801                        None => missing_origins.push(client_order_id),
1802                    }
1803                }
1804            };
1805
1806        for report in mass_status.order_reports().values() {
1807            validate_report_source(report.client_order_id, report.venue_order_id);
1808        }
1809
1810        for fills in mass_status.fill_reports().values() {
1811            for fill in fills {
1812                validate_report_source(fill.client_order_id, fill.venue_order_id);
1813            }
1814        }
1815
1816        if !missing_origins.is_empty() {
1817            let samples = missing_origins
1818                .iter()
1819                .take(LOG_SAMPLES_MAX)
1820                .map(ToString::to_string)
1821                .collect::<Vec<_>>()
1822                .join(", ");
1823
1824            log::warn!(
1825                "Found {} cached order(s) without an execution client origin ({}): \
1826                 continuing reconciliation against mass status client {} for compatibility \
1827                 with existing cache data",
1828                missing_origins.len(),
1829                samples,
1830                mass_status.client_id,
1831            );
1832        }
1833
1834        if !mismatched_origins.is_empty() {
1835            let samples = mismatched_origins
1836                .iter()
1837                .take(LOG_SAMPLES_MAX)
1838                .map(|(client_order_id, cached)| format!("{client_order_id} -> {cached}"))
1839                .collect::<Vec<_>>()
1840                .join(", ");
1841
1842            log::warn!(
1843                "Found {} cached order(s) with an execution client origin conflicting with \
1844                 mass status client {} ({}): continuing reconciliation for compatibility; \
1845                 this conflict will become a startup error in a future release, verify cached \
1846                 order ownership and execution client configuration",
1847                mismatched_origins.len(),
1848                mass_status.client_id,
1849                samples,
1850            );
1851        }
1852    }
1853
1854    /// Checks inflight orders and returns terminal events and intermediate venue queries.
1855    ///
1856    /// For retries below `inflight_max_retries`, generates `QueryOrder` commands to poll
1857    /// the venue for the order's current status. At max retries, generates terminal events
1858    /// (rejection or cancellation) based on the order's status. Orders never accepted are
1859    /// retained when the recovery policy or their client requires definitive venue evidence.
1860    pub fn check_inflight_orders(&mut self) -> InflightCheckResult {
1861        let mut result = InflightCheckResult::default();
1862        let now = dst::time::Instant::now();
1863        let threshold = Duration::from_millis(self.config().inflight_threshold_ms);
1864
1865        let mut to_check = Vec::new();
1866
1867        for (client_order_id, check) in &self.order_inflight_checks {
1868            if now
1869                .checked_duration_since(check.submitted_at)
1870                .is_some_and(|elapsed| elapsed > threshold)
1871            {
1872                to_check.push(*client_order_id);
1873            }
1874        }
1875
1876        for client_order_id in to_check {
1877            self.confirm_submission_outcome(&client_order_id);
1878
1879            if self
1880                .config
1881                .filtered_client_order_ids
1882                .contains(&client_order_id)
1883            {
1884                self.clear_recon_tracking(&client_order_id, true);
1885                continue;
1886            }
1887
1888            if self.order_query_pending.contains(&client_order_id) {
1889                continue;
1890            }
1891
1892            let Some(check) = self.order_inflight_checks.get_mut(&client_order_id) else {
1893                continue;
1894            };
1895
1896            if let Some(last_query_at) = check.last_query_at
1897                && now
1898                    .checked_duration_since(last_query_at)
1899                    .is_none_or(|elapsed| elapsed < threshold)
1900            {
1901                continue;
1902            }
1903
1904            check.retry_count += 1;
1905            check.last_query_at = Some(now);
1906            self.order_query_recency.mark(client_order_id);
1907            self.order_recon_retries
1908                .insert(client_order_id, check.retry_count);
1909
1910            if check.retry_count >= self.config.inflight_max_retries {
1911                let retry_count = check.retry_count;
1912
1913                if self.exhaust_submission(
1914                    client_order_id,
1915                    SubmissionRecoverySource::Inflight,
1916                    retry_count,
1917                ) {
1918                    continue;
1919                }
1920
1921                let ts_now = self.clock.borrow().timestamp_ns();
1922
1923                if let Some(order) = self.get_order(client_order_id) {
1924                    match order.status() {
1925                        OrderStatus::Submitted => {
1926                            // Generate rejection for submitted orders that never got accepted
1927                            if let Some(event) = create_reconciliation_rejected(
1928                                &order,
1929                                Some("INFLIGHT_TIMEOUT"),
1930                                ts_now,
1931                            ) {
1932                                result.events.push(event);
1933                            }
1934                        }
1935                        OrderStatus::PendingUpdate | OrderStatus::PendingCancel => {
1936                            // Generate cancellation for orders stuck in pending modify/cancel
1937                            result
1938                                .events
1939                                .push(create_reconciliation_canceled(&order, ts_now));
1940                        }
1941                        _ => {
1942                            // Order already resolved, just clear tracking
1943                        }
1944                    }
1945                }
1946
1947                self.clear_recon_tracking(&client_order_id, true);
1948            } else if let Some(order) = self.get_order(client_order_id) {
1949                // Intermediate retry: query the venue for current order status
1950                let ts_now = self.clock.borrow().timestamp_ns();
1951                let client_id = self.cache.borrow().client_id(&client_order_id).copied();
1952                let query = TradingCommand::QueryOrder(QueryOrder::new(
1953                    order.trader_id(),
1954                    client_id,
1955                    order.strategy_id(),
1956                    order.instrument_id(),
1957                    order.client_order_id(),
1958                    order.venue_order_id(),
1959                    UUID4::new(),
1960                    ts_now,
1961                    None,
1962                    None, // correlation_id
1963                ));
1964                result.queries.push(query);
1965            }
1966        }
1967
1968        result
1969    }
1970
1971    pub(crate) fn register_submission_retention(&mut self, client: &dyn ExecutionClient) {
1972        if client.retain_unresolved_submissions() {
1973            self.submission_retention
1974                .insert(client.client_id(), client.account_id());
1975        }
1976    }
1977
1978    fn submission_retention_enabled(
1979        &self,
1980        client_order_id: ClientOrderId,
1981        client_id: Option<ClientId>,
1982    ) -> bool {
1983        if self.config.submission_recovery_policy == SubmissionRecoveryPolicy::RetainUnresolved {
1984            return true;
1985        }
1986
1987        if self.submission_retention.is_empty() {
1988            return false;
1989        }
1990
1991        let cache = self.cache.borrow();
1992
1993        if let Some(client_id) = client_id.or_else(|| cache.client_id(&client_order_id).copied()) {
1994            return self.submission_retention.contains_key(&client_id);
1995        }
1996
1997        cache
1998            .order_ref(&client_order_id)
1999            .and_then(|order| order.account_id())
2000            .is_some_and(|account_id| {
2001                self.submission_retention
2002                    .values()
2003                    .any(|id| *id == account_id)
2004            })
2005    }
2006
2007    fn submission_is_retained(&self, client_order_id: ClientOrderId) -> bool {
2008        self.submissions
2009            .get(&client_order_id)
2010            .is_some_and(|submission| submission.exhausted)
2011            && self.submission_recovery_pending(client_order_id)
2012    }
2013
2014    fn filtered_open_orders_for_reconciliation(&self) -> Vec<OrderAny> {
2015        let cache = self.cache.borrow();
2016        let mut orders = cache.orders_open(None, None, None, None, None);
2017        orders.extend(cache.orders_inflight(None, None, None, None, None));
2018        let mut seen_client_order_ids = IndexSet::new();
2019
2020        orders
2021            .into_iter()
2022            .filter(|order| {
2023                seen_client_order_ids.insert(order.client_order_id())
2024                    && !self.submission_is_retained(order.client_order_id())
2025                    && !self
2026                        .config
2027                        .filtered_client_order_ids
2028                        .contains(&order.client_order_id())
2029                    && self.should_reconcile_instrument(&order.instrument_id())
2030            })
2031            .map(|order| order.clone())
2032            .collect()
2033    }
2034
2035    fn open_position_keys_for_reconciliation(&self) -> IndexSet<InstrumentAccountKey> {
2036        let cache = self.cache.borrow();
2037        let positions = cache.positions_open(None, None, None, None, None);
2038        let mut position_keys = IndexSet::new();
2039
2040        for position in positions {
2041            if !self.should_reconcile_instrument(&position.instrument_id) {
2042                continue;
2043            }
2044
2045            position_keys.insert((position.instrument_id, position.account_id));
2046        }
2047
2048        position_keys
2049    }
2050
2051    /// Collects open-order reports and targeted follow-ups, returning reconciliation events.
2052    ///
2053    /// The caller applies the returned events to its execution engine.
2054    pub async fn check_open_orders(
2055        &mut self,
2056        clients: &[&dyn ExecutionClient],
2057    ) -> Vec<OrderEventAny> {
2058        log::debug!("Checking order consistency between cached-state and venues");
2059
2060        let check = self.prepare_open_order_report_check(UUID4::new(), clients);
2061        let mut all_reports = Vec::new();
2062        let mut queried_clients = IndexSet::new();
2063        let mut failed_clients = IndexSet::new();
2064
2065        for client in clients {
2066            let client_id = client.client_id();
2067            queried_clients.insert(client_id);
2068
2069            match client.generate_order_status_reports(&check.command).await {
2070                Ok(reports) => {
2071                    all_reports.extend(
2072                        reports
2073                            .into_iter()
2074                            .map(|report| SourcedOrderStatusReport { client_id, report }),
2075                    );
2076                }
2077                Err(e) => {
2078                    failed_clients.insert(client_id);
2079                    log::warn!("Failed to query order reports from {client_id}: {e}");
2080                }
2081            }
2082        }
2083
2084        let result = self.reconcile_open_order_reports(
2085            &check,
2086            all_reports,
2087            &queried_clients,
2088            &failed_clients,
2089            clients,
2090        );
2091
2092        let mut events = result.events;
2093
2094        if !result.targeted_queries.is_empty() {
2095            let query_delay =
2096                Duration::from_millis(u64::from(self.config.single_order_query_delay_ms));
2097            let query_results =
2098                request_targeted_order_reports(result.targeted_queries, clients, query_delay).await;
2099            events.extend(self.reconcile_targeted_order_reports(query_results, clients));
2100        }
2101
2102        events
2103    }
2104
2105    /// Prepares a bulk open-order report request and snapshots cached open orders.
2106    pub(crate) fn prepare_open_order_report_check(
2107        &mut self,
2108        command_id: UUID4,
2109        clients: &[&dyn ExecutionClient],
2110    ) -> OpenOrderReportCheck {
2111        for client in clients {
2112            self.register_submission_retention(*client);
2113        }
2114
2115        let filtered_orders = self.filtered_open_orders_for_reconciliation();
2116        let active_order_ids: IndexSet<ClientOrderId> =
2117            filtered_orders.iter().map(Order::client_order_id).collect();
2118        self.order_coverage_warnings
2119            .retain(|client_order_id| active_order_ids.contains(client_order_id));
2120        self.order_lookback_warnings
2121            .retain(|client_order_id| active_order_ids.contains(client_order_id));
2122        self.order_coverage_unresolved
2123            .retain(|client_order_id| active_order_ids.contains(client_order_id));
2124
2125        let mut client_coverage = IndexMap::new();
2126
2127        for order in &filtered_orders {
2128            let client_order_id = order.client_order_id();
2129            let coverage = self.resolve_order_report_client_coverage(order, clients);
2130
2131            match &coverage {
2132                ReportClientCoverage::Resolved(_) => {
2133                    if self
2134                        .order_coverage_unresolved
2135                        .shift_remove(&client_order_id)
2136                    {
2137                        self.order_coverage_warnings.shift_remove(&client_order_id);
2138                    }
2139                }
2140                ReportClientCoverage::Unavailable(_) | ReportClientCoverage::Unresolved => {
2141                    self.order_coverage_unresolved.insert(client_order_id);
2142                }
2143            }
2144
2145            client_coverage.insert(client_order_id, coverage);
2146        }
2147
2148        log::debug!(
2149            "Found {} order{} open in cache",
2150            filtered_orders.len(),
2151            if filtered_orders.len() == 1 { "" } else { "s" }
2152        );
2153
2154        let ts_now = self.clock.borrow().timestamp_ns();
2155        let start = self
2156            .config
2157            .open_check_lookback_mins
2158            .map(DurationNanos::from_mins)
2159            .map(|lookback| ts_now.saturating_sub(lookback));
2160
2161        let mut command = GenerateOrderStatusReports::new(
2162            command_id,
2163            ts_now,
2164            self.config.open_check_open_only,
2165            None,
2166            start,
2167            None,
2168            None,
2169            None,
2170        );
2171        command.log_receipt_level = LogLevel::Debug;
2172
2173        OpenOrderReportCheck {
2174            command,
2175            filtered_orders,
2176            client_coverage,
2177        }
2178    }
2179
2180    fn resolve_order_report_client_coverage(
2181        &self,
2182        order: &OrderAny,
2183        clients: &[&dyn ExecutionClient],
2184    ) -> ReportClientCoverage {
2185        if let Some(client_id) = self.cache.borrow().client_id(&order.client_order_id()) {
2186            return ReportClientCoverage::Resolved(IndexSet::from([*client_id]));
2187        }
2188
2189        let client_ids =
2190            responsible_client_ids(order.account_id(), order.instrument_id().venue, clients);
2191
2192        if client_ids.is_empty() {
2193            ReportClientCoverage::Unresolved
2194        } else {
2195            ReportClientCoverage::Resolved(client_ids)
2196        }
2197    }
2198
2199    /// Builds throttled per-order venue queries for fallback open-order reconciliation.
2200    pub fn check_open_order_queries(&mut self) -> Vec<TradingCommand> {
2201        let now = dst::time::Instant::now();
2202        let query_delay = Duration::from_millis(u64::from(self.config.single_order_query_delay_ms));
2203        let query_limit = self.config.max_single_order_queries_per_cycle as usize;
2204
2205        if query_limit == 0 {
2206            return Vec::new();
2207        }
2208
2209        let mut filtered_orders = self.filtered_open_orders_for_reconciliation();
2210        filtered_orders.sort_by_key(|order| {
2211            let client_order_id = order.client_order_id();
2212            (
2213                self.order_query_recency.last_marked(&client_order_id),
2214                client_order_id,
2215            )
2216        });
2217
2218        let mut queries = Vec::new();
2219
2220        for order in filtered_orders {
2221            if queries.len() >= query_limit {
2222                break;
2223            }
2224
2225            let client_order_id = order.client_order_id();
2226            let threshold = Duration::from(self.config.open_check_threshold_ns);
2227
2228            if let Some(elapsed) = self.order_activity.elapsed_at(&client_order_id, now)
2229                && elapsed < threshold
2230            {
2231                let elapsed_ms = elapsed.as_millis();
2232                let threshold_ms = threshold.as_millis();
2233                log::debug!(
2234                    "Deferring open order query for {client_order_id}: recent local activity \
2235                     ({elapsed_ms}ms < threshold={threshold_ms}ms)",
2236                );
2237                continue;
2238            }
2239
2240            if self
2241                .order_query_recency
2242                .within_at(&client_order_id, now, query_delay)
2243            {
2244                continue;
2245            }
2246
2247            self.order_query_recency.mark(client_order_id);
2248            let client_id = self.cache.borrow().client_id(&client_order_id).copied();
2249            let ts_now = self.clock.borrow().timestamp_ns();
2250
2251            let cmd = TradingCommand::QueryOrder(QueryOrder::new(
2252                order.trader_id(),
2253                client_id,
2254                order.strategy_id(),
2255                order.instrument_id(),
2256                client_order_id,
2257                order.venue_order_id(),
2258                UUID4::new(),
2259                ts_now,
2260                None,
2261                None,
2262            ));
2263            queries.push(cmd);
2264        }
2265
2266        queries
2267    }
2268
2269    /// Reconciles bulk open-order report responses against a cached order snapshot.
2270    pub(crate) fn reconcile_open_order_reports(
2271        &mut self,
2272        check: &OpenOrderReportCheck,
2273        mut all_reports: Vec<SourcedOrderStatusReport>,
2274        queried_clients: &IndexSet<ClientId>,
2275        failed_clients: &IndexSet<ClientId>,
2276        clients: &[&dyn ExecutionClient],
2277    ) -> OpenOrderReconciliationResult {
2278        all_reports.retain(|sourced| !self.should_skip_order_report(&sourced.report));
2279        let mut venue_reported_ids = IndexSet::new();
2280
2281        for sourced in &all_reports {
2282            let report = &sourced.report;
2283
2284            let client_order_id = report.client_order_id.or_else(|| {
2285                self.cache
2286                    .borrow()
2287                    .client_order_id(&report.venue_order_id)
2288                    .copied()
2289            });
2290
2291            let Some(client_order_id) = client_order_id else {
2292                continue;
2293            };
2294
2295            venue_reported_ids.insert(client_order_id);
2296            self.order_coverage_warnings.shift_remove(&client_order_id);
2297            self.order_lookback_warnings.shift_remove(&client_order_id);
2298
2299            // A positive report, including one mapped by venue order ID, is proof the venue
2300            // still knows the order: reset the missing-order ladder so only consecutive misses
2301            // accumulate (mirrors the Python engine's per-report clear).
2302            self.order_recon_retries.shift_remove(&client_order_id);
2303        }
2304
2305        let mut events = Vec::new();
2306        let mut targeted_candidates = Vec::new();
2307
2308        for sourced in all_reports {
2309            let report = sourced.report;
2310
2311            let order = match report.client_order_id {
2312                Some(client_order_id) => self.get_order(client_order_id),
2313                None => self.get_order_by_venue_order_id(report.venue_order_id),
2314            };
2315
2316            let Some(order) = order else {
2317                continue;
2318            };
2319
2320            let client_order_id = order.client_order_id();
2321
2322            // Check for recent local activity to avoid race conditions with in-flight fills
2323            let threshold = Duration::from(self.config.open_check_threshold_ns);
2324            let activity_elapsed = self.order_activity.elapsed(&client_order_id);
2325            self.confirm_submitted_report(&report);
2326
2327            if let Some(elapsed) = activity_elapsed
2328                && elapsed < threshold
2329            {
2330                let elapsed_ms = elapsed.as_millis();
2331                let threshold_ms = threshold.as_millis();
2332                log::debug!(
2333                    "Deferring reconciliation for {client_order_id}: recent local activity ({elapsed_ms}ms < threshold={threshold_ms}ms)",
2334                );
2335                continue;
2336            }
2337
2338            let instrument = self.get_instrument(&report.instrument_id);
2339
2340            if terminal_report_has_missing_fills(&report, order.filled_qty()) {
2341                targeted_candidates.push((
2342                    order,
2343                    IndexSet::from([sourced.client_id]),
2344                    Some(report),
2345                ));
2346                continue;
2347            }
2348
2349            let commission_client = clients
2350                .iter()
2351                .find(|client| client.client_id() == sourced.client_id)
2352                .copied();
2353
2354            match self.reconcile_order_report(
2355                &order,
2356                &report,
2357                instrument.as_ref(),
2358                commission_client,
2359            ) {
2360                Ok(order_events) => events.extend(order_events),
2361                Err(e) => log::error!(
2362                    "Deferring reconciliation for {client_order_id}: order reconciliation failed: {e}"
2363                ),
2364            }
2365        }
2366
2367        // Handle orders missing at venue (skip in open_only mode where the
2368        // venue response may omit recently closed orders). When a lookback
2369        // window is set, only consider orders within that window so older
2370        // GTC orders outside the query range are not falsely marked missing.
2371        if self.config.open_check_open_only {
2372            let cached_ids: IndexSet<ClientOrderId> = check
2373                .filtered_orders
2374                .iter()
2375                .map(Order::client_order_id)
2376                .collect();
2377            let missing_at_venue: IndexSet<ClientOrderId> = cached_ids
2378                .difference(&venue_reported_ids)
2379                .copied()
2380                .collect();
2381
2382            if !missing_at_venue.is_empty() {
2383                log::debug!(
2384                    "{} cached open order{} not present in venue current response",
2385                    missing_at_venue.len(),
2386                    if missing_at_venue.len() == 1 {
2387                        " is"
2388                    } else {
2389                        "s are"
2390                    },
2391                );
2392
2393                for client_order_id in missing_at_venue {
2394                    log::debug!("Cached open order missing from venue response: {client_order_id}");
2395                }
2396            }
2397        } else {
2398            let candidates: Vec<&OrderAny> = if let Some(cutoff) = check.command.start {
2399                let mut candidates = Vec::new();
2400
2401                for order in &check.filtered_orders {
2402                    let client_order_id = order.client_order_id();
2403                    if order.ts_last() >= cutoff {
2404                        self.order_lookback_warnings.shift_remove(&client_order_id);
2405                        candidates.push(order);
2406                    } else if !venue_reported_ids.contains(&client_order_id)
2407                        && self.order_lookback_warnings.insert(client_order_id)
2408                    {
2409                        log::warn!(
2410                            "Skipping missing-order reconciliation for {client_order_id}: its last update predates the configured open-check lookback window; absence from the bulk response cannot be treated as evidence and no targeted query will be issued from it"
2411                        );
2412                    }
2413                }
2414
2415                candidates
2416            } else {
2417                check.filtered_orders.iter().collect()
2418            };
2419
2420            for order in candidates {
2421                let client_order_id = order.client_order_id();
2422                if venue_reported_ids.contains(&client_order_id) {
2423                    continue;
2424                }
2425
2426                let responsible_clients = match check.client_coverage.get(&client_order_id) {
2427                    Some(ReportClientCoverage::Resolved(responsible_clients))
2428                        if !responsible_clients.is_empty() =>
2429                    {
2430                        responsible_clients
2431                    }
2432                    _ => {
2433                        if self.order_coverage_warnings.insert(client_order_id) {
2434                            log::warn!(
2435                                "Skipping order reconciliation for {client_order_id}: responsible execution client coverage is unresolved"
2436                            );
2437                        }
2438
2439                        continue;
2440                    }
2441                };
2442
2443                let missing_clients = responsible_clients
2444                    .difference(queried_clients)
2445                    .copied()
2446                    .collect::<IndexSet<_>>();
2447
2448                if !missing_clients.is_empty() {
2449                    if self.order_coverage_warnings.insert(client_order_id) {
2450                        log::warn!(
2451                            "Skipping order reconciliation for {client_order_id}: responsible execution clients were not queried: {missing_clients:?}"
2452                        );
2453                    }
2454
2455                    continue;
2456                }
2457
2458                let failed_responsible_clients = responsible_clients
2459                    .intersection(failed_clients)
2460                    .copied()
2461                    .collect::<IndexSet<_>>();
2462
2463                if !failed_responsible_clients.is_empty() {
2464                    log::warn!(
2465                        "Skipping order reconciliation for {client_order_id}: failed to query responsible execution clients: {failed_responsible_clients:?}"
2466                    );
2467                    continue;
2468                }
2469
2470                self.order_coverage_warnings.shift_remove(&client_order_id);
2471                if let Some(order) = self.prepare_missing_order_query(client_order_id) {
2472                    targeted_candidates.push((order, responsible_clients.clone(), None));
2473                }
2474            }
2475        }
2476
2477        targeted_candidates.sort_by_key(|(order, _, _)| {
2478            let client_order_id = order.client_order_id();
2479            (
2480                self.order_query_recency.last_marked(&client_order_id),
2481                client_order_id,
2482            )
2483        });
2484
2485        let query_limit = self.config.max_single_order_queries_per_cycle as usize;
2486        let mut planned_queries = 0usize;
2487        let mut cap_deferred_orders = 0usize;
2488        let mut targeted_queries = Vec::new();
2489
2490        for (order, responsible_clients, report) in targeted_candidates {
2491            let client_order_id = order.client_order_id();
2492
2493            let required_queries = responsible_clients.len();
2494            let exceeds_query_limit = planned_queries + required_queries > query_limit;
2495            let can_run_oversized_group = planned_queries == 0 && query_limit > 0;
2496            if required_queries == 0 || (exceeds_query_limit && !can_run_oversized_group) {
2497                cap_deferred_orders += 1;
2498                continue;
2499            }
2500
2501            if required_queries > query_limit {
2502                log::warn!(
2503                    "Targeted order query for {client_order_id} requires {required_queries} responsible clients, exceeding the per-cycle limit {query_limit} to avoid indefinite deferral"
2504                );
2505            }
2506
2507            planned_queries += required_queries;
2508            self.order_query_recency.mark(client_order_id);
2509            self.order_query_pending.insert(client_order_id);
2510            let command_id = UUID4::new();
2511            let ts_now = self.clock.borrow().timestamp_ns();
2512
2513            let command = GenerateOrderStatusReport::new(
2514                command_id,
2515                ts_now,
2516                Some(order.instrument_id()),
2517                Some(client_order_id),
2518                order.venue_order_id(),
2519                None,
2520                None,
2521            );
2522            targeted_queries.push(TargetedOrderQuery {
2523                client_order_id,
2524                responsible_clients,
2525                report,
2526                filled_qty: order.filled_qty(),
2527                command,
2528            });
2529        }
2530
2531        if cap_deferred_orders > 0 {
2532            log::warn!(
2533                "Reached max single-order queries ({query_limit}) this cycle, deferring {cap_deferred_orders} order(s)"
2534            );
2535        }
2536
2537        OpenOrderReconciliationResult {
2538            events,
2539            targeted_queries,
2540        }
2541    }
2542
2543    /// Reconciles targeted query results, resolving missing orders only with complete coverage.
2544    pub(crate) fn reconcile_targeted_order_reports(
2545        &mut self,
2546        results: Vec<TargetedOrderReportResult>,
2547        clients: &[&dyn ExecutionClient],
2548    ) -> Vec<OrderEventAny> {
2549        let mut events = Vec::new();
2550        let mut fill_queue = ReconciliationFillQueue::default();
2551
2552        for result in results {
2553            let client_order_id = result.client_order_id;
2554            self.remove_targeted_order_queries(&[client_order_id]);
2555
2556            if let Some(report) = result.report {
2557                self.order_recon_retries.shift_remove(&client_order_id);
2558                self.order_coverage_warnings.shift_remove(&client_order_id);
2559
2560                let Some(order) = self.get_order(client_order_id) else {
2561                    continue;
2562                };
2563
2564                let instrument = self.get_instrument(&report.instrument_id);
2565
2566                let commission_client = result.client_id.and_then(|client_id| {
2567                    clients
2568                        .iter()
2569                        .find(|client| client.client_id() == client_id)
2570                        .copied()
2571                });
2572
2573                log::info!(
2574                    color = LogColor::Blue as u8;
2575                    "Found {client_order_id} via targeted order status query: {}",
2576                    report.order_status,
2577                );
2578
2579                let fills = result.fills.iter().collect::<Vec<_>>();
2580                events.extend(self.reconcile_order_with_fills(
2581                    false,
2582                    &order,
2583                    &report,
2584                    &fills,
2585                    instrument.as_ref(),
2586                    &mut fill_queue,
2587                    commission_client,
2588                ));
2589                continue;
2590            }
2591
2592            if result.coverage_complete {
2593                events.extend(self.resolve_missing_order(client_order_id));
2594            } else {
2595                log::warn!(
2596                    "Deferring missing-order resolution for {client_order_id}: targeted order status coverage was incomplete"
2597                );
2598            }
2599        }
2600
2601        events
2602    }
2603
2604    /// Collects position reports and returns synthetic discrepancy events.
2605    ///
2606    /// Registers each client's tolerance before evaluating its reports. The caller applies the
2607    /// returned events; the live node separately queries authoritative fills before synthetic fallback.
2608    pub async fn check_positions_consistency(
2609        &mut self,
2610        clients: &[&dyn ExecutionClient],
2611    ) -> Vec<OrderEventAny> {
2612        let check = self.prepare_position_report_check(UUID4::new(), clients);
2613        let mut reports = Vec::new();
2614        let mut queried_clients = IndexSet::new();
2615        let mut failed_clients = IndexSet::new();
2616
2617        for client in clients {
2618            let client_id = client.client_id();
2619            queried_clients.insert(client_id);
2620            self.set_position_reconciliation_tolerance(
2621                client.account_id(),
2622                client.position_reconciliation_tolerance(),
2623            );
2624
2625            match client
2626                .generate_position_status_reports(&check.command)
2627                .await
2628            {
2629                Ok(client_reports) => {
2630                    reports.extend(client_reports);
2631                }
2632                Err(e) => {
2633                    failed_clients.insert(client_id);
2634                    log::warn!("Failed to query position reports from {client_id}: {e}");
2635                }
2636            }
2637        }
2638
2639        let active_keys = self
2640            .open_position_keys_for_reconciliation()
2641            .into_iter()
2642            .chain(reports.iter().filter_map(|report| {
2643                (self.should_reconcile_instrument(&report.instrument_id)
2644                    && report.signed_decimal_qty != Decimal::ZERO)
2645                    .then_some((report.instrument_id, report.account_id))
2646            }))
2647            .collect();
2648
2649        let events =
2650            self.reconcile_position_reports(&check, reports, &queried_clients, &failed_clients);
2651
2652        // Global pruning requires unfiltered reports; flat reports must not preserve stale retries
2653        self.retain_position_reconciliation(&active_keys);
2654
2655        events
2656    }
2657
2658    /// Prepares a bulk position report request and records client coverage.
2659    ///
2660    /// Snapshots all activity revisions, including keys without open cached positions, so venue-only
2661    /// positions can be checked against activity that predates the request.
2662    #[must_use]
2663    pub fn prepare_position_report_check(
2664        &self,
2665        command_id: UUID4,
2666        clients: &[&dyn ExecutionClient],
2667    ) -> PositionReportCheck {
2668        let position_keys = self.open_position_keys_for_reconciliation();
2669
2670        let client_coverage = position_keys
2671            .iter()
2672            .map(|key| (*key, resolve_position_report_client_coverage(*key, clients)))
2673            .collect();
2674
2675        let mut activity_revisions = self.position_activity_revisions.clone();
2676
2677        for key in &position_keys {
2678            activity_revisions
2679                .entry(*key)
2680                .or_insert_with(|| self.position_activity_revision(key));
2681        }
2682
2683        log::debug!(
2684            "Found {} unique instrument/account combination{} with open positions",
2685            position_keys.len(),
2686            if position_keys.len() == 1 { "" } else { "s" }
2687        );
2688
2689        let ts_now = self.clock.borrow().timestamp_ns();
2690
2691        let mut command = GeneratePositionStatusReports::new(
2692            command_id, ts_now, None, // instrument_id - query all
2693            None, // start
2694            None, // end
2695            None, // params
2696            None, // correlation_id
2697        );
2698        command.log_receipt_level = LogLevel::Debug;
2699
2700        PositionReportCheck {
2701            command,
2702            client_coverage,
2703            activity_revisions,
2704        }
2705    }
2706
2707    /// Plans fill queries for settled position discrepancies with complete client coverage.
2708    ///
2709    /// Requires an unfiltered check and report snapshot for pruning. Coverage keys and nonflat
2710    /// venue reports retain retry state.
2711    pub fn plan_position_fill_reports(
2712        &mut self,
2713        check: &mut PositionReportCheck,
2714        reports: &[PositionStatusReport],
2715        queried_clients: &IndexSet<ClientId>,
2716        failed_clients: &IndexSet<ClientId>,
2717        clients: &[&dyn ExecutionClient],
2718    ) -> PositionFillReportPlan {
2719        let venue_positions = self.group_position_reports(reports.iter().cloned());
2720
2721        let keys = check
2722            .client_coverage
2723            .keys()
2724            .copied()
2725            .chain(venue_positions.iter().filter_map(|(key, reports)| {
2726                reports
2727                    .iter()
2728                    .any(|report| report.signed_decimal_qty != Decimal::ZERO)
2729                    .then_some(*key)
2730            }))
2731            .collect::<IndexSet<_>>();
2732
2733        let query_end = self.timestamp_ns();
2734        let lookback = DurationNanos::from_mins(self.config.position_check_lookback_mins);
2735        let query_start = query_end.saturating_sub(lookback);
2736        let mut discrepancy_keys = IndexSet::new();
2737        let mut queries = Vec::new();
2738
2739        for &key in &keys {
2740            let coverage = check
2741                .client_coverage
2742                .entry(key)
2743                .or_insert_with(|| resolve_position_report_client_coverage(key, clients));
2744            let prepared_revision = *check.activity_revisions.entry(key).or_default();
2745            let venue_reports = venue_positions
2746                .get(&key)
2747                .map(Vec::as_slice)
2748                .unwrap_or_default();
2749            let comparison = self.position_quantity_comparison(key, venue_reports);
2750            let tolerance = self.position_reconciliation_tolerance(key.1);
2751
2752            if comparison.quantities_match(tolerance) {
2753                self.clear_position_reconciliation(&key);
2754                continue;
2755            }
2756
2757            discrepancy_keys.insert(key);
2758
2759            if self.position_activity_revision(&key) > prepared_revision
2760                || self.position_activity_is_recent(&key)
2761            {
2762                continue;
2763            }
2764
2765            let report_shape = comparison.report_shape();
2766            let retries = self.position_reconciliation_retries(&key, report_shape);
2767            if retries >= self.config().position_check_retries {
2768                continue;
2769            }
2770
2771            let responsible_clients = match coverage {
2772                ReportClientCoverage::Resolved(responsible_clients) => responsible_clients,
2773
2774                // Coverage only decides whether an absent report proves a flat position
2775                ReportClientCoverage::Unavailable(responsible_clients)
2776                    if !venue_reports.is_empty() =>
2777                {
2778                    responsible_clients
2779                }
2780                ReportClientCoverage::Unavailable(_) => {
2781                    log::debug!(
2782                        "Skipping fill report query for {}/{}: responsible execution client coverage is unavailable",
2783                        key.0,
2784                        key.1,
2785                    );
2786                    continue;
2787                }
2788                ReportClientCoverage::Unresolved => {
2789                    log::warn!(
2790                        "Skipping fill report query for {}/{}: responsible execution client coverage is unresolved",
2791                        key.0,
2792                        key.1,
2793                    );
2794                    continue;
2795                }
2796            };
2797
2798            if responsible_clients.is_empty()
2799                || !responsible_clients.is_subset(queried_clients)
2800                || !responsible_clients.is_disjoint(failed_clients)
2801            {
2802                log::warn!(
2803                    "Skipping fill report query for {}/{}: responsible position report coverage is incomplete",
2804                    key.0,
2805                    key.1,
2806                );
2807                continue;
2808            }
2809
2810            for client_id in responsible_clients.iter() {
2811                let mut command = GenerateFillReports::new(
2812                    UUID4::new(),
2813                    query_end,
2814                    Some(key.0),
2815                    None,
2816                    Some(query_start),
2817                    Some(query_end),
2818                    None,
2819                    Some(check.command.command_id),
2820                );
2821                command.log_receipt_level = LogLevel::Debug;
2822                queries.push(PositionFillReportQuery {
2823                    key,
2824                    client_id: *client_id,
2825                    command,
2826                });
2827            }
2828        }
2829
2830        self.retain_position_reconciliation(&keys);
2831
2832        PositionFillReportPlan {
2833            queries,
2834            discrepancy_keys,
2835        }
2836    }
2837
2838    /// Checks whether position activity is unchanged since the check was prepared.
2839    #[must_use]
2840    pub fn position_report_check_is_current(
2841        &self,
2842        check: &PositionReportCheck,
2843        key: &InstrumentAccountKey,
2844    ) -> bool {
2845        check
2846            .activity_revisions
2847            .get(key)
2848            .is_some_and(|revision| self.position_activity_revision(key) == *revision)
2849    }
2850
2851    /// Validates fill attribution and supplies a cached position ID when unambiguous.
2852    ///
2853    /// # Errors
2854    ///
2855    /// Returns an error if cached order or position state conflicts with the fill,
2856    /// or inferred-fill history cannot be evaluated.
2857    pub fn prepare_position_fill_report(
2858        &self,
2859        report: &mut FillReport,
2860        venue_reports: &[PositionStatusReport],
2861    ) -> anyhow::Result<PositionFillReportPreparation> {
2862        let cache = self.cache();
2863        let venue_client_order_id = cache.client_order_id(&report.venue_order_id).copied();
2864        if let (Some(report_client_order_id), Some(venue_client_order_id)) =
2865            (report.client_order_id, venue_client_order_id)
2866        {
2867            anyhow::ensure!(
2868                report_client_order_id == venue_client_order_id,
2869                "fill {} client order ID {report_client_order_id} conflicts with venue order mapping {venue_client_order_id}",
2870                report.trade_id,
2871            );
2872        }
2873
2874        let client_order_id = report.client_order_id.or(venue_client_order_id);
2875        let order = client_order_id.and_then(|id| cache.order(&id));
2876        if let Some(order) = &order {
2877            anyhow::ensure!(
2878                order.instrument_id() == report.instrument_id
2879                    && order.order_side() == report.order_side
2880                    && order
2881                        .account_id()
2882                        .is_none_or(|account_id| account_id == report.account_id)
2883                    && order
2884                        .venue_order_id()
2885                        .is_none_or(|venue_order_id| venue_order_id == report.venue_order_id),
2886                "fill {} conflicts with cached order {}",
2887                report.trade_id,
2888                order.client_order_id(),
2889            );
2890        }
2891
2892        let hedge_context = report.venue_position_id.is_some()
2893            || venue_reports
2894                .iter()
2895                .any(|venue_report| venue_report.venue_position_id.is_some());
2896        let flip = client_order_id
2897            .map(|id| cache.hedging_flip_position_ids(id))
2898            .transpose()?
2899            .flatten();
2900
2901        let mapped_position_id = flip.map(|ids| ids[1]).or_else(|| {
2902            client_order_id
2903                .and_then(|client_order_id| cache.position_id(&client_order_id))
2904                .copied()
2905        });
2906
2907        if hedge_context
2908            && let (Some(venue_position_id), Some(mapped_position_id)) =
2909                (report.venue_position_id, mapped_position_id)
2910        {
2911            anyhow::ensure!(
2912                venue_position_id == mapped_position_id
2913                    || flip.is_some_and(|ids| ids.contains(&venue_position_id)),
2914                "fill {} position ID {venue_position_id} conflicts with cached order position {mapped_position_id}",
2915                report.trade_id,
2916            );
2917        }
2918
2919        if let Some(order) = order
2920            && has_active_inferred_fill(&order)?
2921        {
2922            return Ok(PositionFillReportPreparation::InferredOverlap);
2923        }
2924
2925        if fill_precedes_snapshot_reconciled_position(&cache, report) {
2926            return Ok(PositionFillReportPreparation::SnapshotOverlap);
2927        }
2928
2929        if report.venue_position_id.is_none() && flip.is_some() {
2930            report.venue_position_id = mapped_position_id;
2931            return Ok(PositionFillReportPreparation::Ready);
2932        }
2933
2934        if !hedge_context {
2935            return Ok(PositionFillReportPreparation::Ready);
2936        }
2937
2938        if report.venue_position_id.is_some() {
2939            return Ok(PositionFillReportPreparation::Ready);
2940        }
2941
2942        let Some(position_id) = mapped_position_id else {
2943            return Ok(PositionFillReportPreparation::Unattributed);
2944        };
2945
2946        let position = cache.position(&position_id).ok_or_else(|| {
2947            anyhow::anyhow!(
2948                "fill {} maps to position {position_id}, which is not cached",
2949                report.trade_id,
2950            )
2951        })?;
2952
2953        anyhow::ensure!(
2954            position.account_id == report.account_id
2955                && position.instrument_id == report.instrument_id,
2956            "fill {} maps to position {position_id} with a different account or instrument",
2957            report.trade_id,
2958        );
2959        anyhow::ensure!(
2960            position.is_open(),
2961            "fill {} maps to non-open position {position_id}",
2962            report.trade_id,
2963        );
2964        anyhow::ensure!(
2965            !position.is_opposite_side(report.order_side) || report.last_qty <= position.quantity,
2966            "fill {} without a venue position ID would cross position {position_id}",
2967            report.trade_id,
2968        );
2969
2970        report.venue_position_id = Some(position_id);
2971        Ok(PositionFillReportPreparation::Ready)
2972    }
2973
2974    /// Records a position-check fill that failed to apply, keeping its first failure time.
2975    pub fn record_unapplied_fill_report(&mut self, report: &FillReport) {
2976        let fill_key = (report.account_id, report.instrument_id, report.trade_id);
2977        if !self.fills_unapplied.contains_key(&fill_key) {
2978            self.fills_unapplied.mark(fill_key);
2979        }
2980
2981        let ttl = Duration::from_mins(self.config.position_check_lookback_mins)
2982            .max(Duration::from_mins(1));
2983        self.fills_unapplied.prune_older_than(ttl);
2984    }
2985
2986    /// Checks whether a fill has stayed unapplied past the position check threshold.
2987    ///
2988    /// Such a fill no longer holds back synthetic position reconciliation.
2989    #[must_use]
2990    pub fn is_unapplied_fill_report_expired(&self, report: &FillReport) -> bool {
2991        let fill_key = (report.account_id, report.instrument_id, report.trade_id);
2992        self.fills_unapplied.contains_key(&fill_key)
2993            && !self.fills_unapplied.within(
2994                &fill_key,
2995                Duration::from(self.config.position_check_threshold_ns),
2996            )
2997    }
2998
2999    /// Checks whether cached position fills match the report, including quantity and commission.
3000    #[must_use]
3001    pub fn position_contains_fill_report(&self, report: &FillReport) -> bool {
3002        let cache = self.cache();
3003        let positions = cache.positions_refs(
3004            None,
3005            Some(&report.instrument_id),
3006            None,
3007            Some(&report.account_id),
3008            None,
3009        );
3010        position_fill_report_matches(
3011            &cache,
3012            report,
3013            &positions,
3014            |id| cache.hedging_flip_position_ids(id),
3015            None,
3016        )
3017    }
3018
3019    /// Counts leading reports already matched by the current immutable cache snapshot.
3020    ///
3021    /// Stops at the first mismatch or account/instrument change. Reuse ends before dispatch.
3022    #[cfg(feature = "node")]
3023    pub(crate) fn position_fill_reports_contained_prefix(&self, reports: &[FillReport]) -> usize {
3024        let Some(first) = reports.first() else {
3025            return 0;
3026        };
3027
3028        if reports.len() == 1 {
3029            return usize::from(self.position_contains_fill_report(first));
3030        }
3031
3032        let cache = self.cache();
3033        let positions = cache.positions_refs(
3034            None,
3035            Some(&first.instrument_id),
3036            None,
3037            Some(&first.account_id),
3038            None,
3039        );
3040        let mut replay = IndexMap::new();
3041
3042        // Reuse is confined to this immutable borrow and ends before any fill dispatch
3043        let mut flips = IndexMap::new();
3044        reports
3045            .iter()
3046            .take_while(|report| {
3047                report.instrument_id == first.instrument_id
3048                    && report.account_id == first.account_id
3049                    && position_fill_report_matches(
3050                        &cache,
3051                        report,
3052                        &positions,
3053                        |id| {
3054                            if let Some(flip) = flips.get(&id) {
3055                                return Ok(*flip);
3056                            }
3057
3058                            let flip = cache.hedging_flip_position_ids(id)?;
3059                            flips.insert(id, flip);
3060                            Ok(flip)
3061                        },
3062                        Some(&mut replay),
3063                    )
3064            })
3065            .count()
3066    }
3067
3068    /// Reconciles cached positions against venue position reports.
3069    ///
3070    /// Callers may supply a filtered check and reports without pruning retry state for other positions.
3071    /// Global pruning is handled by [`Self::plan_position_fill_reports`] and
3072    /// [`Self::check_positions_consistency`].
3073    #[must_use]
3074    pub fn reconcile_position_reports(
3075        &mut self,
3076        check: &PositionReportCheck,
3077        reports: Vec<PositionStatusReport>,
3078        queried_clients: &IndexSet<ClientId>,
3079        failed_clients: &IndexSet<ClientId>,
3080    ) -> Vec<OrderEventAny> {
3081        log::debug!("Checking position consistency between cached-state and venues");
3082
3083        let venue_positions = self.group_position_reports(reports);
3084        let mut events = Vec::new();
3085
3086        for (key, coverage) in &check.client_coverage {
3087            let prepared_revision = check
3088                .activity_revisions
3089                .get(key)
3090                .copied()
3091                .unwrap_or_default();
3092
3093            if self.position_activity_revision(key) > prepared_revision {
3094                log::debug!(
3095                    "Deferring position reconciliation for {}/{}: local activity recorded during report request",
3096                    key.0,
3097                    key.1,
3098                );
3099                continue;
3100            }
3101
3102            let venue_reports = venue_positions
3103                .get(key)
3104                .map(Vec::as_slice)
3105                .unwrap_or_default();
3106
3107            if venue_reports.is_empty() {
3108                match coverage {
3109                    ReportClientCoverage::Resolved(responsible_clients)
3110                        if responsible_clients.is_empty() =>
3111                    {
3112                        log::warn!(
3113                            "Skipping position reconciliation for {}/{}: responsible execution client coverage is unresolved",
3114                            key.0,
3115                            key.1,
3116                        );
3117                        continue;
3118                    }
3119                    ReportClientCoverage::Resolved(responsible_clients)
3120                        if !responsible_clients.is_subset(queried_clients) =>
3121                    {
3122                        log::warn!(
3123                            "Skipping position reconciliation for {}/{}: responsible execution clients were not all queried",
3124                            key.0,
3125                            key.1,
3126                        );
3127                        continue;
3128                    }
3129                    ReportClientCoverage::Resolved(responsible_clients)
3130                        if !responsible_clients.is_disjoint(failed_clients) =>
3131                    {
3132                        let failed_responsible_clients = responsible_clients
3133                            .intersection(failed_clients)
3134                            .copied()
3135                            .collect::<IndexSet<_>>();
3136                        log::warn!(
3137                            "Skipping position reconciliation for {}/{}: failed to query responsible execution clients: {failed_responsible_clients:?}",
3138                            key.0,
3139                            key.1,
3140                        );
3141                        continue;
3142                    }
3143                    ReportClientCoverage::Resolved(_) => {}
3144                    ReportClientCoverage::Unavailable(responsible_clients) => {
3145                        log::debug!(
3146                            "Skipping position reconciliation for {}/{}: complete bulk position coverage is unavailable from responsible execution clients: {responsible_clients:?}",
3147                            key.0,
3148                            key.1,
3149                        );
3150                        continue;
3151                    }
3152                    ReportClientCoverage::Unresolved => {
3153                        log::warn!(
3154                            "Skipping position reconciliation for {}/{}: responsible execution client coverage is unresolved",
3155                            key.0,
3156                            key.1,
3157                        );
3158                        continue;
3159                    }
3160                }
3161            }
3162
3163            if let Some(discrepancy_events) = self.check_position_discrepancy(*key, venue_reports) {
3164                events.extend(discrepancy_events);
3165            }
3166        }
3167
3168        let current_position_keys = self.open_position_keys_for_reconciliation();
3169
3170        for (key, venue_reports) in &venue_positions {
3171            if check.client_coverage.contains_key(key)
3172                || venue_reports
3173                    .iter()
3174                    .all(|report| report.signed_decimal_qty == Decimal::ZERO)
3175            {
3176                continue;
3177            }
3178
3179            if current_position_keys.contains(key) {
3180                log::debug!(
3181                    "Deferring position reconciliation for {}/{}: position opened after client coverage was recorded",
3182                    key.0,
3183                    key.1,
3184                );
3185                continue;
3186            }
3187
3188            if let Some(discrepancy_events) = self.check_position_discrepancy(*key, venue_reports) {
3189                events.extend(discrepancy_events);
3190            }
3191        }
3192
3193        events
3194    }
3195
3196    fn group_position_reports(
3197        &self,
3198        reports: impl IntoIterator<Item = PositionStatusReport>,
3199    ) -> IndexMap<InstrumentAccountKey, Vec<PositionStatusReport>> {
3200        let mut venue_positions: IndexMap<InstrumentAccountKey, Vec<PositionStatusReport>> =
3201            IndexMap::new();
3202
3203        for report in reports {
3204            if !self.should_reconcile_instrument(&report.instrument_id) {
3205                continue;
3206            }
3207
3208            venue_positions
3209                .entry((report.instrument_id, report.account_id))
3210                .or_default()
3211                .push(report);
3212        }
3213
3214        venue_positions
3215            .into_iter()
3216            .map(|(key, reports)| (key, distinct_position_reports(reports)))
3217            .collect()
3218    }
3219
3220    /// Returns any external order claim for the given instrument ID.
3221    #[must_use]
3222    pub fn get_external_order_claim(&self, instrument_id: &InstrumentId) -> Option<StrategyId> {
3223        self.cache.borrow().external_order_claim(instrument_id)
3224    }
3225
3226    /// Claims external orders for a specific strategy and instrument.
3227    ///
3228    /// # Errors
3229    ///
3230    /// Returns an error if the instrument already has a registered claim.
3231    pub fn claim_external_orders(
3232        &mut self,
3233        instrument_id: InstrumentId,
3234        strategy_id: StrategyId,
3235    ) -> anyhow::Result<()> {
3236        self.cache
3237            .borrow_mut()
3238            .register_external_order_claims(strategy_id, &[instrument_id])
3239    }
3240
3241    /// Observes a local order event and updates tracking state.
3242    ///
3243    /// This is the `LiveNode` dispatch path for order events: acknowledgement
3244    /// events clear reconciliation tracking, fills record position
3245    /// activity, and every event stamps local activity. The stamp must come
3246    /// AFTER any [`Self::clear_recon_tracking`] call - that call drops the
3247    /// local-activity mark, which is the sole grace gate protecting a
3248    /// just-acknowledged order from missing-order reconciliation while the
3249    /// venue report lags.
3250    pub fn observe_order_event(&mut self, event: &OrderEventAny) {
3251        match event {
3252            OrderEventAny::Filled(fill) => {
3253                self.record_position_activity(fill.instrument_id, fill.account_id);
3254            }
3255            OrderEventAny::Accepted(_)
3256            | OrderEventAny::Rejected(_)
3257            | OrderEventAny::Canceled(_)
3258            | OrderEventAny::Expired(_)
3259            | OrderEventAny::Denied(_)
3260            | OrderEventAny::Updated(_)
3261            | OrderEventAny::ModifyRejected(_)
3262            | OrderEventAny::CancelRejected(_) => {
3263                self.clear_recon_tracking(&event.client_order_id(), true);
3264            }
3265            _ => {}
3266        }
3267
3268        self.record_local_activity(event.client_order_id());
3269    }
3270
3271    /// Observes an incoming execution report and updates tracking state.
3272    ///
3273    /// This should be called **before** the report is dispatched to the execution
3274    /// engine, so that the manager's state is current when periodic checks run.
3275    ///
3276    /// Updates performed per report variant:
3277    /// - `Order`: updates reconciliation tracking based on order status
3278    /// - `Fill`: records order activity and advances the position revision once, without marking
3279    ///   the fill as processed; continuous fill recovery checks this increment after dispatch
3280    /// - `OrderWithFills`: updates order tracking and records position activity per fill
3281    /// - `Position`: records position activity
3282    /// - `MassStatus`: no-op (handled separately via startup reconciliation)
3283    pub fn observe_execution_report(&mut self, report: &ExecutionReport) {
3284        match report {
3285            ExecutionReport::Order(order_report) => {
3286                self.observe_order_report(order_report);
3287            }
3288            ExecutionReport::Fill(fill_report) => {
3289                let client_order_id = fill_report.client_order_id.or_else(|| {
3290                    self.cache
3291                        .borrow()
3292                        .client_order_id(&fill_report.venue_order_id)
3293                        .copied()
3294                });
3295
3296                if let Some(coid) = client_order_id {
3297                    self.record_local_activity(coid);
3298                }
3299
3300                self.record_position_activity(fill_report.instrument_id, fill_report.account_id);
3301            }
3302            ExecutionReport::OrderWithFills(order_report, fills) => {
3303                self.observe_order_report(order_report);
3304
3305                for fill_report in fills {
3306                    self.record_position_activity(
3307                        fill_report.instrument_id,
3308                        fill_report.account_id,
3309                    );
3310                }
3311            }
3312            ExecutionReport::Position(position_report) => {
3313                self.record_position_activity(
3314                    position_report.instrument_id,
3315                    position_report.account_id,
3316                );
3317            }
3318            ExecutionReport::MassStatus(_) => {
3319                // Handled separately via reconcile_execution_mass_status
3320            }
3321        }
3322    }
3323
3324    fn confirm_submitted_report(&mut self, report: &OrderStatusReport) -> bool {
3325        if report.order_status == OrderStatus::Submitted
3326            && let Some(client_order_id) = report.client_order_id
3327            && self.submission_retention_enabled(client_order_id, None)
3328            && let Some(order) = self.get_order(client_order_id)
3329            && self
3330                .submissions
3331                .get(&client_order_id)
3332                .is_none_or(|submission| {
3333                    submission.trader_id == order.trader_id()
3334                        && submission.strategy_id == order.strategy_id()
3335                        && submission.instrument_id == order.instrument_id()
3336                })
3337            && report.instrument_id == order.instrument_id()
3338            && order.account_id() == Some(report.account_id)
3339            && order
3340                .venue_order_id()
3341                .is_none_or(|id| id == report.venue_order_id)
3342            && self
3343                .cache
3344                .borrow()
3345                .client_order_id(&report.venue_order_id)
3346                .is_none_or(|id| *id == client_order_id)
3347        {
3348            if Self::submission_is_unacknowledged(&order) {
3349                let client_id = self.cache.borrow().client_id(&client_order_id).copied();
3350                self.track_submission(order.init_event(), client_id);
3351            }
3352
3353            if let Some(submission) = self.submissions.get_mut(&client_order_id) {
3354                submission.venue_confirmed = true;
3355            }
3356
3357            self.clear_recon_tracking(&client_order_id, false);
3358            self.record_local_activity(client_order_id);
3359            return true;
3360        }
3361
3362        false
3363    }
3364
3365    fn observe_order_report(&mut self, report: &OrderStatusReport) {
3366        let Some(client_order_id) = report.client_order_id else {
3367            return;
3368        };
3369
3370        if self.confirm_submitted_report(report) {
3371            return;
3372        }
3373
3374        let accepted_during_pending_command = report.order_status == OrderStatus::Accepted
3375            && self.get_order(client_order_id).is_some_and(|order| {
3376                matches!(
3377                    order.status(),
3378                    OrderStatus::PendingUpdate | OrderStatus::PendingCancel
3379                )
3380            });
3381
3382        if !matches!(
3383            report.order_status,
3384            OrderStatus::PendingUpdate | OrderStatus::PendingCancel
3385        ) && !accepted_during_pending_command
3386        {
3387            self.clear_recon_tracking(&client_order_id, report.order_status.is_closed());
3388        }
3389
3390        // Dispatch may suppress a terminal report, such as a stale cancel for the
3391        // old leg of a cancel-replace. Keep the settling grace until the node
3392        // confirms the cached order closed after dispatch.
3393        self.record_local_activity(client_order_id);
3394    }
3395
3396    /// Purges closed orders from the cache that are older than the configured buffer.
3397    pub fn purge_closed_orders(&mut self) {
3398        let Some(buffer_mins) = self.config.purge_closed_orders_buffer_mins else {
3399            return;
3400        };
3401
3402        let ts_now = self.timestamp_ns();
3403        let buffer_secs = mins_to_secs(u64::from(buffer_mins));
3404
3405        self.cache
3406            .borrow_mut()
3407            .purge_closed_orders(ts_now, buffer_secs);
3408    }
3409
3410    /// Purges closed positions from the cache that are older than the configured buffer.
3411    pub fn purge_closed_positions(&mut self) {
3412        let Some(buffer_mins) = self.config.purge_closed_positions_buffer_mins else {
3413            return;
3414        };
3415
3416        let ts_now = self.clock.borrow().timestamp_ns();
3417        let buffer_secs = mins_to_secs(u64::from(buffer_mins));
3418
3419        self.cache
3420            .borrow_mut()
3421            .purge_closed_positions(ts_now, buffer_secs);
3422    }
3423
3424    /// Purges old account events from the cache based on the configured lookback.
3425    pub fn purge_account_events(&mut self) {
3426        let Some(lookback_mins) = self.config.purge_account_events_lookback_mins else {
3427            return;
3428        };
3429
3430        let ts_now = self.clock.borrow().timestamp_ns();
3431        let lookback_secs = mins_to_secs(u64::from(lookback_mins));
3432
3433        self.cache
3434            .borrow_mut()
3435            .purge_account_events(ts_now, lookback_secs);
3436    }
3437
3438    fn get_order(&self, client_order_id: ClientOrderId) -> Option<OrderAny> {
3439        self.cache().order(&client_order_id).map(|o| o.clone())
3440    }
3441
3442    fn get_order_by_venue_order_id(&self, venue_order_id: VenueOrderId) -> Option<OrderAny> {
3443        let cache = self.cache();
3444        cache
3445            .client_order_id(&venue_order_id)
3446            .and_then(|client_order_id| cache.order(client_order_id).map(|o| o.clone()))
3447    }
3448
3449    fn get_instrument(&self, instrument_id: &InstrumentId) -> Option<InstrumentAny> {
3450        self.cache().instrument(instrument_id).cloned()
3451    }
3452
3453    fn should_skip_order_report(&self, report: &OrderStatusReport) -> bool {
3454        let client_order_id = report.client_order_id.or_else(|| {
3455            self.cache
3456                .borrow()
3457                .client_order_id(&report.venue_order_id)
3458                .copied()
3459        });
3460
3461        if let Some(client_order_id) = client_order_id
3462            && self
3463                .config
3464                .filtered_client_order_ids
3465                .contains(&client_order_id)
3466        {
3467            log::debug!(
3468                "Skipping order report {client_order_id}: in filtered_client_order_ids list"
3469            );
3470            return true;
3471        }
3472
3473        if !self.should_reconcile_instrument(&report.instrument_id) {
3474            log::debug!(
3475                "Skipping order report for {}: not in reconciliation_instrument_ids",
3476                report.instrument_id
3477            );
3478            return true;
3479        }
3480
3481        false
3482    }
3483
3484    /// Checks whether the instrument passes the configured reconciliation filter.
3485    fn should_reconcile_instrument(&self, instrument_id: &InstrumentId) -> bool {
3486        self.config.reconciliation_instrument_ids.is_empty()
3487            || self
3488                .config
3489                .reconciliation_instrument_ids
3490                .contains(instrument_id)
3491    }
3492
3493    fn prepare_missing_order_query(&mut self, client_order_id: ClientOrderId) -> Option<OrderAny> {
3494        let order = self.get_order(client_order_id)?;
3495
3496        if self.submission_is_retained(client_order_id) {
3497            return None;
3498        }
3499
3500        // The order may have closed while the report request was in flight;
3501        // the check must come before the retry increment or the stale empty
3502        // response recreates tracking state that nothing prunes afterwards.
3503        if order.status().is_closed() {
3504            log::debug!(
3505                "Skipping missing-order resolution for {client_order_id}: already {}",
3506                order.status()
3507            );
3508            self.clear_recon_tracking(&client_order_id, true);
3509            return None;
3510        }
3511
3512        // Recent local activity is the real-time settling window for missing
3513        // orders. Venue/domain timestamps can be ahead of the trading clock and
3514        // must not stall reconciliation.
3515        if self.order_activity.within(
3516            &client_order_id,
3517            Duration::from(self.config.open_check_threshold_ns),
3518        ) {
3519            return None;
3520        }
3521
3522        if order.is_inflight() && Self::submission_is_unacknowledged(&order) {
3523            let client_id = self.cache.borrow().client_id(&client_order_id).copied();
3524            self.track_submission(order.init_event(), client_id);
3525        }
3526
3527        let retries = self.order_recon_retries.entry(client_order_id).or_insert(0);
3528        *retries = retries.saturating_add(1);
3529
3530        if *retries < self.config.open_check_missing_retries {
3531            log::debug!(
3532                "Order {} not found at venue, retry {}/{}",
3533                client_order_id,
3534                retries,
3535                self.config.open_check_missing_retries
3536            );
3537            return None;
3538        }
3539
3540        Some(order)
3541    }
3542
3543    fn resolve_missing_order(&mut self, client_order_id: ClientOrderId) -> Vec<OrderEventAny> {
3544        let mut events = Vec::new();
3545
3546        let Some(order) = self.get_order(client_order_id) else {
3547            return events;
3548        };
3549
3550        if order.status().is_closed() {
3551            log::debug!(
3552                "Skipping missing-order resolution for {client_order_id}: already {}",
3553                order.status()
3554            );
3555            self.clear_recon_tracking(&client_order_id, true);
3556            return events;
3557        }
3558
3559        if self.order_activity.within(
3560            &client_order_id,
3561            Duration::from(self.config.open_check_threshold_ns),
3562        ) {
3563            log::debug!(
3564                "Deferring missing-order resolution for {client_order_id}: recent local activity"
3565            );
3566            return events;
3567        }
3568
3569        let retries = self.recon_check_retry_count(&client_order_id);
3570        let ts_now = self.clock.borrow().timestamp_ns();
3571
3572        // The targeted query was scheduled only after the missing-order budget was reached.
3573        // An interleaved inflight check can subsequently overwrite the shared retry counter.
3574        let exhausted_retries = retries.max(self.config.open_check_missing_retries);
3575
3576        if self.submission_recovery_pending(client_order_id)
3577            && !self.order_inflight_checks.contains_key(&client_order_id)
3578        {
3579            self.exhaust_submission(
3580                client_order_id,
3581                SubmissionRecoverySource::MissingOrder,
3582                exhausted_retries,
3583            );
3584            return events;
3585        }
3586
3587        match order.status() {
3588            OrderStatus::Accepted | OrderStatus::Submitted => {
3589                if order.status() == OrderStatus::Submitted
3590                    && self.exhaust_submission(
3591                        client_order_id,
3592                        SubmissionRecoverySource::MissingOrder,
3593                        exhausted_retries,
3594                    )
3595                {
3596                    return events;
3597                }
3598                log::warn!(
3599                    "Order {client_order_id} not found at venue after {retries} retries and a targeted query, marking as REJECTED"
3600                );
3601
3602                if let Some(rejected) =
3603                    create_reconciliation_rejected(&order, Some("NOT_FOUND_AT_VENUE"), ts_now)
3604                {
3605                    events.push(rejected);
3606                }
3607            }
3608            OrderStatus::PartiallyFilled => {
3609                log::warn!(
3610                    "Order {client_order_id} not found at venue after {retries} retries and a targeted query, marking as CANCELED"
3611                );
3612                events.push(create_reconciliation_canceled(&order, ts_now));
3613            }
3614            OrderStatus::PendingUpdate | OrderStatus::PendingCancel => {
3615                log::debug!(
3616                    "Deferring resolution for {client_order_id}: still inflight as {}",
3617                    order.status()
3618                );
3619
3620                if self.submission_recovery_pending(client_order_id) {
3621                    return events;
3622                }
3623
3624                // Narrow tracking reset mirroring the Python engine:
3625                // zero the retry ladder and stamp the query time so the
3626                // inflight checker first observes a full threshold delay
3627                // and then retries from scratch. The order must stay
3628                // registered in `order_inflight_checks` - the inflight checker
3629                // walks that map, unlike Python which rescans cached
3630                // inflight orders every cycle - and keeps its
3631                // local-activity mark.
3632                self.order_recon_retries.shift_remove(&client_order_id);
3633                if let Some(check) = self.order_inflight_checks.get_mut(&client_order_id) {
3634                    check.retry_count = 0;
3635                    check.last_query_at = Some(dst::time::Instant::now());
3636                }
3637
3638                self.order_query_recency.mark(client_order_id);
3639                return events;
3640            }
3641            status => {
3642                log::warn!(
3643                    "Skipping missing-order resolution for {client_order_id}: unexpected status {status}"
3644                );
3645            }
3646        }
3647
3648        self.clear_recon_tracking(&client_order_id, true);
3649        events
3650    }
3651
3652    /// Collects cached and venue net, long, and short quantities for comparison.
3653    fn position_quantity_comparison(
3654        &self,
3655        key: InstrumentAccountKey,
3656        venue_reports: &[PositionStatusReport],
3657    ) -> PositionQuantityComparison {
3658        let (instrument_id, account_id) = key;
3659
3660        let cached_positions = {
3661            let cache = self.cache.borrow();
3662            cache
3663                .positions_open(None, Some(&instrument_id), None, Some(&account_id), None)
3664                .into_iter()
3665                .map(|position| (*position).clone())
3666                .collect::<Vec<_>>()
3667        };
3668
3669        let (cached_signed_qty, cached_long_qty, cached_short_qty) =
3670            position_qty_aggregates(cached_positions.iter().map(Position::signed_decimal_qty));
3671        let (venue_signed_qty, venue_long_qty, venue_short_qty) =
3672            position_qty_aggregates(venue_reports.iter().map(|report| report.signed_decimal_qty));
3673        let nonflat_count = venue_reports
3674            .iter()
3675            .filter(|report| report.signed_decimal_qty != Decimal::ZERO)
3676            .count();
3677        let venue_report = venue_reports
3678            .iter()
3679            .find(|report| report.signed_decimal_qty != Decimal::ZERO)
3680            .or_else(|| venue_reports.last())
3681            .cloned();
3682        let venue_has_side_reports = venue_reports.iter().any(PositionStatusReport::is_long)
3683            && venue_reports.iter().any(PositionStatusReport::is_short);
3684
3685        PositionQuantityComparison {
3686            cached_positions,
3687            cached_signed_qty,
3688            cached_long_qty,
3689            cached_short_qty,
3690            venue_signed_qty,
3691            venue_long_qty,
3692            venue_short_qty,
3693            nonflat_count,
3694            venue_report,
3695            venue_has_side_reports,
3696        }
3697    }
3698
3699    fn check_position_discrepancy(
3700        &mut self,
3701        key: InstrumentAccountKey,
3702        venue_reports: &[PositionStatusReport],
3703    ) -> Option<Vec<OrderEventAny>> {
3704        let (instrument_id, account_id) = key;
3705        let comparison = self.position_quantity_comparison(key, venue_reports);
3706        let tolerance = self.position_reconciliation_tolerance(account_id);
3707        let quantities_match = comparison.quantities_match(tolerance);
3708        let report_shape = comparison.report_shape();
3709        let PositionQuantityComparison {
3710            cached_positions,
3711            cached_signed_qty,
3712            cached_long_qty,
3713            cached_short_qty,
3714            venue_signed_qty,
3715            venue_long_qty,
3716            venue_short_qty,
3717            venue_report,
3718            ..
3719        } = comparison;
3720
3721        if quantities_match {
3722            self.clear_position_reconciliation(&key);
3723            return None;
3724        }
3725
3726        if !self.config.generate_missing_orders {
3727            log::debug!(
3728                "Discrepancy for {instrument_id} position when `generate_missing_orders` disabled, skipping"
3729            );
3730            return None;
3731        }
3732
3733        let ts_now = self.clock.borrow().timestamp_ns();
3734
3735        // Grace window measured on the monotonic `dst::time` clock; see `record_position_activity`
3736        if self.position_activity_is_recent(&key) {
3737            log::debug!(
3738                "Skipping position reconciliation for {instrument_id}: recent activity within threshold"
3739            );
3740            return None;
3741        }
3742
3743        let retries = self.position_reconciliation_retries(&key, report_shape);
3744
3745        if retries >= self.config.position_check_retries {
3746            return None;
3747        }
3748
3749        if report_shape == PositionReportShape::MultiLeg {
3750            let new_retries = retries + 1;
3751            self.set_position_reconciliation_retries(key, report_shape, new_retries);
3752            log::warn!(
3753                "Deferring position reconciliation for {instrument_id}/{account_id}: venue reports have ambiguous side aggregates (cached net={cached_signed_qty}, long={cached_long_qty}, short={cached_short_qty}; venue net={venue_signed_qty}, long={venue_long_qty}, short={venue_short_qty})"
3754            );
3755
3756            if new_retries >= self.config.position_check_retries {
3757                log::error!(
3758                    "Position discrepancy for {instrument_id}/{account_id} unresolved after {} attempts; no further reconciliation attempts will be made for the current report shape",
3759                    self.config.position_check_retries,
3760                );
3761            }
3762
3763            return None;
3764        }
3765
3766        log::warn!(
3767            "Position discrepancy detected for {instrument_id}: cached_signed_qty={cached_signed_qty}, venue_signed_qty={venue_signed_qty}"
3768        );
3769
3770        let Some(instrument) = self.get_instrument(&instrument_id) else {
3771            log::debug!("Cannot reconcile position for {instrument_id}: instrument not in cache");
3772            let new_retries = retries + 1;
3773            self.set_position_reconciliation_retries(key, report_shape, new_retries);
3774            if new_retries >= self.config.position_check_retries {
3775                log::error!(
3776                    "Position discrepancy for {instrument_id} unresolved after {} attempts \
3777                     (cached_qty={cached_signed_qty}, venue_qty={venue_signed_qty}); \
3778                     no further reconciliation attempts will be made for the current report shape",
3779                    self.config.position_check_retries,
3780                );
3781            }
3782
3783            return None;
3784        };
3785
3786        let cached_avg_px = position_avg_px(&cached_positions);
3787        let venue_avg_px = venue_report.as_ref().and_then(|r| r.avg_px_open);
3788        let venue_position_id = venue_report
3789            .as_ref()
3790            .and_then(|report| report.venue_position_id);
3791        let venue_ts_last = venue_report.as_ref().map_or(ts_now, |r| r.ts_last);
3792
3793        let crosses_zero = (cached_signed_qty > Decimal::ZERO && venue_signed_qty < Decimal::ZERO)
3794            || (cached_signed_qty < Decimal::ZERO && venue_signed_qty > Decimal::ZERO);
3795
3796        let result = if crosses_zero {
3797            let position_ids = match venue_position_id {
3798                Some(open_position_id) => match cached_positions.as_slice() {
3799                    [position] => Some((Some(position.id), Some(open_position_id))),
3800                    _ => {
3801                        log::warn!(
3802                            "Deferring hedge cross-zero reconciliation for {instrument_id}/{account_id}: cached and venue position identities are ambiguous"
3803                        );
3804                        None
3805                    }
3806                },
3807                None => Some((None, None)),
3808            };
3809
3810            position_ids.and_then(|(close_position_id, open_position_id)| {
3811                self.reconcile_cross_zero_position(
3812                    &instrument,
3813                    account_id,
3814                    cached_signed_qty,
3815                    cached_avg_px,
3816                    venue_signed_qty,
3817                    venue_avg_px,
3818                    close_position_id,
3819                    open_position_id,
3820                    ts_now,
3821                    venue_ts_last,
3822                )
3823            })
3824        } else {
3825            let qty_diff = venue_signed_qty - cached_signed_qty;
3826
3827            let order_side = if qty_diff > Decimal::ZERO {
3828                OrderSide::Buy
3829            } else {
3830                OrderSide::Sell
3831            };
3832
3833            let reconciliation_px = calculate_reconciliation_price(
3834                cached_signed_qty,
3835                cached_avg_px,
3836                venue_signed_qty,
3837                venue_avg_px,
3838                venue_report
3839                    .as_ref()
3840                    .map(|report| report.avg_px_open_reconciliation)
3841                    .unwrap_or_default(),
3842                venue_report
3843                    .as_ref()
3844                    .and_then(|report| report.avg_px_open_precision),
3845            );
3846
3847            reconciliation_px
3848                .or(venue_avg_px)
3849                .or(cached_avg_px)
3850                .and_then(|fill_px| {
3851                    let quantity =
3852                        Quantity::from_decimal_dp(qty_diff.abs(), instrument.size_precision())
3853                            .ok()?;
3854                    let order_report = create_position_reconciliation_report(
3855                        &instrument,
3856                        account_id,
3857                        order_side,
3858                        quantity,
3859                        fill_px,
3860                        venue_position_id,
3861                        None,
3862                        ts_now,
3863                        venue_ts_last,
3864                    );
3865
3866                    log::info!(
3867                        color = LogColor::Blue as u8;
3868                        "Generating synthetic fill for position reconciliation {instrument_id}: side={order_side:?}, qty={}, px={fill_px}", qty_diff.abs(),
3869                    );
3870
3871                    let (events, _) = self.handle_external_order(
3872                        &order_report,
3873                        account_id,
3874                        &instrument,
3875                        &[],
3876                        true,
3877                        None,
3878                        None,
3879                    );
3880                    Some(events)
3881                })
3882        };
3883
3884        // Track retries when reconciliation didn't produce events
3885        if result.as_ref().is_none_or(Vec::is_empty) {
3886            let new_retries = retries + 1;
3887            self.set_position_reconciliation_retries(key, report_shape, new_retries);
3888            if new_retries >= self.config.position_check_retries {
3889                log::error!(
3890                    "Position discrepancy for {} unresolved after {} attempts \
3891                     (cached_qty={}, venue_qty={}); \
3892                     no further reconciliation attempts will be made for the current report shape",
3893                    instrument_id,
3894                    self.config.position_check_retries,
3895                    cached_signed_qty,
3896                    venue_signed_qty,
3897                );
3898            }
3899        } else {
3900            self.clear_position_reconciliation(&key);
3901        }
3902
3903        result
3904    }
3905
3906    /// Handles position reconciliation when position flips sign, splitting into two
3907    /// fills: close existing position then open new position in opposite direction.
3908    #[expect(clippy::too_many_arguments)]
3909    fn reconcile_cross_zero_position(
3910        &self,
3911        instrument: &InstrumentAny,
3912        account_id: AccountId,
3913        cached_signed_qty: Decimal,
3914        cached_avg_px: Option<Decimal>,
3915        venue_signed_qty: Decimal,
3916        venue_avg_px: Option<Decimal>,
3917        close_position_id: Option<PositionId>,
3918        open_position_id: Option<PositionId>,
3919        ts_now: UnixNanos,
3920        venue_ts_last: UnixNanos,
3921    ) -> Option<Vec<OrderEventAny>> {
3922        let instrument_id = instrument.id();
3923
3924        log::info!(
3925            color = LogColor::Blue as u8;
3926            "Position crosses zero for {instrument_id}: cached={cached_signed_qty}, venue={venue_signed_qty}. Splitting into two fills",
3927        );
3928
3929        let close_qty = cached_signed_qty.abs();
3930
3931        let close_side = if cached_signed_qty < Decimal::ZERO {
3932            OrderSide::Buy // Close short by buying
3933        } else {
3934            OrderSide::Sell // Close long by selling
3935        };
3936
3937        let open_qty = venue_signed_qty.abs();
3938
3939        let open_side = if venue_signed_qty > Decimal::ZERO {
3940            OrderSide::Buy // Open long
3941        } else {
3942            OrderSide::Sell // Open short
3943        };
3944
3945        let Some(close_px) = cached_avg_px else {
3946            log::warn!("Cannot close position for {instrument_id}: no cached average price");
3947            return None;
3948        };
3949
3950        let open_report = match venue_avg_px {
3951            Some(open_px) => {
3952                let quantity =
3953                    Quantity::from_decimal_dp(open_qty, instrument.size_precision()).ok()?;
3954                let report = create_position_reconciliation_report(
3955                    instrument,
3956                    account_id,
3957                    open_side,
3958                    quantity,
3959                    open_px,
3960                    open_position_id,
3961                    Some("OPEN"),
3962                    ts_now,
3963                    venue_ts_last,
3964                );
3965                Some((report, open_px))
3966            }
3967            None => None,
3968        };
3969
3970        let quantity = Quantity::from_decimal_dp(close_qty, instrument.size_precision()).ok()?;
3971        let close_report = create_position_reconciliation_report(
3972            instrument,
3973            account_id,
3974            close_side,
3975            quantity,
3976            close_px,
3977            close_position_id,
3978            Some("CLOSE"),
3979            ts_now,
3980            venue_ts_last,
3981        );
3982
3983        log::info!(
3984            color = LogColor::Blue as u8;
3985            "Generating close fill for cross-zero {instrument_id}: side={close_side:?}, qty={close_qty}, px={close_px}",
3986        );
3987
3988        let (close_events, _) = self.handle_external_order(
3989            &close_report,
3990            account_id,
3991            instrument,
3992            &[],
3993            true,
3994            None,
3995            None,
3996        );
3997        let mut all_events = close_events;
3998
3999        if let Some((open_report, open_px)) = open_report {
4000            log::info!(
4001                color = LogColor::Blue as u8;
4002                "Generating open fill for cross-zero {instrument_id}: side={open_side:?}, qty={open_qty}, px={open_px}",
4003            );
4004
4005            let (open_events, _) = self.handle_external_order(
4006                &open_report,
4007                account_id,
4008                instrument,
4009                &[],
4010                true,
4011                None,
4012                None,
4013            );
4014            all_events.extend(open_events);
4015        } else {
4016            log::warn!("Cannot open new position for {instrument_id}: no venue average price");
4017        }
4018
4019        Some(all_events)
4020    }
4021
4022    fn reconcile_position_report(
4023        &self,
4024        report: &PositionStatusReport,
4025        account_id: AccountId,
4026        instruments_with_unattributed_fills: &IndexSet<InstrumentId>,
4027    ) -> Option<Vec<OrderEventAny>> {
4028        match report.venue_position_id {
4029            Some(venue_position_id) => self.reconcile_position_report_hedging(
4030                report,
4031                venue_position_id,
4032                account_id,
4033                instruments_with_unattributed_fills,
4034            ),
4035            None => self.reconcile_position_report_netting(report, account_id),
4036        }
4037    }
4038
4039    fn reconcile_position_report_hedging(
4040        &self,
4041        report: &PositionStatusReport,
4042        venue_position_id: PositionId,
4043        account_id: AccountId,
4044        instruments_with_unattributed_fills: &IndexSet<InstrumentId>,
4045    ) -> Option<Vec<OrderEventAny>> {
4046        // Skip if fills exist for this instrument but lack venue_position_id
4047        // (can't determine which hedge position they belong to)
4048        if instruments_with_unattributed_fills.contains(&report.instrument_id) {
4049            log::debug!(
4050                "Skipping hedge position {venue_position_id} reconciliation: unattributed fills in batch"
4051            );
4052            return None;
4053        }
4054
4055        log::debug!(
4056            "Reconciling HEDGE position for {}, venue_position_id={}",
4057            report.instrument_id,
4058            venue_position_id
4059        );
4060
4061        let position = {
4062            let cache = self.cache.borrow();
4063            cache.position_owned(&venue_position_id)
4064        };
4065
4066        match position {
4067            Some(position) => {
4068                let cached_signed_qty = position.signed_decimal_qty();
4069                let venue_signed_qty = report.signed_decimal_qty;
4070
4071                if cached_signed_qty == venue_signed_qty {
4072                    log::debug!(
4073                        "Hedge position {venue_position_id} matches venue: qty={cached_signed_qty}"
4074                    );
4075                    return None;
4076                }
4077
4078                if !self.config.generate_missing_orders {
4079                    log::error!(
4080                        "Cannot reconcile {} {}: position net qty {} != reported net qty {} \
4081                         and `generate_missing_orders` is disabled",
4082                        report.instrument_id,
4083                        venue_position_id,
4084                        cached_signed_qty,
4085                        venue_signed_qty
4086                    );
4087                    return None;
4088                }
4089
4090                self.reconcile_hedge_position_discrepancy(
4091                    report,
4092                    venue_position_id,
4093                    account_id,
4094                    &position,
4095                    cached_signed_qty,
4096                )
4097            }
4098            None => {
4099                if report.signed_decimal_qty == Decimal::ZERO {
4100                    return None;
4101                }
4102
4103                if !self.config.generate_missing_orders {
4104                    log::error!(
4105                        "Cannot reconcile position: {venue_position_id} not found and `generate_missing_orders` is disabled"
4106                    );
4107                    return None;
4108                }
4109
4110                self.reconcile_missing_hedge_position(report, venue_position_id, account_id)
4111            }
4112        }
4113    }
4114
4115    fn reconcile_hedge_position_discrepancy(
4116        &self,
4117        report: &PositionStatusReport,
4118        venue_position_id: PositionId,
4119        account_id: AccountId,
4120        position: &Position,
4121        cached_signed_qty: Decimal,
4122    ) -> Option<Vec<OrderEventAny>> {
4123        let instrument = self.get_instrument(&report.instrument_id)?;
4124        let venue_signed_qty = report.signed_decimal_qty;
4125
4126        let diff = (cached_signed_qty - venue_signed_qty).abs();
4127        let diff_qty = Quantity::from_decimal_dp(diff, instrument.size_precision()).ok()?;
4128
4129        if diff_qty.is_zero() {
4130            log::debug!(
4131                "Difference quantity rounds to zero for {}, skipping",
4132                instrument.id()
4133            );
4134            return None;
4135        }
4136
4137        log::warn!(
4138            "Hedge position discrepancy for {} {}: cached={}, venue={}, generating reconciliation order",
4139            report.instrument_id,
4140            venue_position_id,
4141            cached_signed_qty,
4142            venue_signed_qty
4143        );
4144
4145        let current_avg_px = if position.avg_px_open > Decimal::ZERO {
4146            Some(position.avg_px_open)
4147        } else {
4148            None
4149        };
4150
4151        self.create_position_reconciliation_order(
4152            report,
4153            account_id,
4154            &instrument,
4155            cached_signed_qty,
4156            diff_qty,
4157            current_avg_px,
4158        )
4159    }
4160
4161    fn reconcile_missing_hedge_position(
4162        &self,
4163        report: &PositionStatusReport,
4164        venue_position_id: PositionId,
4165        account_id: AccountId,
4166    ) -> Option<Vec<OrderEventAny>> {
4167        let instrument = self.get_instrument(&report.instrument_id)?;
4168        let venue_signed_qty = report.signed_decimal_qty;
4169
4170        let qty = venue_signed_qty.abs();
4171        let diff_qty = Quantity::from_decimal_dp(qty, instrument.size_precision()).ok()?;
4172
4173        if diff_qty.is_zero() {
4174            return None;
4175        }
4176
4177        log::warn!(
4178            "Missing hedge position for {} {}: venue reports {}, generating reconciliation order",
4179            report.instrument_id,
4180            venue_position_id,
4181            venue_signed_qty
4182        );
4183
4184        self.create_position_reconciliation_order(
4185            report,
4186            account_id,
4187            &instrument,
4188            Decimal::ZERO,
4189            diff_qty,
4190            None,
4191        )
4192    }
4193
4194    fn reconcile_position_report_netting(
4195        &self,
4196        report: &PositionStatusReport,
4197        account_id: AccountId,
4198    ) -> Option<Vec<OrderEventAny>> {
4199        let instrument_id = report.instrument_id;
4200
4201        log::debug!("Reconciling NET position for {instrument_id}");
4202
4203        let instrument = self.get_instrument(&instrument_id)?;
4204
4205        let (cached_signed_qty, cached_avg_px) = {
4206            let cache = self.cache.borrow();
4207            let positions =
4208                cache.positions_open(None, Some(&instrument_id), None, Some(&account_id), None);
4209            let cached_signed_qty = positions
4210                .iter()
4211                .map(|position| position.signed_decimal_qty())
4212                .sum::<Decimal>();
4213            let cached_avg_px = position_avg_px(positions.iter().map(|position| &**position));
4214
4215            (cached_signed_qty, cached_avg_px)
4216        };
4217
4218        let venue_signed_qty = report.signed_decimal_qty;
4219
4220        log::debug!("venue_signed_qty={venue_signed_qty}, cached_signed_qty={cached_signed_qty}");
4221
4222        let diff = (cached_signed_qty - venue_signed_qty).abs();
4223        let tolerance = self.position_reconciliation_tolerance(account_id);
4224        if diff <= tolerance {
4225            log::debug!("Position quantities match for {instrument_id}, no reconciliation needed");
4226            return None;
4227        }
4228
4229        if !self.config.generate_missing_orders {
4230            log::debug!(
4231                "Discrepancy for {instrument_id} position when `generate_missing_orders` disabled, skipping"
4232            );
4233            return None;
4234        }
4235
4236        let diff_qty = Quantity::from_decimal_dp(diff, instrument.size_precision()).ok()?;
4237
4238        if diff_qty.is_zero() {
4239            log::debug!(
4240                "Difference quantity rounds to zero for {instrument_id}, skipping order generation"
4241            );
4242            return None;
4243        }
4244
4245        let crosses_zero = (cached_signed_qty > Decimal::ZERO && venue_signed_qty < Decimal::ZERO)
4246            || (cached_signed_qty < Decimal::ZERO && venue_signed_qty > Decimal::ZERO);
4247
4248        if crosses_zero {
4249            let ts_now = self.clock.borrow().timestamp_ns();
4250            return self.reconcile_cross_zero_position(
4251                &instrument,
4252                account_id,
4253                cached_signed_qty,
4254                cached_avg_px,
4255                venue_signed_qty,
4256                report.avg_px_open,
4257                None,
4258                None,
4259                ts_now,
4260                report.ts_last,
4261            );
4262        }
4263
4264        if cached_signed_qty == Decimal::ZERO {
4265            return self.create_position_from_report(report, account_id, &instrument);
4266        }
4267
4268        self.create_position_reconciliation_order(
4269            report,
4270            account_id,
4271            &instrument,
4272            cached_signed_qty,
4273            diff_qty,
4274            cached_avg_px,
4275        )
4276    }
4277
4278    /// Creates a position from a venue position report when no orders/fills exist.
4279    ///
4280    /// This handles the case where the venue reports an open position but there are
4281    /// no order or fill reports to create it from (e.g., orders are already closed).
4282    fn create_position_from_report(
4283        &self,
4284        report: &PositionStatusReport,
4285        account_id: AccountId,
4286        instrument: &InstrumentAny,
4287    ) -> Option<Vec<OrderEventAny>> {
4288        let instrument_id = report.instrument_id;
4289        let venue_signed_qty = report.signed_decimal_qty;
4290
4291        if venue_signed_qty == Decimal::ZERO {
4292            return None;
4293        }
4294
4295        let order_side = if venue_signed_qty > Decimal::ZERO {
4296            OrderSide::Buy
4297        } else {
4298            OrderSide::Sell
4299        };
4300
4301        let qty_abs = venue_signed_qty.abs();
4302        let venue_avg_px = report.avg_px_open?;
4303
4304        let ts_now = self.clock.borrow().timestamp_ns();
4305        let order_qty = Quantity::from_decimal_dp(qty_abs, instrument.size_precision()).ok()?;
4306        let order_report = create_position_reconciliation_report(
4307            instrument,
4308            account_id,
4309            order_side,
4310            order_qty,
4311            venue_avg_px,
4312            report.venue_position_id,
4313            None,
4314            ts_now,
4315            report.ts_last,
4316        );
4317
4318        log::info!(
4319            color = LogColor::Blue as u8;
4320            "Creating position from venue report for {instrument_id}: side={order_side:?}, qty={qty_abs}, avg_px={venue_avg_px}",
4321        );
4322
4323        let (events, _) = self.handle_external_order(
4324            &order_report,
4325            account_id,
4326            instrument,
4327            &[],
4328            true,
4329            None,
4330            None,
4331        );
4332        Some(events)
4333    }
4334
4335    fn create_position_reconciliation_order(
4336        &self,
4337        report: &PositionStatusReport,
4338        account_id: AccountId,
4339        instrument: &InstrumentAny,
4340        cached_signed_qty: Decimal,
4341        diff_qty: Quantity,
4342        current_avg_px: Option<Decimal>,
4343    ) -> Option<Vec<OrderEventAny>> {
4344        let venue_signed_qty = report.signed_decimal_qty;
4345        let instrument_id = report.instrument_id;
4346
4347        let order_side = if venue_signed_qty > cached_signed_qty {
4348            OrderSide::Buy
4349        } else {
4350            OrderSide::Sell
4351        };
4352
4353        let reconciliation_px = calculate_reconciliation_price(
4354            cached_signed_qty,
4355            current_avg_px,
4356            venue_signed_qty,
4357            report.avg_px_open,
4358            report.avg_px_open_reconciliation,
4359            report.avg_px_open_precision,
4360        );
4361
4362        let fill_px = reconciliation_px
4363            .or(report.avg_px_open)
4364            .or(current_avg_px)?;
4365
4366        let ts_now = self.clock.borrow().timestamp_ns();
4367        let order_report = create_position_reconciliation_report(
4368            instrument,
4369            account_id,
4370            order_side,
4371            diff_qty,
4372            fill_px,
4373            report.venue_position_id,
4374            None,
4375            ts_now,
4376            report.ts_last,
4377        );
4378
4379        log::info!(
4380            color = LogColor::Blue as u8;
4381            "Generating reconciliation order for {instrument_id}: side={order_side:?}, qty={diff_qty}, px={fill_px}",
4382        );
4383
4384        let (events, _) = self.handle_external_order(
4385            &order_report,
4386            account_id,
4387            instrument,
4388            &[],
4389            true,
4390            None,
4391            None,
4392        );
4393        Some(events)
4394    }
4395
4396    fn reconcile_order_report(
4397        &self,
4398        order: &OrderAny,
4399        report: &OrderStatusReport,
4400        instrument: Option<&InstrumentAny>,
4401        commission_client: Option<&dyn ExecutionClient>,
4402    ) -> anyhow::Result<Vec<OrderEventAny>> {
4403        let has_missing_fills = terminal_report_has_missing_fills(report, order.filled_qty());
4404        anyhow::ensure!(
4405            !has_missing_fills,
4406            "terminal report for {} has unaccounted fills; waiting for fill reports",
4407            order.client_order_id(),
4408        );
4409        let ts_now = self.clock.borrow().timestamp_ns();
4410
4411        let mut events = Vec::new();
4412        let mut amended = None;
4413
4414        if order.status() == OrderStatus::PendingUpdate
4415            && matches!(
4416                report.order_status,
4417                OrderStatus::Accepted | OrderStatus::Triggered | OrderStatus::PartiallyFilled
4418            )
4419            && should_reconciliation_update(order, report)
4420        {
4421            events = generate_reconciliation_order_pre_fill_events(order, report, ts_now);
4422            let mut working = order.clone();
4423            for event in &events {
4424                working
4425                    .apply(event.clone())
4426                    .context("failed to project reconciled amendment")?;
4427            }
4428
4429            amended = Some(working);
4430        }
4431
4432        let order = amended.as_ref().unwrap_or(order);
4433        let commission = if matches!(
4434            report.order_status,
4435            OrderStatus::PartiallyFilled | OrderStatus::Filled
4436        ) && report.filled_qty > order.filled_qty()
4437            && let Some(instrument) = instrument
4438        {
4439            let fill_qty = report.filled_qty - order.filled_qty();
4440            let price_and_liquidity =
4441                incremental_inferred_fill_price_and_liquidity(order, report, instrument);
4442
4443            resolve_inferred_fill_commission(
4444                fill_qty,
4445                price_and_liquidity,
4446                instrument,
4447                commission_client,
4448            )?
4449        } else {
4450            None
4451        };
4452
4453        if matches!(
4454            report.order_status,
4455            OrderStatus::Canceled | OrderStatus::Expired
4456        ) && order.status() == OrderStatus::Filled
4457            && report.filled_qty == order.filled_qty()
4458        {
4459            return Ok(Vec::new());
4460        }
4461
4462        events.extend(reconcile_order_report_with_commission(
4463            order, report, instrument, ts_now, commission,
4464        ));
4465        Ok(events)
4466    }
4467
4468    /// Reconciles an order with its associated fills atomically.
4469    #[expect(
4470        clippy::too_many_arguments,
4471        reason = "Snapshot and continuous reports share fill projection"
4472    )]
4473    fn reconcile_order_with_fills(
4474        &mut self,
4475        is_snapshot: bool,
4476        order: &OrderAny,
4477        report: &OrderStatusReport,
4478        fills: &[&FillReport],
4479        instrument: Option<&InstrumentAny>,
4480        fill_queue: &mut ReconciliationFillQueue,
4481        commission_client: Option<&dyn ExecutionClient>,
4482    ) -> Vec<OrderEventAny> {
4483        self.confirm_submitted_report(report);
4484        let mut events = Vec::new();
4485        let mut working = order.clone();
4486        let mut sorted_fills: Vec<&FillReport> = fills.to_vec();
4487        sorted_fills.sort_by_key(|f| f.ts_event);
4488
4489        let ts_now = self.clock.borrow().timestamp_ns();
4490
4491        if matches!(
4492            report.order_status,
4493            OrderStatus::Canceled | OrderStatus::Expired
4494        ) && report.ts_triggered.is_some()
4495            && working.status() != OrderStatus::Triggered
4496            && TRIGGERABLE_ORDER_TYPES.contains(&working.order_type())
4497        {
4498            let triggered = create_reconciliation_triggered(&working, report, ts_now);
4499            if working.apply(triggered.clone()).is_ok() {
4500                events.push(triggered);
4501            }
4502        }
4503
4504        let requires_snapshot_projection = !sorted_fills.is_empty()
4505            || is_snapshot
4506                && (report.order_status == OrderStatus::Voided
4507                    || report.filled_qty < working.filled_qty()
4508                    || report.order_status == OrderStatus::Filled
4509                        && report.quantity < working.quantity());
4510        if !requires_snapshot_projection {
4511            match self.reconcile_order_report(&working, report, instrument, commission_client) {
4512                Ok(order_events) => events.extend(order_events),
4513                Err(e) => log::error!(
4514                    "Deferring order reconciliation for {}: {e}",
4515                    order.client_order_id(),
4516                ),
4517            }
4518
4519            return events;
4520        }
4521
4522        for event in generate_reconciliation_order_pre_fill_events(&working, report, ts_now) {
4523            if let Err(e) = working.apply(event.clone()) {
4524                log::warn!(
4525                    "Cannot project reconciliation event for {}: {e}",
4526                    order.client_order_id()
4527                );
4528                return events;
4529            }
4530
4531            events.push(event);
4532        }
4533
4534        if let Some(inst) = instrument {
4535            for fill in sorted_fills {
4536                let Some((event, fill_key)) =
4537                    self.create_order_fill(&working, fill, inst, &fill_queue.pending_fill_keys)
4538                else {
4539                    continue;
4540                };
4541
4542                if let Err(e) = working.apply(OrderEventAny::Filled(event.clone())) {
4543                    if self.is_fill_applied(&event, fill_key) {
4544                        self.fills_processed.mark(fill_key);
4545                        continue;
4546                    }
4547
4548                    log::warn!(
4549                        "Cannot project reconciliation fill for {}: {e}",
4550                        order.client_order_id()
4551                    );
4552                    return events;
4553                }
4554
4555                fill_queue.push(&mut events, event, fill_key);
4556            }
4557        }
4558
4559        // Continuous reports can precede streamed fills; only snapshots can reverse fills
4560        if !is_snapshot {
4561            match self.reconcile_order_report(&working, report, instrument, commission_client) {
4562                Ok(order_events) => events.extend(order_events),
4563                Err(e) => log::warn!("Deferring order reconciliation: {e}"),
4564            }
4565
4566            return events;
4567        }
4568
4569        if terminal_report_has_missing_fills(report, working.filled_qty()) {
4570            log::warn!(
4571                "Deferring terminal reconciliation for {}: fill reports are incomplete",
4572                order.client_order_id(),
4573            );
4574            return events;
4575        }
4576
4577        let commission = if report.filled_qty > working.filled_qty()
4578            && let Some(instrument) = instrument
4579        {
4580            let fill_qty = report.filled_qty - working.filled_qty();
4581
4582            let price_and_liquidity =
4583                incremental_inferred_fill_price_and_liquidity(&working, report, instrument);
4584
4585            match resolve_inferred_fill_commission(
4586                fill_qty,
4587                price_and_liquidity,
4588                instrument,
4589                commission_client,
4590            ) {
4591                Ok(commission) => commission,
4592                Err(e) => {
4593                    log::error!(
4594                        "Deferring inferred fill for {}: venue commission calculation failed: {e}",
4595                        order.client_order_id(),
4596                    );
4597                    return events;
4598                }
4599            }
4600        } else {
4601            None
4602        };
4603
4604        for event in generate_reconciliation_order_snapshot_events_with_commission(
4605            &working, report, instrument, ts_now, commission,
4606        ) {
4607            if let Err(e) = working.apply(event.clone()) {
4608                log::warn!(
4609                    "Cannot project reconciliation snapshot event for {}: {e}",
4610                    order.client_order_id()
4611                );
4612                break;
4613            }
4614
4615            events.push(event);
4616        }
4617
4618        events
4619    }
4620
4621    #[expect(clippy::too_many_arguments)]
4622    fn handle_external_order(
4623        &self,
4624        report: &OrderStatusReport,
4625        account_id: AccountId,
4626        instrument: &InstrumentAny,
4627        fills: &[&FillReport],
4628        is_synthetic: bool,
4629        fill_queue: Option<&mut ReconciliationFillQueue>,
4630        commission_client: Option<&dyn ExecutionClient>,
4631    ) -> (Vec<OrderEventAny>, Option<ExternalOrderMetadata>) {
4632        let claimed_strategy = self
4633            .cache
4634            .borrow()
4635            .external_order_claim(&report.instrument_id);
4636
4637        let (strategy_id, tags) = if let Some(claimed_strategy) = claimed_strategy {
4638            let order_id = report
4639                .client_order_id
4640                .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
4641            log::info!(
4642                color = LogColor::Blue as u8;
4643                "External order {} for {} claimed by strategy {}",
4644                order_id,
4645                report.instrument_id,
4646                claimed_strategy,
4647            );
4648
4649            // Keep the synthetic tag so the engine can recognize a position it opened
4650            (
4651                claimed_strategy,
4652                is_synthetic.then(|| vec![*TAG_RECONCILIATION]),
4653            )
4654        } else {
4655            // Unclaimed orders use EXTERNAL strategy ID with tag distinguishing source
4656            let tag = if is_synthetic {
4657                *TAG_RECONCILIATION
4658            } else {
4659                *TAG_VENUE
4660            };
4661
4662            (StrategyId::from("EXTERNAL"), Some(vec![tag]))
4663        };
4664
4665        // Filter unclaimed venue orders (but not synthetic reconciliation orders)
4666        if self.config.filter_unclaimed_external && claimed_strategy.is_none() && !is_synthetic {
4667            return (Vec::new(), None);
4668        }
4669
4670        let client_order_id = report
4671            .client_order_id
4672            .unwrap_or_else(|| ClientOrderId::from(report.venue_order_id.as_str()));
4673
4674        if !report.quantity.is_positive() {
4675            log::error!(
4676                "Skipping external order {} ({}) for {}: non-positive quantity in report {:?}",
4677                client_order_id,
4678                report.venue_order_id,
4679                report.instrument_id,
4680                report,
4681            );
4682            return (Vec::new(), None);
4683        }
4684
4685        let ts_now = self.clock.borrow().timestamp_ns();
4686
4687        let Some(order_side) = report.order_side else {
4688            log::error!(
4689                "Skipping external order {} ({}) for {}: order side is not specified",
4690                client_order_id,
4691                report.venue_order_id,
4692                report.instrument_id,
4693            );
4694            return (Vec::new(), None);
4695        };
4696
4697        let initialized = match OrderInitialized::new_checked(
4698            self.config.trader_id,
4699            strategy_id,
4700            report.instrument_id,
4701            client_order_id,
4702            order_side,
4703            report.order_type,
4704            report.quantity,
4705            report.time_in_force,
4706            report.post_only,
4707            report.reduce_only,
4708            false, // quote_quantity
4709            true,  // reconciliation
4710            UUID4::new(),
4711            ts_now,
4712            ts_now,
4713            report.price,
4714            report.activation_price,
4715            report.trigger_price,
4716            report.trigger_type,
4717            report.limit_offset,
4718            report.trailing_offset,
4719            report.trailing_offset_type,
4720            report.expire_time,
4721            report.display_qty,
4722            None, // emulation_trigger
4723            None, // trigger_instrument_id
4724            report.contingency_type,
4725            report.order_list_id,
4726            report.linked_order_ids.clone(),
4727            report.parent_order_id,
4728            None, // exec_algorithm_id
4729            None, // exec_algorithm_params
4730            None, // exec_spawn_id
4731            tags,
4732        ) {
4733            Ok(initialized) => initialized,
4734            Err(e) => {
4735                log::error!("Failed to create order from report: {e}");
4736                return (Vec::new(), None);
4737            }
4738        };
4739
4740        let initialized = OrderEventAny::Initialized(initialized);
4741
4742        let order = match OrderAny::from_events(vec![initialized.clone()]) {
4743            Ok(order) => order,
4744            Err(e) => {
4745                log::error!("Failed to create order from report: {e}");
4746                return (Vec::new(), None);
4747            }
4748        };
4749
4750        let replace_inferred_fill = !fills.is_empty()
4751            && matches!(
4752                report.order_status,
4753                OrderStatus::Accepted
4754                    | OrderStatus::Canceled
4755                    | OrderStatus::Expired
4756                    | OrderStatus::Filled
4757                    | OrderStatus::PartiallyFilled
4758                    | OrderStatus::Triggered
4759            );
4760        let mut prepared_fills = Vec::new();
4761        let mut prepared_fill_keys = fill_queue
4762            .as_deref()
4763            .map(|queue| queue.pending_fill_keys.clone())
4764            .unwrap_or_default();
4765        let mut real_fill_total = Decimal::ZERO;
4766
4767        if replace_inferred_fill {
4768            let mut sorted_fills: Vec<&FillReport> = fills.to_vec();
4769            sorted_fills.sort_by_key(|fill| fill.ts_event);
4770
4771            if fill_queue.is_none() {
4772                log::error!(
4773                    "Cannot reconcile external order {client_order_id}: fill queue is unavailable"
4774                );
4775                return (Vec::new(), None);
4776            }
4777
4778            for fill in sorted_fills {
4779                if let Some((fill_event, fill_key)) =
4780                    self.create_order_fill(&order, fill, instrument, &prepared_fill_keys)
4781                {
4782                    real_fill_total += fill.last_qty.as_decimal();
4783                    prepared_fill_keys.insert(fill_key);
4784                    prepared_fills.push((fill_event, fill_key));
4785                }
4786            }
4787        }
4788
4789        let report_filled = report.filled_qty.as_decimal();
4790
4791        let inferred_qty = if report_filled.is_zero()
4792            || matches!(
4793                report.order_status,
4794                OrderStatus::Accepted | OrderStatus::Triggered
4795            ) {
4796            None
4797        } else if replace_inferred_fill {
4798            if real_fill_total < report_filled {
4799                match Quantity::from_decimal_dp(
4800                    report_filled - real_fill_total,
4801                    instrument.size_precision(),
4802                ) {
4803                    Ok(quantity) => Some(quantity),
4804                    Err(e) => {
4805                        log::error!(
4806                            "Cannot reconcile external order {client_order_id}: residual fill quantity is invalid: {e}"
4807                        );
4808                        return (Vec::new(), None);
4809                    }
4810                }
4811            } else {
4812                None
4813            }
4814        } else if matches!(
4815            report.order_status,
4816            OrderStatus::PartiallyFilled
4817                | OrderStatus::Filled
4818                | OrderStatus::Canceled
4819                | OrderStatus::Expired
4820                | OrderStatus::Voided
4821        ) {
4822            Some(report.filled_qty)
4823        } else {
4824            None
4825        };
4826
4827        let defer_terminal = !is_synthetic
4828            && claimed_strategy.is_some()
4829            && matches!(
4830                report.order_status,
4831                OrderStatus::Canceled | OrderStatus::Expired
4832            )
4833            && inferred_qty.is_some();
4834
4835        if defer_terminal {
4836            log::warn!(
4837                "Deferring terminal reconciliation for claimed order {client_order_id}: fill reports are incomplete"
4838            );
4839
4840            if prepared_fills.is_empty() {
4841                return (Vec::new(), None);
4842            }
4843        }
4844
4845        let inferred_commission = if is_synthetic || defer_terminal {
4846            None
4847        } else if let Some(inferred_qty) = inferred_qty {
4848            let price_and_liquidity = inferred_fill_price_and_liquidity(&order, report, instrument);
4849
4850            match resolve_inferred_fill_commission(
4851                inferred_qty,
4852                price_and_liquidity,
4853                instrument,
4854                commission_client,
4855            ) {
4856                Ok(commission) => commission,
4857                Err(e) => {
4858                    log::error!(
4859                        "Deferring external order {client_order_id}: venue commission calculation failed: {e}"
4860                    );
4861                    return (Vec::new(), None);
4862                }
4863            }
4864        } else {
4865            None
4866        };
4867
4868        {
4869            let mut cache = self.cache.borrow_mut();
4870
4871            let source_client_id = if is_synthetic {
4872                None
4873            } else {
4874                commission_client.map(ExecutionClient::client_id)
4875            };
4876
4877            if let Err(e) = cache.add_order(order.clone(), None, source_client_id, false) {
4878                // Deterministic synthetic reconciliation IDs hash the same logical event
4879                // to the same client_order_id, so a restart replay can legitimately collide
4880                // with a cached order. Differentiate expected dedup from stuck state.
4881                match cache.order(&client_order_id) {
4882                    Some(existing) if is_synthetic && existing.is_closed() => {
4883                        log::debug!(
4884                            "Skipping synthetic reconciliation order {client_order_id} for {}: \
4885                             replay deduped (cached status={:?})",
4886                            report.instrument_id,
4887                            existing.status(),
4888                        );
4889                    }
4890                    Some(existing) if is_synthetic => {
4891                        log::warn!(
4892                            "Synthetic reconciliation order {client_order_id} for {} exists in \
4893                             cache in non-terminal state {:?}; fill not regenerated",
4894                            report.instrument_id,
4895                            existing.status(),
4896                        );
4897                    }
4898                    _ => {
4899                        log::error!("Failed to add external order to cache: {e}");
4900                    }
4901                }
4902
4903                return (Vec::new(), None);
4904            }
4905
4906            if let Err(e) = cache.index_venue_order_id(&client_order_id, &report.venue_order_id) {
4907                log::warn!("Failed to index venue order ID: {e}");
4908            }
4909        }
4910
4911        Self::publish_order_event(&initialized);
4912
4913        log::info!(
4914            color = LogColor::Blue as u8;
4915            "Created external order {} ({}) for {} [{}]",
4916            client_order_id,
4917            report.venue_order_id,
4918            report.instrument_id,
4919            report.order_status,
4920        );
4921
4922        let ts_now = self.clock.borrow().timestamp_ns();
4923        let mut order_events = generate_external_order_status_events_with_commission(
4924            &order,
4925            report,
4926            &account_id,
4927            instrument,
4928            ts_now,
4929            inferred_commission,
4930        );
4931
4932        if replace_inferred_fill {
4933            let terminal_event = if order_events.last().is_some_and(|event| {
4934                matches!(
4935                    event,
4936                    OrderEventAny::Canceled(_) | OrderEventAny::Expired(_),
4937                )
4938            }) {
4939                order_events.pop()
4940            } else {
4941                None
4942            };
4943
4944            if order_events
4945                .last()
4946                .is_some_and(|event| matches!(event, OrderEventAny::Filled(_)))
4947            {
4948                order_events.pop();
4949            }
4950
4951            let fill_queue =
4952                fill_queue.expect("fill queue availability was checked before cache mutation");
4953
4954            for (fill_event, fill_key) in prepared_fills {
4955                fill_queue.push(&mut order_events, fill_event, fill_key);
4956            }
4957
4958            if !defer_terminal
4959                && let Some(inferred_qty) = inferred_qty
4960                && let Some(inferred_fill) = create_inferred_fill_for_qty(
4961                    &order,
4962                    report,
4963                    &account_id,
4964                    instrument,
4965                    inferred_qty,
4966                    ts_now,
4967                    inferred_commission,
4968                )
4969            {
4970                order_events.push(inferred_fill);
4971            }
4972
4973            if !defer_terminal && let Some(event) = terminal_event {
4974                order_events.push(event);
4975            }
4976        }
4977
4978        let metadata = ExternalOrderMetadata {
4979            client_order_id,
4980            venue_order_id: report.venue_order_id,
4981            instrument_id: report.instrument_id,
4982            strategy_id,
4983            ts_init: ts_now,
4984        };
4985
4986        (order_events, Some(metadata))
4987    }
4988
4989    fn publish_order_event(event: &OrderEventAny) {
4990        let topic = switchboard::get_event_order_topic(event.strategy_id());
4991        msgbus::publish_order_event(topic, event);
4992    }
4993
4994    /// Adjusts fills for instruments with incomplete first lifecycle (partial window).
4995    ///
4996    /// When historical fills don't fully explain the current position (e.g., lookback window
4997    /// started mid-position), this creates synthetic fills to align with the venue position.
4998    fn adjust_mass_status_fills(&self, mass_status: &ExecutionMassStatus) -> AdjustedReports {
4999        let mut final_orders: IndexMap<VenueOrderId, OrderStatusReport> =
5000            mass_status.order_reports();
5001        let mut final_fills: IndexMap<VenueOrderId, Vec<FillReport>> = mass_status.fill_reports();
5002        let mut order_only_ids = IndexSet::new();
5003        let mut order_only_fill_keys = IndexSet::new();
5004
5005        final_fills.retain(|_, fills| {
5006            fills.retain(|fill| {
5007                if fill.last_qty.is_zero() {
5008                    log::warn!("Skipping zero-quantity fill report: {fill}");
5009                    return false;
5010                }
5011
5012                true
5013            });
5014
5015            !fills.is_empty()
5016        });
5017
5018        if self.config.filter_position_reports {
5019            return AdjustedReports {
5020                orders: final_orders,
5021                fills: final_fills,
5022                order_only_ids,
5023                order_only_fill_keys,
5024            };
5025        }
5026
5027        let mut instruments_to_adjust = Vec::new();
5028
5029        for (instrument_id, position_reports) in mass_status.position_reports() {
5030            if !self.should_reconcile_instrument(&instrument_id) {
5031                log::debug!(
5032                    "Skipping fill adjustment for {instrument_id}: not in reconciliation_instrument_ids"
5033                );
5034                continue;
5035            }
5036
5037            // Skip hedge mode instruments (have venue_position_id) as partial-window
5038            // adjustment assumes a single net position per instrument
5039            let is_hedge_mode = position_reports
5040                .iter()
5041                .any(|r| r.venue_position_id.is_some());
5042
5043            if is_hedge_mode {
5044                log::debug!(
5045                    "Skipping fill adjustment for {instrument_id}: hedge mode (has venue_position_id)"
5046                );
5047                continue;
5048            }
5049
5050            let has_retained_position = {
5051                let cache = self.cache.borrow();
5052                !cache
5053                    .positions_open(
5054                        None,
5055                        Some(&instrument_id),
5056                        None,
5057                        Some(&mass_status.account_id),
5058                        None,
5059                    )
5060                    .is_empty()
5061            };
5062
5063            if has_retained_position {
5064                log::debug!(
5065                    "Skipping fill adjustment for {instrument_id}: retained open position in cache"
5066                );
5067                continue;
5068            }
5069
5070            if let Some(instrument) = self.get_instrument(&instrument_id) {
5071                instruments_to_adjust.push(instrument);
5072            } else {
5073                log::debug!(
5074                    "Skipping fill adjustment for {instrument_id}: instrument not found in cache"
5075                );
5076            }
5077        }
5078
5079        if instruments_to_adjust.is_empty() {
5080            return AdjustedReports {
5081                orders: final_orders,
5082                fills: final_fills,
5083                order_only_ids,
5084                order_only_fill_keys,
5085            };
5086        }
5087
5088        log_info!(
5089            "Adjusting fills for {} instrument(s) with position reports",
5090            instruments_to_adjust.len(),
5091            color = LogColor::Blue
5092        );
5093
5094        for instrument in &instruments_to_adjust {
5095            let instrument_id = instrument.id();
5096
5097            let result = if self.config.generate_missing_orders {
5098                process_mass_status_for_reconciliation(mass_status, instrument, None)
5099            } else {
5100                process_mass_status_for_reconciliation_without_synthetic_reports(
5101                    mass_status,
5102                    instrument,
5103                    None,
5104                )
5105            };
5106
5107            match result {
5108                Ok(result) => {
5109                    order_only_ids.extend(result.order_only_ids);
5110                    order_only_fill_keys.extend(result.order_only_fill_keys);
5111                    final_orders.retain(|_, order| order.instrument_id != instrument_id);
5112                    final_fills.retain(|_, fills| {
5113                        fills
5114                            .first()
5115                            .is_none_or(|f| f.instrument_id != instrument_id)
5116                    });
5117
5118                    final_orders.extend(result.orders);
5119                    final_fills.extend(result.fills);
5120                }
5121                Err(e) => {
5122                    log::warn!("Failed to adjust fills for {instrument_id}: {e}");
5123                }
5124            }
5125        }
5126
5127        log_info!(
5128            "After adjustment: {} order(s), {} fill group(s)",
5129            final_orders.len(),
5130            final_fills.len(),
5131            color = LogColor::Blue
5132        );
5133
5134        AdjustedReports {
5135            orders: final_orders,
5136            fills: final_fills,
5137            order_only_ids,
5138            order_only_fill_keys,
5139        }
5140    }
5141
5142    fn is_fill_applied(&self, fill: &OrderFilled, fill_key: FillKey) -> bool {
5143        if fill.last_qty.is_zero() {
5144            return false;
5145        }
5146
5147        let cache = self.cache();
5148        cache
5149            .order(&fill.client_order_id)
5150            .or_else(|| {
5151                cache
5152                    .client_order_id(&fill.venue_order_id)
5153                    .and_then(|client_order_id| cache.order(client_order_id))
5154            })
5155            .is_some_and(|order| {
5156                order.account_id() == Some(fill_key.0)
5157                    && order.instrument_id() == fill_key.1
5158                    && order.trade_ids().contains(&&fill_key.2)
5159            })
5160    }
5161
5162    fn create_order_fill(
5163        &self,
5164        order: &OrderAny,
5165        fill: &FillReport,
5166        instrument: &InstrumentAny,
5167        pending_fill_keys: &IndexSet<FillKey>,
5168    ) -> Option<(OrderFilled, FillKey)> {
5169        if fill.last_qty.is_zero() {
5170            log::warn!("Skipping zero-quantity fill report: {fill}");
5171            return None;
5172        }
5173
5174        let fill_key = (fill.account_id, fill.instrument_id, fill.trade_id);
5175        if self.fills_processed.contains_key(&fill_key) || pending_fill_keys.contains(&fill_key) {
5176            return None;
5177        }
5178
5179        let order_side = order.order_side();
5180        if fill.order_side != order_side {
5181            log::warn!(
5182                "Fill side mismatch for {}: cached={:?}, venue={:?}",
5183                order.client_order_id(),
5184                order_side,
5185                fill.order_side,
5186            );
5187        }
5188
5189        let ts_now = self.clock.borrow().timestamp_ns();
5190
5191        let event = OrderFilled::new(
5192            order.trader_id(),
5193            order.strategy_id(),
5194            order.instrument_id(),
5195            order.client_order_id(),
5196            fill.venue_order_id,
5197            fill.account_id,
5198            fill.trade_id,
5199            fill.order_side,
5200            order.order_type(),
5201            fill.last_qty,
5202            fill.last_px,
5203            instrument.quote_currency(),
5204            fill.liquidity_side,
5205            fill.report_id,
5206            fill.ts_event,
5207            ts_now,
5208            true, // reconciliation
5209            fill.venue_position_id,
5210            Some(fill.commission),
5211            None,
5212        );
5213
5214        Some((event, fill_key))
5215    }
5216}
5217
5218fn position_fill_report_matches(
5219    cache: &Cache,
5220    report: &FillReport,
5221    positions: &[PositionRef<'_>],
5222    mut flip_for_order: impl FnMut(ClientOrderId) -> anyhow::Result<Option<[PositionId; 2]>>,
5223    mut replay: Option<&mut IndexMap<PositionId, IndexMap<TradeId, Vec<usize>>>>,
5224) -> bool {
5225    let client_order_id = report
5226        .client_order_id
5227        .or_else(|| cache.client_order_id(&report.venue_order_id).copied());
5228
5229    let flip = match client_order_id.map(&mut flip_for_order).transpose() {
5230        Ok(flip) => flip.flatten(),
5231        Err(_) => return false,
5232    };
5233
5234    if let (Some(ids), Some(id)) = (flip, report.venue_position_id)
5235        && !ids.contains(&id)
5236    {
5237        return false;
5238    }
5239
5240    let mut matched = false;
5241    let mut quantity = Quantity::zero(report.last_qty.precision);
5242    let mut commission = Money::zero(report.commission.currency);
5243
5244    for position in positions {
5245        if flip.map_or_else(
5246            || report.venue_position_id.is_some_and(|id| position.id != id),
5247            |ids| !ids.contains(&position.id),
5248        ) {
5249            continue;
5250        }
5251
5252        let totals = if let Some(replay) = replay.as_deref_mut() {
5253            let indexes = replay
5254                .entry(position.id)
5255                .or_insert_with(|| position_fill_report_indexes(position))
5256                .get(&report.trade_id);
5257            position_fill_report_totals(
5258                position,
5259                report,
5260                client_order_id,
5261                flip.is_some(),
5262                indexes
5263                    .into_iter()
5264                    .flatten()
5265                    .map(|&index| &position.replay_events[index]),
5266            )
5267        } else {
5268            position_fill_report_totals(
5269                position,
5270                report,
5271                client_order_id,
5272                flip.is_some(),
5273                position.replay_events.iter(),
5274            )
5275        };
5276
5277        let Some((position_quantity, position_commission)) = totals else {
5278            return false;
5279        };
5280
5281        let Some(next_quantity) = quantity.checked_add(position_quantity) else {
5282            return false;
5283        };
5284
5285        let Some(next_commission) = commission.checked_add(position_commission) else {
5286            return false;
5287        };
5288
5289        matched |= position_quantity.non_zero();
5290        quantity = next_quantity;
5291        commission = next_commission;
5292    }
5293
5294    matched && quantity == report.last_qty && commission == report.commission
5295}
5296
5297fn position_fill_report_indexes(position: &Position) -> IndexMap<TradeId, Vec<usize>> {
5298    let mut replay = IndexMap::<_, Vec<usize>>::new();
5299
5300    for (index, event) in position.replay_events.iter().enumerate() {
5301        if let PositionReplayEvent::Filled(fill) = event {
5302            replay.entry(fill.trade_id).or_default().push(index);
5303        }
5304    }
5305
5306    replay
5307}
5308
5309fn position_fill_report_totals<'a>(
5310    position: &Position,
5311    report: &FillReport,
5312    client_order_id: Option<ClientOrderId>,
5313    apply_voids: bool,
5314    replay: impl Iterator<Item = &'a PositionReplayEvent>,
5315) -> Option<(Quantity, Money)> {
5316    let mut fills = replay.filter_map(|event| match event {
5317        PositionReplayEvent::Filled(fill)
5318            if fill.position_id == Some(position.id)
5319                && fill.account_id == report.account_id
5320                && fill.instrument_id == report.instrument_id
5321                && fill.venue_order_id == report.venue_order_id
5322                && fill.trade_id == report.trade_id
5323                && fill.order_side == report.order_side
5324                && fill.last_px == report.last_px
5325                && fill.liquidity_side == report.liquidity_side
5326                && client_order_id.is_none_or(|id| fill.client_order_id == id) =>
5327        {
5328            Some(fill)
5329        }
5330        _ => None,
5331    });
5332
5333    let (mut position_quantity, mut position_commission) = fills.try_fold(
5334        (
5335            Quantity::zero(report.last_qty.precision),
5336            Money::zero(report.commission.currency),
5337        ),
5338        |(quantity, commission), fill| {
5339            let fill_commission = fill.commission?;
5340            if fill_commission.currency != report.commission.currency {
5341                return None;
5342            }
5343
5344            Some((
5345                quantity.checked_add(fill.last_qty)?,
5346                commission.checked_add(fill_commission)?,
5347            ))
5348        },
5349    )?;
5350
5351    if apply_voids
5352        && let Some(correction) = position.fill_voids.iter().rev().find(|correction| {
5353            correction.event.trade_id == report.trade_id
5354                && correction.event.venue_order_id == report.venue_order_id
5355                && client_order_id.is_none_or(|id| correction.event.client_order_id == id)
5356        })
5357    {
5358        let corrected_quantity = position_quantity.checked_sub(correction.voided_qty)?;
5359        let corrected_commission = position_commission.checked_sub(
5360            correction
5361                .commission_voided
5362                .unwrap_or(Money::zero(report.commission.currency)),
5363        )?;
5364        position_quantity = corrected_quantity;
5365        position_commission = corrected_commission;
5366    }
5367
5368    Some((position_quantity, position_commission))
5369}
5370
5371#[cfg(test)]
5372mod tests {
5373    use nautilus_common::{clock::VirtualClock, config::ConfigError};
5374    use nautilus_core::{DurationNanos, Params};
5375    use nautilus_execution::reconciliation::generate_reconciliation_order_events;
5376    use nautilus_model::{
5377        accounts::AccountAny,
5378        enums::{LiquiditySide, OmsType, OrderType, PositionSide, TimeInForce},
5379        events::order::spec::{OrderPendingCancelSpec, OrderPendingUpdateSpec, OrderUpdatedSpec},
5380        identifiers::{Symbol, Venue},
5381        instruments::{
5382            CurrencyPair, Instrument,
5383            stubs::{btcusd_bybit, crypto_perpetual_ethusdt},
5384        },
5385        orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
5386        types::{AccountBalance, Currency, MarginBalance, Money, Price, fixed::FIXED_PRECISION},
5387    };
5388    use rstest::rstest;
5389    use rust_decimal_macros::dec;
5390
5391    use super::{
5392        super::reconciliation::tests::{CommissionOutcome, CommissionStubClient},
5393        *,
5394    };
5395
5396    #[rstest]
5397    fn test_new_validates_open_check_lookback_mins_boundaries() {
5398        let create_manager = |mins| {
5399            ExecutionManager::new(
5400                Rc::new(RefCell::new(VirtualClock::new())),
5401                Rc::new(RefCell::new(Cache::default())),
5402                ExecutionManagerConfig {
5403                    open_check_lookback_mins: Some(mins),
5404                    ..Default::default()
5405                },
5406            )
5407        };
5408
5409        assert!(create_manager(307_445_734).is_ok());
5410        assert!(matches!(
5411            create_manager(307_445_735),
5412            Err(ConfigError::Range { field, .. })
5413                if field == "ExecutionManagerConfig.open_check_lookback_mins"
5414        ));
5415    }
5416
5417    #[rstest]
5418    fn test_new_validates_reconciliation_lookback_mins_boundaries() {
5419        let create_manager = |mins| {
5420            ExecutionManager::new(
5421                Rc::new(RefCell::new(VirtualClock::new())),
5422                Rc::new(RefCell::new(Cache::default())),
5423                ExecutionManagerConfig {
5424                    lookback_mins: Some(mins),
5425                    ..Default::default()
5426                },
5427            )
5428        };
5429
5430        assert!(create_manager(307_445_734_561_825_860).is_ok());
5431        assert!(matches!(
5432            create_manager(307_445_734_561_825_861),
5433            Err(ConfigError::Range { field, .. })
5434                if field == "ExecutionManagerConfig.lookback_mins"
5435        ));
5436    }
5437
5438    #[rstest]
5439    fn test_new_reports_every_invalid_lookback_field() {
5440        let error = ExecutionManager::new(
5441            Rc::new(RefCell::new(VirtualClock::new())),
5442            Rc::new(RefCell::new(Cache::default())),
5443            ExecutionManagerConfig {
5444                lookback_mins: Some(307_445_734_561_825_861),
5445                open_check_lookback_mins: Some(307_445_735),
5446                position_check_lookback_mins: 307_445_735,
5447                ..Default::default()
5448            },
5449        )
5450        .expect_err("all lookback fields are out of range");
5451
5452        let ConfigError::Multiple { errors } = error else {
5453            panic!("expected a `Multiple` error, was {error:?}");
5454        };
5455
5456        let fields = errors
5457            .iter()
5458            .map(|e| match e {
5459                ConfigError::Range { field, .. } => field.as_str(),
5460                other => panic!("expected a `Range` error, was {other:?}"),
5461            })
5462            .collect::<Vec<_>>();
5463
5464        assert_eq!(
5465            fields,
5466            [
5467                "ExecutionManagerConfig.lookback_mins",
5468                "ExecutionManagerConfig.open_check_lookback_mins",
5469                "ExecutionManagerConfig.position_check_lookback_mins",
5470            ]
5471        );
5472    }
5473
5474    struct PositionCoverageStubClient;
5475
5476    #[async_trait::async_trait(?Send)]
5477    impl ExecutionClient for PositionCoverageStubClient {
5478        fn is_connected(&self) -> bool {
5479            true
5480        }
5481
5482        fn client_id(&self) -> ClientId {
5483            ClientId::from("BYBIT")
5484        }
5485
5486        fn account_id(&self) -> AccountId {
5487            AccountId::from("TEST-001")
5488        }
5489
5490        fn venue(&self) -> Venue {
5491            Venue::from("BYBIT")
5492        }
5493
5494        fn oms_type(&self) -> OmsType {
5495            OmsType::Netting
5496        }
5497
5498        fn get_account(&self) -> Option<AccountAny> {
5499            None
5500        }
5501
5502        fn provides_bulk_position_coverage(&self, instrument_id: InstrumentId) -> bool {
5503            !instrument_id.symbol.as_str().ends_with("-SPOT")
5504        }
5505
5506        fn generate_account_state(
5507            &self,
5508            _balances: Vec<AccountBalance>,
5509            _margins: Vec<MarginBalance>,
5510            _reported: bool,
5511            _ts_event: UnixNanos,
5512            _info: Option<Params>,
5513        ) -> anyhow::Result<()> {
5514            Ok(())
5515        }
5516
5517        fn start(&mut self) -> anyhow::Result<()> {
5518            Ok(())
5519        }
5520
5521        fn stop(&mut self) -> anyhow::Result<()> {
5522            Ok(())
5523        }
5524    }
5525
5526    fn cached_commission_fixtures() -> (
5527        ExecutionManager,
5528        Rc<RefCell<Cache>>,
5529        OrderAny,
5530        OrderStatusReport,
5531        InstrumentAny,
5532    ) {
5533        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
5534        let clock = Rc::new(RefCell::new(VirtualClock::new()));
5535        let cache = Rc::new(RefCell::new(Cache::default()));
5536        cache
5537            .borrow_mut()
5538            .add_instrument(instrument.clone())
5539            .expect("instrument is cacheable");
5540        let client_order_id = ClientOrderId::from("O-COMMISSION-CACHED");
5541        let venue_order_id = VenueOrderId::from("V-COMMISSION-CACHED");
5542        insert_accepted_limit_order(
5543            &cache,
5544            client_order_id,
5545            venue_order_id,
5546            instrument.id(),
5547            ClientId::from("STUB"),
5548        );
5549        let order = cache
5550            .borrow()
5551            .order_owned(&client_order_id)
5552            .expect("accepted order is cached");
5553        let report = OrderStatusReport::new(
5554            AccountId::from("TEST-001"),
5555            instrument.id(),
5556            Some(client_order_id),
5557            venue_order_id,
5558            OrderSide::Buy.into(),
5559            OrderType::Limit,
5560            TimeInForce::Gtc,
5561            OrderStatus::Filled,
5562            Quantity::from("10.0"),
5563            Quantity::from("10.0"),
5564            UnixNanos::from(1),
5565            UnixNanos::from(1),
5566            UnixNanos::from(1),
5567            None,
5568        )
5569        .with_avg_px(dec!(100.0));
5570        let manager =
5571            ExecutionManager::new(clock, cache.clone(), ExecutionManagerConfig::default())
5572                .expect("valid config");
5573
5574        (manager, cache, order, report, instrument)
5575    }
5576
5577    fn external_report_with_partial_fill(
5578        instrument: &InstrumentAny,
5579    ) -> (OrderStatusReport, FillReport) {
5580        let account_id = AccountId::from("STUB-001");
5581        let venue_order_id = VenueOrderId::from("V-EXT-1");
5582        let report = OrderStatusReport::new(
5583            account_id,
5584            instrument.id(),
5585            None,
5586            venue_order_id,
5587            OrderSide::Buy.into(),
5588            OrderType::Limit,
5589            TimeInForce::Gtc,
5590            OrderStatus::Filled,
5591            Quantity::from("10.0"),
5592            Quantity::from("10.0"),
5593            UnixNanos::from(1),
5594            UnixNanos::from(1),
5595            UnixNanos::from(1),
5596            None,
5597        )
5598        .with_price(Price::from("100.00"))
5599        .with_avg_px(dec!(100.0));
5600
5601        let fill = FillReport::new(
5602            account_id,
5603            instrument.id(),
5604            venue_order_id,
5605            TradeId::from("T-EXT-1"),
5606            OrderSide::Buy,
5607            Quantity::from("4.0"),
5608            Price::from("100.00"),
5609            Money::new(0.1, Currency::USDT()),
5610            LiquiditySide::Taker,
5611            None,
5612            None,
5613            UnixNanos::from(1),
5614            UnixNanos::from(1),
5615            None,
5616        );
5617
5618        (report, fill)
5619    }
5620
5621    fn inferred_fills(events: &[OrderEventAny]) -> Vec<OrderFilled> {
5622        events
5623            .iter()
5624            .filter_map(|event| match event {
5625                OrderEventAny::Filled(filled) if filled.last_qty == Quantity::from("6.0") => {
5626                    Some(filled.clone())
5627                }
5628                _ => None,
5629            })
5630            .collect()
5631    }
5632
5633    #[rstest]
5634    fn test_handle_external_order_applies_venue_commission_to_inferred_fill() {
5635        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
5636        let clock = Rc::new(RefCell::new(VirtualClock::new()));
5637        let cache = Rc::new(RefCell::new(Cache::default()));
5638        cache
5639            .borrow_mut()
5640            .add_instrument(instrument.clone())
5641            .expect("instrument is cacheable");
5642        let manager = ExecutionManager::new(clock, cache, ExecutionManagerConfig::default())
5643            .expect("valid config");
5644        let (report, fill) = external_report_with_partial_fill(&instrument);
5645        let expected = Money::new(2.5, Currency::USDT());
5646        let client = CommissionStubClient::new(CommissionOutcome::Value(expected));
5647        let mut fill_queue = ReconciliationFillQueue::default();
5648
5649        let (events, _) = manager.handle_external_order(
5650            &report,
5651            AccountId::from("STUB-001"),
5652            &instrument,
5653            &[&fill],
5654            false,
5655            Some(&mut fill_queue),
5656            Some(&client),
5657        );
5658
5659        let inferred = inferred_fills(&events);
5660        assert_eq!(inferred.len(), 1, "one inferred fill covers the 6.0 gap");
5661        assert_eq!(inferred[0].commission, Some(expected));
5662    }
5663
5664    #[rstest]
5665    fn test_handle_external_order_skips_inferred_fill_when_commission_fails() {
5666        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
5667        let clock = Rc::new(RefCell::new(VirtualClock::new()));
5668        let cache = Rc::new(RefCell::new(Cache::default()));
5669        cache
5670            .borrow_mut()
5671            .add_instrument(instrument.clone())
5672            .expect("instrument is cacheable");
5673        let manager =
5674            ExecutionManager::new(clock, cache.clone(), ExecutionManagerConfig::default())
5675                .expect("valid config");
5676        let (report, fill) = external_report_with_partial_fill(&instrument);
5677        let client = CommissionStubClient::new(CommissionOutcome::Failure);
5678        let mut fill_queue = ReconciliationFillQueue::default();
5679
5680        let (events, metadata) = manager.handle_external_order(
5681            &report,
5682            AccountId::from("STUB-001"),
5683            &instrument,
5684            &[&fill],
5685            false,
5686            Some(&mut fill_queue),
5687            Some(&client),
5688        );
5689
5690        assert!(events.is_empty());
5691        assert!(metadata.is_none());
5692        assert!(fill_queue.pending_fill_keys.is_empty());
5693        assert!(
5694            cache
5695                .borrow()
5696                .order(&ClientOrderId::from(report.venue_order_id.as_str()))
5697                .is_none(),
5698            "commission failure must precede external order cache mutation"
5699        );
5700
5701        let expected = Money::new(2.5, Currency::USDT());
5702        let retry_client = CommissionStubClient::new(CommissionOutcome::Value(expected));
5703        let (retry_events, retry_metadata) = manager.handle_external_order(
5704            &report,
5705            AccountId::from("STUB-001"),
5706            &instrument,
5707            &[&fill],
5708            false,
5709            Some(&mut fill_queue),
5710            Some(&retry_client),
5711        );
5712        let inferred = inferred_fills(&retry_events);
5713
5714        assert!(retry_metadata.is_some());
5715        assert_eq!(inferred.len(), 1);
5716        assert_eq!(inferred[0].commission, Some(expected));
5717        assert_eq!(fill_queue.pending_fill_keys.len(), 1);
5718        assert!(
5719            cache
5720                .borrow()
5721                .order(&ClientOrderId::from(report.venue_order_id.as_str()))
5722                .is_some()
5723        );
5724    }
5725
5726    #[rstest]
5727    fn test_handle_external_order_without_explicit_fills_resolves_commission_before_cache() {
5728        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
5729        let clock = Rc::new(RefCell::new(VirtualClock::new()));
5730        let cache = Rc::new(RefCell::new(Cache::default()));
5731        cache
5732            .borrow_mut()
5733            .add_instrument(instrument.clone())
5734            .expect("instrument is cacheable");
5735        let manager =
5736            ExecutionManager::new(clock, cache.clone(), ExecutionManagerConfig::default())
5737                .expect("valid config");
5738        let (report, _) = external_report_with_partial_fill(&instrument);
5739        let failing_client = CommissionStubClient::new(CommissionOutcome::Failure);
5740
5741        let (failed_events, failed_metadata) = manager.handle_external_order(
5742            &report,
5743            AccountId::from("STUB-001"),
5744            &instrument,
5745            &[],
5746            false,
5747            None,
5748            Some(&failing_client),
5749        );
5750
5751        assert!(failed_events.is_empty());
5752        assert!(failed_metadata.is_none());
5753        assert!(
5754            cache
5755                .borrow()
5756                .order(&ClientOrderId::from(report.venue_order_id.as_str()))
5757                .is_none()
5758        );
5759
5760        let expected = Money::new(4.0, Currency::USDT());
5761        let retry_client = CommissionStubClient::new(CommissionOutcome::Value(expected));
5762        let (retry_events, retry_metadata) = manager.handle_external_order(
5763            &report,
5764            AccountId::from("STUB-001"),
5765            &instrument,
5766            &[],
5767            false,
5768            None,
5769            Some(&retry_client),
5770        );
5771
5772        let fills: Vec<_> = retry_events
5773            .iter()
5774            .filter_map(|event| match event {
5775                OrderEventAny::Filled(fill) => Some(fill),
5776                _ => None,
5777            })
5778            .collect();
5779
5780        assert!(retry_metadata.is_some());
5781        assert_eq!(fills.len(), 1);
5782        assert_eq!(fills[0].last_qty, Quantity::from("10.0"));
5783        assert_eq!(fills[0].commission, Some(expected));
5784    }
5785
5786    #[rstest]
5787    fn test_handle_external_order_with_no_override_emits_fill_without_commission() {
5788        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
5789        let clock = Rc::new(RefCell::new(VirtualClock::new()));
5790        let cache = Rc::new(RefCell::new(Cache::default()));
5791        cache
5792            .borrow_mut()
5793            .add_instrument(instrument.clone())
5794            .expect("instrument is cacheable");
5795        let manager = ExecutionManager::new(clock, cache, ExecutionManagerConfig::default())
5796            .expect("valid config");
5797        let (report, fill) = external_report_with_partial_fill(&instrument);
5798        let client = CommissionStubClient::new(CommissionOutcome::NoOverride);
5799        let mut fill_queue = ReconciliationFillQueue::default();
5800
5801        let (events, _) = manager.handle_external_order(
5802            &report,
5803            AccountId::from("STUB-001"),
5804            &instrument,
5805            &[&fill],
5806            false,
5807            Some(&mut fill_queue),
5808            Some(&client),
5809        );
5810
5811        let inferred = inferred_fills(&events);
5812        assert_eq!(inferred.len(), 1);
5813        assert_eq!(inferred[0].commission, None);
5814    }
5815
5816    #[rstest]
5817    #[case::filled(
5818        OrderStatus::Filled,
5819        "10.0",
5820        "6.0",
5821        Price::from_decimal_dp(dec!(33.3333333333333333), FIXED_PRECISION).unwrap(),
5822        1
5823    )]
5824    #[case::canceled(OrderStatus::Canceled, "8.0", "4.0", "20.00", 2)]
5825    #[case::expired(OrderStatus::Expired, "8.0", "4.0", "20.00", 2)]
5826    fn test_cached_reconciliation_applies_explicit_fill_and_defers_failed_residual(
5827        #[case] status: OrderStatus,
5828        #[case] filled_qty: Quantity,
5829        #[case] residual_qty: Quantity,
5830        #[case] residual_px: Price,
5831        #[case] event_count: usize,
5832    ) {
5833        let (mut manager, _cache, order, mut report, instrument) = cached_commission_fixtures();
5834        report.order_status = status;
5835        report.filled_qty = filled_qty;
5836        report.avg_px = Some(dec!(60.0));
5837
5838        let explicit_fill = FillReport::new(
5839            report.account_id,
5840            report.instrument_id,
5841            report.venue_order_id,
5842            TradeId::from("T-COMMISSION-EXPLICIT"),
5843            OrderSide::Buy,
5844            Quantity::from("4.0"),
5845            Price::from("100.0"),
5846            Money::new(0.25, Currency::USDT()),
5847            LiquiditySide::Taker,
5848            report.client_order_id,
5849            None,
5850            UnixNanos::from(1),
5851            UnixNanos::from(1),
5852            None,
5853        );
5854        let failing_client = CommissionStubClient::new(CommissionOutcome::Failure);
5855        let mut fill_queue = ReconciliationFillQueue::default();
5856
5857        let first_events = manager.reconcile_order_with_fills(
5858            true,
5859            &order,
5860            &report,
5861            &[&explicit_fill],
5862            Some(&instrument),
5863            &mut fill_queue,
5864            Some(&failing_client),
5865        );
5866        let mut working = order;
5867
5868        for event in &first_events {
5869            working
5870                .apply(event.clone())
5871                .expect("explicit fill projects cleanly");
5872        }
5873
5874        assert_eq!(first_events.len(), 1);
5875
5876        let OrderEventAny::Filled(explicit) = &first_events[0] else {
5877            panic!("expected the valid explicit fill");
5878        };
5879
5880        assert_eq!(explicit.last_qty, Quantity::from("4.0"));
5881        assert_eq!(
5882            explicit.commission,
5883            Some(Money::new(0.25, Currency::USDT()))
5884        );
5885        assert_eq!(working.status(), OrderStatus::PartiallyFilled);
5886
5887        let expected = Money::new(1.5, Currency::USDT());
5888        let retry_client = CommissionStubClient::new(CommissionOutcome::Value(expected));
5889        let mut reported_residual = explicit_fill.clone();
5890        reported_residual.trade_id = TradeId::from("T-COMMISSION-RESIDUAL");
5891        reported_residual.last_qty = residual_qty;
5892        reported_residual.last_px = residual_px;
5893        reported_residual.commission = expected;
5894
5895        let residual_reports = if status == OrderStatus::Filled {
5896            Vec::new()
5897        } else {
5898            vec![&reported_residual]
5899        };
5900
5901        let retry_events = manager.reconcile_order_with_fills(
5902            true,
5903            &working,
5904            &report,
5905            &residual_reports,
5906            Some(&instrument),
5907            &mut fill_queue,
5908            Some(&retry_client),
5909        );
5910
5911        assert_eq!(retry_events.len(), event_count);
5912
5913        let OrderEventAny::Filled(residual) = &retry_events[0] else {
5914            panic!("expected the residual fill");
5915        };
5916
5917        assert_eq!(residual.last_qty, residual_qty);
5918        assert_eq!(residual.last_px, residual_px);
5919        assert_eq!(residual.commission, Some(expected));
5920        assert_eq!(
5921            retry_client.seen(),
5922            (status == OrderStatus::Filled).then_some((
5923                residual_qty,
5924                residual.last_px,
5925                residual.liquidity_side
5926            )),
5927            "commission must use the exact price and liquidity carried by the residual fill"
5928        );
5929
5930        for event in &retry_events {
5931            working
5932                .apply(event.clone())
5933                .expect("residual precedes terminal status");
5934        }
5935
5936        retry_client.clear_seen();
5937        let replay = manager.reconcile_order_with_fills(
5938            true,
5939            &working,
5940            &report,
5941            &[],
5942            Some(&instrument),
5943            &mut fill_queue,
5944            Some(&retry_client),
5945        );
5946
5947        assert_eq!(working.status(), status);
5948        assert_eq!(working.filled_qty(), filled_qty);
5949        assert_eq!(
5950            working.commissions().get(&Currency::USDT()),
5951            Some(&Money::from("1.75 USDT"))
5952        );
5953        assert!(replay.is_empty());
5954        assert_eq!(retry_client.seen(), None);
5955    }
5956
5957    #[rstest]
5958    fn test_cached_reconciliation_preserves_explicit_fill_side() {
5959        let (mut manager, _cache, order, mut report, instrument) = cached_commission_fixtures();
5960        report.order_status = OrderStatus::PartiallyFilled;
5961        report.filled_qty = Quantity::from("4.0");
5962        let trade_id = TradeId::from("T-CONFLICTING-SIDE");
5963
5964        let explicit_fill = FillReport::new(
5965            report.account_id,
5966            report.instrument_id,
5967            report.venue_order_id,
5968            trade_id,
5969            OrderSide::Sell,
5970            Quantity::from("4.0"),
5971            Price::from("100.0"),
5972            Money::new(0.25, Currency::USDT()),
5973            LiquiditySide::Taker,
5974            report.client_order_id,
5975            None,
5976            UnixNanos::from(1),
5977            UnixNanos::from(1),
5978            None,
5979        );
5980        let mut fill_queue = ReconciliationFillQueue::default();
5981
5982        let events = manager.reconcile_order_with_fills(
5983            true,
5984            &order,
5985            &report,
5986            &[&explicit_fill],
5987            Some(&instrument),
5988            &mut fill_queue,
5989            None,
5990        );
5991
5992        assert_eq!(events.len(), 1);
5993
5994        let OrderEventAny::Filled(fill) = &events[0] else {
5995            panic!("expected the explicit fill");
5996        };
5997
5998        assert_eq!(fill.trade_id, trade_id);
5999        assert_eq!(fill.order_side, OrderSide::Sell);
6000        assert_eq!(fill.last_qty, Quantity::from("4.0"));
6001        assert_eq!(fill.last_px, Price::from("100.0"));
6002    }
6003
6004    #[rstest]
6005    fn test_continuous_report_preserves_newer_fills() {
6006        let (mut manager, _cache, mut order, mut report, instrument) = cached_commission_fixtures();
6007        let fill = TestOrderEventStubs::filled(
6008            &order,
6009            &instrument,
6010            Some(TradeId::from("T-NEWER-STREAM")),
6011            None,
6012            Some(Price::from("100.00")),
6013            Some(Quantity::from("2.0")),
6014            Some(LiquiditySide::Maker),
6015            None,
6016            None,
6017            Some(AccountId::from("TEST-001")),
6018        );
6019        order.apply(fill).unwrap();
6020        report.order_status = OrderStatus::PartiallyFilled;
6021        report.filled_qty = Quantity::from("1.0");
6022        let mut fill_queue = ReconciliationFillQueue::default();
6023
6024        let events = manager.reconcile_order_with_fills(
6025            false,
6026            &order,
6027            &report,
6028            &[],
6029            Some(&instrument),
6030            &mut fill_queue,
6031            None,
6032        );
6033
6034        assert!(events.is_empty());
6035        assert!(fill_queue.pending_fill_keys.is_empty());
6036    }
6037
6038    #[rstest]
6039    #[case::with_fills(true)]
6040    #[case::without_fills(false)]
6041    fn test_cached_snapshot_without_instrument_defers_unaccounted_fills(#[case] has_fills: bool) {
6042        let (mut manager, _cache, order, mut report, _instrument) = cached_commission_fixtures();
6043        report.order_status = OrderStatus::Canceled;
6044
6045        let explicit_fill = FillReport::new(
6046            report.account_id,
6047            report.instrument_id,
6048            report.venue_order_id,
6049            TradeId::from("T-MISSING-INSTRUMENT"),
6050            OrderSide::Buy,
6051            Quantity::from("4.0"),
6052            Price::from("100.0"),
6053            Money::new(0.25, Currency::USDT()),
6054            LiquiditySide::Taker,
6055            report.client_order_id,
6056            None,
6057            UnixNanos::from(1),
6058            UnixNanos::from(1),
6059            None,
6060        );
6061        let mut fill_queue = ReconciliationFillQueue::default();
6062
6063        let fills = if has_fills {
6064            vec![&explicit_fill]
6065        } else {
6066            Vec::new()
6067        };
6068
6069        let events = manager.reconcile_order_with_fills(
6070            true,
6071            &order,
6072            &report,
6073            &fills,
6074            None,
6075            &mut fill_queue,
6076            None,
6077        );
6078
6079        assert!(events.is_empty());
6080        assert_eq!(order.status(), OrderStatus::Accepted);
6081        assert_eq!(order.filled_qty(), Quantity::from("0.0"));
6082        assert!(fill_queue.pending_fill_keys.is_empty());
6083    }
6084
6085    #[rstest]
6086    #[case::canceled(OrderStatus::Canceled)]
6087    #[case::expired(OrderStatus::Expired)]
6088    fn test_terminal_order_report_does_not_void_cached_fills(#[case] status: OrderStatus) {
6089        let (manager, _cache, mut order, mut report, instrument) = cached_commission_fixtures();
6090        let fill = create_inferred_fill_for_qty(
6091            &order,
6092            &report,
6093            &report.account_id,
6094            &instrument,
6095            Quantity::from("4.0"),
6096            UnixNanos::from(1),
6097            None,
6098        )
6099        .unwrap();
6100        order.apply(fill).unwrap();
6101        report.order_status = status;
6102        report.filled_qty = Quantity::from("2.0");
6103        let client = CommissionStubClient::new(CommissionOutcome::Failure);
6104
6105        let events = manager
6106            .reconcile_order_report(&order, &report, Some(&instrument), Some(&client))
6107            .unwrap();
6108
6109        for event in &events {
6110            order.apply(event.clone()).unwrap();
6111        }
6112
6113        assert_eq!(events.len(), 1);
6114        assert_eq!(order.status(), status);
6115        assert_eq!(order.filled_qty(), Quantity::from("4.0"));
6116        assert_eq!(client.seen(), None);
6117    }
6118
6119    #[rstest]
6120    fn test_filled_order_ignores_superseded_cancel_report() {
6121        let (manager, cache, order, mut report, instrument) = cached_commission_fixtures();
6122        let venue_order_id = VenueOrderId::from("V-REPLACEMENT");
6123        let updated = OrderUpdatedSpec::builder()
6124            .trader_id(order.trader_id())
6125            .strategy_id(order.strategy_id())
6126            .instrument_id(order.instrument_id())
6127            .client_order_id(order.client_order_id())
6128            .account_id(report.account_id)
6129            .venue_order_id(venue_order_id)
6130            .quantity(order.quantity())
6131            .build();
6132        let order = cache
6133            .borrow_mut()
6134            .update_order(&OrderEventAny::Updated(updated))
6135            .unwrap();
6136        let mut fill_report = report.clone();
6137        fill_report.venue_order_id = venue_order_id;
6138        let commission = Money::from("1.25 USDT");
6139        let fill = create_inferred_fill_for_qty(
6140            &order,
6141            &fill_report,
6142            &report.account_id,
6143            &instrument,
6144            Quantity::from("10.0"),
6145            UnixNanos::from(2),
6146            Some(commission),
6147        )
6148        .unwrap();
6149        let order = cache.borrow_mut().update_order(&fill).unwrap();
6150        report.order_status = OrderStatus::Canceled;
6151        report.filled_qty = Quantity::from("0.0");
6152        report.avg_px = None;
6153        let client = CommissionStubClient::new(CommissionOutcome::Failure);
6154
6155        let events = manager
6156            .reconcile_order_report(&order, &report, Some(&instrument), Some(&client))
6157            .unwrap();
6158
6159        assert!(events.is_empty());
6160        assert_eq!(order.status(), OrderStatus::Filled);
6161        assert_eq!(order.venue_order_id(), Some(venue_order_id));
6162        assert_eq!(order.filled_qty(), Quantity::from("10.0"));
6163        assert_eq!(order.avg_px(), Some(dec!(100.0)));
6164        assert_eq!(
6165            order.commissions().get(&Currency::USDT()),
6166            Some(&commission)
6167        );
6168        assert_eq!(client.seen(), None);
6169    }
6170
6171    #[rstest]
6172    fn test_continuous_reconciliation_uses_source_client_and_retries_commission() {
6173        let (mut manager, _cache, order, report, _instrument) = cached_commission_fixtures();
6174        let client_id = ClientId::from("STUB");
6175
6176        let check = OpenOrderReportCheck {
6177            command: GenerateOrderStatusReports::new(
6178                UUID4::new(),
6179                UnixNanos::from(1),
6180                true,
6181                None,
6182                None,
6183                None,
6184                None,
6185                None,
6186            ),
6187            filtered_orders: vec![order],
6188            client_coverage: IndexMap::from([(
6189                report.client_order_id.unwrap(),
6190                ReportClientCoverage::Resolved(IndexSet::from([client_id])),
6191            )]),
6192        };
6193
6194        let queried_clients = IndexSet::from([client_id]);
6195        let failed_clients = IndexSet::new();
6196        let failing_client = CommissionStubClient::new(CommissionOutcome::Failure);
6197
6198        let failed = manager.reconcile_open_order_reports(
6199            &check,
6200            vec![SourcedOrderStatusReport {
6201                client_id,
6202                report: report.clone(),
6203            }],
6204            &queried_clients,
6205            &failed_clients,
6206            &[&failing_client],
6207        );
6208
6209        assert!(failed.events.is_empty());
6210
6211        let expected = Money::new(1.5, Currency::USDT());
6212        let retry_client = CommissionStubClient::new(CommissionOutcome::Value(expected));
6213        let retry = manager.reconcile_open_order_reports(
6214            &check,
6215            vec![SourcedOrderStatusReport { client_id, report }],
6216            &queried_clients,
6217            &failed_clients,
6218            &[&retry_client],
6219        );
6220
6221        assert_eq!(retry.events.len(), 1);
6222
6223        let OrderEventAny::Filled(fill) = &retry.events[0] else {
6224            panic!("expected inferred fill on valid retry");
6225        };
6226
6227        assert_eq!(fill.last_qty, Quantity::from("10.0"));
6228        assert_eq!(fill.commission, Some(expected));
6229    }
6230
6231    #[rstest]
6232    fn test_open_check_lookback_exclusion_warns_once_without_reconciliation_actions() {
6233        let client_order_id = ClientOrderId::from("O-LOOKBACK-OLD");
6234        let venue_order_id = VenueOrderId::from("V-LOOKBACK-OLD");
6235        let client_id = ClientId::from("BINANCE");
6236        let instrument_id = crypto_perpetual_ethusdt().id();
6237        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6238        let cache = Rc::new(RefCell::new(Cache::default()));
6239        insert_accepted_limit_order(
6240            &cache,
6241            client_order_id,
6242            venue_order_id,
6243            instrument_id,
6244            client_id,
6245        );
6246        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6247        let cutoff = order.ts_last().saturating_add(DurationNanos::new(1));
6248
6249        let check = OpenOrderReportCheck {
6250            command: GenerateOrderStatusReports::new(
6251                UUID4::new(),
6252                UnixNanos::from(1),
6253                false,
6254                None,
6255                Some(cutoff),
6256                None,
6257                None,
6258                None,
6259            ),
6260            filtered_orders: vec![order],
6261            client_coverage: IndexMap::from([(
6262                client_order_id,
6263                ReportClientCoverage::Resolved(IndexSet::from([client_id])),
6264            )]),
6265        };
6266
6267        let queried_clients = IndexSet::from([client_id]);
6268
6269        let mut manager = ExecutionManager::new(
6270            clock,
6271            cache.clone(),
6272            ExecutionManagerConfig {
6273                open_check_open_only: false,
6274                ..Default::default()
6275            },
6276        )
6277        .expect("valid config");
6278
6279        for _ in 0..2 {
6280            let result = manager.reconcile_open_order_reports(
6281                &check,
6282                Vec::new(),
6283                &queried_clients,
6284                &IndexSet::new(),
6285                &[],
6286            );
6287
6288            assert!(result.events.is_empty());
6289            assert!(result.targeted_queries.is_empty());
6290            assert_eq!(
6291                cache.borrow().order(&client_order_id).unwrap().status(),
6292                OrderStatus::Accepted
6293            );
6294            assert!(!manager.order_recon_retries.contains_key(&client_order_id));
6295            assert!(!manager.order_query_recency.contains_key(&client_order_id));
6296            assert!(!manager.order_query_pending.contains(&client_order_id));
6297            assert_eq!(
6298                manager.order_lookback_warnings,
6299                IndexSet::from([client_order_id])
6300            );
6301            assert_eq!(manager.order_lookback_warnings.len(), 1);
6302        }
6303    }
6304
6305    #[rstest]
6306    fn test_open_check_lookback_warning_clears_at_boundary_and_rearms() {
6307        let client_order_id = ClientOrderId::from("O-LOOKBACK-REARM");
6308        let client_id = ClientId::from("BINANCE");
6309        let cache = Rc::new(RefCell::new(Cache::default()));
6310        insert_accepted_limit_order(
6311            &cache,
6312            client_order_id,
6313            VenueOrderId::from("V-LOOKBACK-REARM"),
6314            crypto_perpetual_ethusdt().id(),
6315            client_id,
6316        );
6317        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6318        let old_cutoff = order.ts_last().saturating_add(DurationNanos::new(1));
6319
6320        let make_check = |start| OpenOrderReportCheck {
6321            command: GenerateOrderStatusReports::new(
6322                UUID4::new(),
6323                UnixNanos::from(1),
6324                false,
6325                None,
6326                Some(start),
6327                None,
6328                None,
6329                None,
6330            ),
6331            filtered_orders: vec![order.clone()],
6332            client_coverage: IndexMap::from([(
6333                client_order_id,
6334                ReportClientCoverage::Resolved(IndexSet::from([client_id])),
6335            )]),
6336        };
6337
6338        let queried_clients = IndexSet::new();
6339
6340        let mut manager = ExecutionManager::new(
6341            Rc::new(RefCell::new(VirtualClock::new())),
6342            cache,
6343            ExecutionManagerConfig {
6344                open_check_open_only: false,
6345                ..Default::default()
6346            },
6347        )
6348        .expect("valid config");
6349
6350        manager.reconcile_open_order_reports(
6351            &make_check(old_cutoff),
6352            Vec::new(),
6353            &queried_clients,
6354            &IndexSet::new(),
6355            &[],
6356        );
6357        assert!(manager.order_lookback_warnings.contains(&client_order_id));
6358
6359        let boundary = order.ts_last();
6360        let boundary_result = manager.reconcile_open_order_reports(
6361            &make_check(boundary),
6362            Vec::new(),
6363            &queried_clients,
6364            &IndexSet::new(),
6365            &[],
6366        );
6367        assert!(boundary_result.targeted_queries.is_empty());
6368        assert!(!manager.order_lookback_warnings.contains(&client_order_id));
6369        assert!(manager.order_coverage_warnings.contains(&client_order_id));
6370
6371        manager.reconcile_open_order_reports(
6372            &make_check(old_cutoff),
6373            Vec::new(),
6374            &queried_clients,
6375            &IndexSet::new(),
6376            &[],
6377        );
6378        assert_eq!(
6379            manager.order_lookback_warnings,
6380            IndexSet::from([client_order_id])
6381        );
6382    }
6383
6384    #[rstest]
6385    fn test_venue_order_id_mapped_report_clears_old_order_lookback_warning() {
6386        let client_order_id = ClientOrderId::from("O-LOOKBACK-MAPPED");
6387        let venue_order_id = VenueOrderId::from("V-LOOKBACK-MAPPED");
6388        let client_id = ClientId::from("BINANCE");
6389        let instrument_id = crypto_perpetual_ethusdt().id();
6390        let cache = Rc::new(RefCell::new(Cache::default()));
6391        insert_accepted_limit_order(
6392            &cache,
6393            client_order_id,
6394            venue_order_id,
6395            instrument_id,
6396            client_id,
6397        );
6398        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6399        let cutoff = order.ts_last().saturating_add(DurationNanos::new(1));
6400
6401        let check = OpenOrderReportCheck {
6402            command: GenerateOrderStatusReports::new(
6403                UUID4::new(),
6404                UnixNanos::from(1),
6405                false,
6406                None,
6407                Some(cutoff),
6408                None,
6409                None,
6410                None,
6411            ),
6412            filtered_orders: vec![order],
6413            client_coverage: IndexMap::from([(
6414                client_order_id,
6415                ReportClientCoverage::Resolved(IndexSet::from([client_id])),
6416            )]),
6417        };
6418
6419        let mut manager = ExecutionManager::new(
6420            Rc::new(RefCell::new(VirtualClock::new())),
6421            cache,
6422            ExecutionManagerConfig {
6423                open_check_open_only: false,
6424                ..Default::default()
6425            },
6426        )
6427        .expect("valid config");
6428
6429        manager.order_lookback_warnings.insert(client_order_id);
6430
6431        // The report carries NO client_order_id, so it resolves through the
6432        // cache's venue_order_id mapping.
6433        let report = OrderStatusReport::new(
6434            AccountId::from("TEST-001"),
6435            instrument_id,
6436            None,
6437            venue_order_id,
6438            OrderSide::Buy.into(),
6439            OrderType::Limit,
6440            TimeInForce::Gtc,
6441            OrderStatus::Accepted,
6442            Quantity::from("10.0"),
6443            Quantity::from("0.0"),
6444            UnixNanos::from(0),
6445            UnixNanos::from(0),
6446            UnixNanos::from(0),
6447            None,
6448        );
6449
6450        let result = manager.reconcile_open_order_reports(
6451            &check,
6452            vec![SourcedOrderStatusReport { client_id, report }],
6453            &IndexSet::from([client_id]),
6454            &IndexSet::new(),
6455            &[],
6456        );
6457
6458        assert!(result.targeted_queries.is_empty());
6459        assert!(!manager.order_lookback_warnings.contains(&client_order_id));
6460    }
6461
6462    #[rstest]
6463    fn test_positive_report_clears_old_order_lookback_warning_without_reinserting_it() {
6464        let client_order_id = ClientOrderId::from("O-LOOKBACK-REPORTED");
6465        let venue_order_id = VenueOrderId::from("V-LOOKBACK-REPORTED");
6466        let client_id = ClientId::from("BINANCE");
6467        let instrument_id = crypto_perpetual_ethusdt().id();
6468        let cache = Rc::new(RefCell::new(Cache::default()));
6469        insert_accepted_limit_order(
6470            &cache,
6471            client_order_id,
6472            venue_order_id,
6473            instrument_id,
6474            client_id,
6475        );
6476        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6477        let cutoff = order.ts_last().saturating_add(DurationNanos::new(1));
6478
6479        let check = OpenOrderReportCheck {
6480            command: GenerateOrderStatusReports::new(
6481                UUID4::new(),
6482                UnixNanos::from(1),
6483                false,
6484                None,
6485                Some(cutoff),
6486                None,
6487                None,
6488                None,
6489            ),
6490            filtered_orders: vec![order],
6491            client_coverage: IndexMap::from([(
6492                client_order_id,
6493                ReportClientCoverage::Resolved(IndexSet::from([client_id])),
6494            )]),
6495        };
6496
6497        let mut manager = ExecutionManager::new(
6498            Rc::new(RefCell::new(VirtualClock::new())),
6499            cache,
6500            ExecutionManagerConfig {
6501                open_check_open_only: false,
6502                ..Default::default()
6503            },
6504        )
6505        .expect("valid config");
6506
6507        manager.order_lookback_warnings.insert(client_order_id);
6508
6509        let report = OrderStatusReport::new(
6510            AccountId::from("TEST-001"),
6511            instrument_id,
6512            Some(client_order_id),
6513            venue_order_id,
6514            OrderSide::Buy.into(),
6515            OrderType::Limit,
6516            TimeInForce::Gtc,
6517            OrderStatus::Accepted,
6518            Quantity::from("10.0"),
6519            Quantity::from("0.0"),
6520            UnixNanos::from(0),
6521            UnixNanos::from(0),
6522            UnixNanos::from(0),
6523            None,
6524        );
6525
6526        let result = manager.reconcile_open_order_reports(
6527            &check,
6528            vec![SourcedOrderStatusReport { client_id, report }],
6529            &IndexSet::from([client_id]),
6530            &IndexSet::new(),
6531            &[],
6532        );
6533
6534        assert!(result.targeted_queries.is_empty());
6535        assert!(!manager.order_lookback_warnings.contains(&client_order_id));
6536    }
6537
6538    #[rstest]
6539    fn test_targeted_reconciliation_uses_source_client_and_retries_commission() {
6540        let (mut manager, _cache, _order, report, _instrument) = cached_commission_fixtures();
6541        let client_order_id = report.client_order_id.unwrap();
6542        let client_id = ClientId::from("STUB");
6543        let failing_client = CommissionStubClient::new(CommissionOutcome::Failure);
6544
6545        let failed = manager.reconcile_targeted_order_reports(
6546            vec![TargetedOrderReportResult {
6547                client_order_id,
6548                client_id: Some(client_id),
6549                report: Some(report.clone()),
6550                fills: Vec::new(),
6551                coverage_complete: true,
6552            }],
6553            &[&failing_client],
6554        );
6555
6556        assert!(failed.is_empty());
6557
6558        let expected = Money::new(1.5, Currency::USDT());
6559        let retry_client = CommissionStubClient::new(CommissionOutcome::Value(expected));
6560        let retry = manager.reconcile_targeted_order_reports(
6561            vec![TargetedOrderReportResult {
6562                client_order_id,
6563                client_id: Some(client_id),
6564                report: Some(report),
6565                fills: Vec::new(),
6566                coverage_complete: true,
6567            }],
6568            &[&retry_client],
6569        );
6570
6571        assert_eq!(retry.len(), 1);
6572
6573        let OrderEventAny::Filled(fill) = &retry[0] else {
6574            panic!("expected inferred fill on valid targeted retry");
6575        };
6576
6577        assert_eq!(fill.last_qty, Quantity::from("10.0"));
6578        assert_eq!(fill.commission, Some(expected));
6579    }
6580
6581    #[rstest]
6582    fn test_clear_recon_tracking_removes_targeted_query() {
6583        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6584        let cache = Rc::new(RefCell::new(Cache::default()));
6585        let mut manager = ExecutionManager::new(clock, cache, ExecutionManagerConfig::default())
6586            .expect("valid config");
6587        let client_order_id = ClientOrderId::from("O-TARGETED-CLEAR");
6588        manager.order_query_pending.insert(client_order_id);
6589        manager.order_lookback_warnings.insert(client_order_id);
6590
6591        manager.clear_recon_tracking(&client_order_id, true);
6592
6593        assert!(manager.order_query_pending.is_empty());
6594        assert!(manager.order_lookback_warnings.is_empty());
6595    }
6596
6597    #[rstest]
6598    fn test_submission_confirmation_skips_untracked_order_cache(
6599        #[values(false, true)] previously_registered: bool,
6600    ) {
6601        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6602        let cache = Rc::new(RefCell::new(Cache::default()));
6603        let mut manager = ExecutionManager::new(
6604            clock,
6605            cache.clone(),
6606            ExecutionManagerConfig {
6607                submission_recovery_policy: SubmissionRecoveryPolicy::RetainUnresolved,
6608                ..Default::default()
6609            },
6610        )
6611        .expect("valid config");
6612        let order = OrderTestBuilder::new(OrderType::Limit)
6613            .instrument_id(InstrumentId::from("ETHUSDT.BYBIT"))
6614            .quantity(Quantity::from("10.0"))
6615            .price(Price::from("100.0"))
6616            .build();
6617        let client_order_id = order.client_order_id();
6618        let account_id = AccountId::from("TEST-001");
6619        let submitted = TestOrderEventStubs::submitted(&order, account_id);
6620        if previously_registered {
6621            manager.register_submission(order.init_event(), None);
6622        }
6623        cache
6624            .borrow_mut()
6625            .add_order(order, None, None, false)
6626            .unwrap();
6627        let order = cache.borrow_mut().update_order(&submitted).unwrap();
6628        let accepted =
6629            TestOrderEventStubs::accepted(&order, account_id, VenueOrderId::from("V-UNTRACKED"));
6630        cache.borrow_mut().update_order(&accepted).unwrap();
6631
6632        if previously_registered {
6633            manager.confirm_submission_outcome(&client_order_id);
6634        }
6635        assert!(!manager.submissions.contains_key(&client_order_id));
6636        assert!(!manager.order_inflight_checks.contains_key(&client_order_id));
6637
6638        // Any cache access would fail while the mutable borrow is held
6639        let cached = cache.borrow_mut();
6640        manager.confirm_submission_outcome(&client_order_id);
6641
6642        assert_eq!(
6643            cached.order(&client_order_id).unwrap().status(),
6644            OrderStatus::Accepted,
6645        );
6646    }
6647
6648    #[rstest]
6649    fn test_register_inflight_skips_filtered_order() {
6650        let client_order_id = ClientOrderId::from("O-FILTERED-REGISTER");
6651        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6652        let cache = Rc::new(RefCell::new(Cache::default()));
6653
6654        let mut manager = ExecutionManager::new(
6655            clock,
6656            cache,
6657            ExecutionManagerConfig {
6658                filtered_client_order_ids: IndexSet::from([client_order_id]),
6659                ..Default::default()
6660            },
6661        )
6662        .expect("valid config");
6663
6664        manager.register_inflight(client_order_id);
6665
6666        assert!(!manager.order_inflight_checks.contains_key(&client_order_id));
6667        assert!(!manager.order_recon_retries.contains_key(&client_order_id));
6668    }
6669
6670    #[rstest]
6671    #[cfg_attr(
6672        not(all(feature = "simulation", madsim)),
6673        tokio::test(start_paused = true)
6674    )]
6675    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
6676    async fn test_inflight_check_retires_order_filtered_after_registration() {
6677        let client_order_id = ClientOrderId::from("O-FILTERED-LATE");
6678        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6679        let cache = Rc::new(RefCell::new(Cache::default()));
6680
6681        let mut manager = ExecutionManager::new(
6682            clock,
6683            cache,
6684            ExecutionManagerConfig {
6685                inflight_threshold_ms: 100,
6686                ..Default::default()
6687            },
6688        )
6689        .expect("valid config");
6690
6691        manager.register_inflight(client_order_id);
6692        manager
6693            .config
6694            .filtered_client_order_ids
6695            .insert(client_order_id);
6696        dst::time::sleep(Duration::from_millis(101)).await;
6697
6698        let first = manager.check_inflight_orders();
6699
6700        assert!(first.events.is_empty());
6701        assert!(first.queries.is_empty());
6702        assert!(!manager.order_inflight_checks.contains_key(&client_order_id));
6703        assert!(!manager.order_recon_retries.contains_key(&client_order_id));
6704
6705        dst::time::sleep(Duration::from_millis(101)).await;
6706        let second = manager.check_inflight_orders();
6707        assert!(second.events.is_empty());
6708        assert!(second.queries.is_empty());
6709        assert!(!manager.order_inflight_checks.contains_key(&client_order_id));
6710    }
6711
6712    #[rstest]
6713    #[case(false, OrderStatus::PendingUpdate, true, true, true)]
6714    #[case(false, OrderStatus::Accepted, false, true, true)]
6715    #[case(false, OrderStatus::Canceled, false, true, false)]
6716    #[case(true, OrderStatus::PendingCancel, true, true, true)]
6717    #[case(true, OrderStatus::Accepted, false, true, true)]
6718    #[case(true, OrderStatus::Filled, false, true, false)]
6719    fn test_observe_order_status_report_tracking_matrix(
6720        #[case] with_fills: bool,
6721        #[case] status: OrderStatus,
6722        #[case] expect_inflight: bool,
6723        #[case] expect_activity: bool,
6724        #[case] expect_last_query: bool,
6725    ) {
6726        let client_order_id = ClientOrderId::from("O-STATUS-MATRIX");
6727        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6728        let cache = Rc::new(RefCell::new(Cache::default()));
6729        let mut manager = ExecutionManager::new(clock, cache, ExecutionManagerConfig::default())
6730            .expect("valid config");
6731        manager.register_inflight(client_order_id);
6732        manager.order_query_recency.mark(client_order_id);
6733        manager.order_coverage_warnings.insert(client_order_id);
6734        manager.order_coverage_unresolved.insert(client_order_id);
6735        manager.order_query_pending.insert(client_order_id);
6736
6737        let order_report = OrderStatusReport::new(
6738            AccountId::from("TEST-001"),
6739            crypto_perpetual_ethusdt().id(),
6740            Some(client_order_id),
6741            VenueOrderId::from("V-STATUS-MATRIX"),
6742            OrderSide::Buy.into(),
6743            OrderType::Limit,
6744            TimeInForce::Gtc,
6745            status,
6746            Quantity::from("10.0"),
6747            Quantity::from("0.0"),
6748            UnixNanos::from(1_000),
6749            UnixNanos::from(1_000),
6750            UnixNanos::from(1_000),
6751            None,
6752        );
6753
6754        let report = if with_fills {
6755            ExecutionReport::OrderWithFills(Box::new(order_report), Vec::new())
6756        } else {
6757            ExecutionReport::Order(Box::new(order_report))
6758        };
6759
6760        manager.observe_execution_report(&report);
6761
6762        assert_eq!(
6763            manager.order_inflight_checks.contains_key(&client_order_id),
6764            expect_inflight,
6765        );
6766        assert_eq!(
6767            manager.order_recon_retries.contains_key(&client_order_id),
6768            expect_inflight,
6769        );
6770        assert_eq!(
6771            manager.order_activity.contains_key(&client_order_id),
6772            expect_activity,
6773        );
6774        assert_eq!(
6775            manager.order_query_recency.contains_key(&client_order_id),
6776            expect_last_query,
6777        );
6778        assert_eq!(
6779            manager.order_coverage_warnings.contains(&client_order_id),
6780            expect_inflight,
6781        );
6782        assert_eq!(
6783            manager.order_coverage_unresolved.contains(&client_order_id),
6784            expect_inflight,
6785        );
6786        assert_eq!(
6787            manager.order_query_pending.contains(&client_order_id),
6788            expect_inflight,
6789        );
6790    }
6791
6792    #[rstest]
6793    #[case(OrderStatus::PendingUpdate)]
6794    #[case(OrderStatus::PendingCancel)]
6795    fn test_accepted_report_during_pending_command_preserves_inflight_tracking(
6796        #[case] pending_status: OrderStatus,
6797    ) {
6798        let client_order_id = ClientOrderId::from("O-PENDING-COMMAND");
6799        let venue_order_id = VenueOrderId::from("V-PENDING-COMMAND");
6800        let account_id = AccountId::from("TEST-001");
6801        let client_id = ClientId::from("TEST");
6802        let instrument_id = crypto_perpetual_ethusdt().id();
6803        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6804        let cache = Rc::new(RefCell::new(Cache::default()));
6805        insert_accepted_limit_order(
6806            &cache,
6807            client_order_id,
6808            venue_order_id,
6809            instrument_id,
6810            client_id,
6811        );
6812
6813        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6814
6815        let event = match pending_status {
6816            OrderStatus::PendingUpdate => OrderEventAny::PendingUpdate(
6817                OrderPendingUpdateSpec::builder()
6818                    .trader_id(order.trader_id())
6819                    .strategy_id(order.strategy_id())
6820                    .instrument_id(instrument_id)
6821                    .client_order_id(client_order_id)
6822                    .account_id(account_id)
6823                    .venue_order_id(venue_order_id)
6824                    .build(),
6825            ),
6826            OrderStatus::PendingCancel => OrderEventAny::PendingCancel(
6827                OrderPendingCancelSpec::builder()
6828                    .trader_id(order.trader_id())
6829                    .strategy_id(order.strategy_id())
6830                    .instrument_id(instrument_id)
6831                    .client_order_id(client_order_id)
6832                    .account_id(account_id)
6833                    .venue_order_id(venue_order_id)
6834                    .build(),
6835            ),
6836            _ => unreachable!(),
6837        };
6838
6839        cache.borrow_mut().update_order(&event).unwrap();
6840
6841        let mut manager =
6842            ExecutionManager::new(clock, cache.clone(), ExecutionManagerConfig::default())
6843                .expect("valid config");
6844        manager.register_inflight(client_order_id);
6845        manager.order_query_recency.mark(client_order_id);
6846        manager.order_coverage_warnings.insert(client_order_id);
6847        manager.order_coverage_unresolved.insert(client_order_id);
6848        manager.order_query_pending.insert(client_order_id);
6849        let report = OrderStatusReport::new(
6850            account_id,
6851            instrument_id,
6852            Some(client_order_id),
6853            venue_order_id,
6854            OrderSide::Buy.into(),
6855            OrderType::Limit,
6856            TimeInForce::Gtc,
6857            OrderStatus::Accepted,
6858            Quantity::from("10.0"),
6859            Quantity::from("0.0"),
6860            UnixNanos::from(1_000),
6861            UnixNanos::from(1_000),
6862            UnixNanos::from(1_000),
6863            None,
6864        )
6865        .with_price(Price::from("100.0"));
6866
6867        manager.observe_execution_report(&ExecutionReport::Order(Box::new(report.clone())));
6868        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6869        let events =
6870            generate_reconciliation_order_events(&order, &report, None, UnixNanos::from(1_000));
6871
6872        assert!(events.is_empty());
6873        assert_eq!(order.status(), pending_status);
6874        assert!(manager.order_inflight_checks.contains_key(&client_order_id));
6875        assert!(manager.order_recon_retries.contains_key(&client_order_id));
6876        assert!(manager.order_query_recency.contains_key(&client_order_id));
6877        assert!(manager.order_activity.contains_key(&client_order_id));
6878        assert!(manager.order_coverage_warnings.contains(&client_order_id));
6879        assert!(manager.order_coverage_unresolved.contains(&client_order_id));
6880        assert!(manager.order_query_pending.contains(&client_order_id));
6881    }
6882
6883    #[rstest]
6884    fn test_superseded_cancel_report_preserves_missing_order_grace() {
6885        let client_order_id = ClientOrderId::from("O-CANCEL-REPLACE");
6886        let old_venue_order_id = VenueOrderId::from("V-CANCEL-REPLACE-OLD");
6887        let new_venue_order_id = VenueOrderId::from("V-CANCEL-REPLACE-NEW");
6888        let account_id = AccountId::from("TEST-001");
6889        let client_id = ClientId::from("TEST");
6890        let instrument_id = crypto_perpetual_ethusdt().id();
6891        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6892        let cache = Rc::new(RefCell::new(Cache::default()));
6893        insert_accepted_limit_order(
6894            &cache,
6895            client_order_id,
6896            old_venue_order_id,
6897            instrument_id,
6898            client_id,
6899        );
6900
6901        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6902        let pending_update = OrderPendingUpdateSpec::builder()
6903            .trader_id(order.trader_id())
6904            .strategy_id(order.strategy_id())
6905            .instrument_id(order.instrument_id())
6906            .client_order_id(client_order_id)
6907            .account_id(account_id)
6908            .venue_order_id(old_venue_order_id)
6909            .build();
6910        cache
6911            .borrow_mut()
6912            .update_order(&OrderEventAny::PendingUpdate(pending_update))
6913            .unwrap();
6914        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6915        let updated = OrderUpdatedSpec::builder()
6916            .trader_id(order.trader_id())
6917            .strategy_id(order.strategy_id())
6918            .instrument_id(order.instrument_id())
6919            .client_order_id(client_order_id)
6920            .quantity(order.quantity())
6921            .venue_order_id(new_venue_order_id)
6922            .account_id(account_id)
6923            .build();
6924        cache
6925            .borrow_mut()
6926            .update_order(&OrderEventAny::Updated(updated))
6927            .unwrap();
6928
6929        let mut manager = ExecutionManager::new(
6930            clock,
6931            cache.clone(),
6932            ExecutionManagerConfig {
6933                open_check_missing_retries: 1,
6934                ..Default::default()
6935            },
6936        )
6937        .expect("valid config");
6938
6939        manager.record_local_activity(client_order_id);
6940        assert!(
6941            manager
6942                .prepare_missing_order_query(client_order_id)
6943                .is_none()
6944        );
6945
6946        let report = OrderStatusReport::new(
6947            account_id,
6948            instrument_id,
6949            Some(client_order_id),
6950            old_venue_order_id,
6951            OrderSide::Buy.into(),
6952            OrderType::Limit,
6953            TimeInForce::Gtc,
6954            OrderStatus::Canceled,
6955            Quantity::from("10.0"),
6956            Quantity::from("0.0"),
6957            UnixNanos::from(1_000),
6958            UnixNanos::from(2_000),
6959            UnixNanos::from(3_000),
6960            None,
6961        );
6962
6963        manager.observe_execution_report(&ExecutionReport::Order(Box::new(report.clone())));
6964        let order = cache.borrow().order_owned(&client_order_id).unwrap();
6965        let events =
6966            generate_reconciliation_order_events(&order, &report, None, UnixNanos::from(1_000));
6967
6968        assert!(events.is_empty());
6969        assert_eq!(order.status(), OrderStatus::Accepted);
6970        assert_eq!(order.venue_order_id(), Some(new_venue_order_id));
6971        assert!(manager.order_activity.contains_key(&client_order_id));
6972        assert!(
6973            manager
6974                .prepare_missing_order_query(client_order_id)
6975                .is_none()
6976        );
6977        assert_eq!(manager.recon_check_retry_count(&client_order_id), 0);
6978    }
6979
6980    #[rstest]
6981    #[cfg_attr(
6982        not(all(feature = "simulation", madsim)),
6983        tokio::test(start_paused = true)
6984    )]
6985    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
6986    async fn test_prune_order_local_activity_uses_open_check_threshold() {
6987        let old_id = ClientOrderId::from("O-ACTIVITY-OLD");
6988        let fresh_id = ClientOrderId::from("O-ACTIVITY-FRESH");
6989        let clock = Rc::new(RefCell::new(VirtualClock::new()));
6990        let cache = Rc::new(RefCell::new(Cache::default()));
6991
6992        let mut manager = ExecutionManager::new(
6993            clock,
6994            cache,
6995            ExecutionManagerConfig {
6996                open_check_threshold_ns: DurationNanos::from_millis(100),
6997                ..Default::default()
6998            },
6999        )
7000        .expect("valid config");
7001
7002        manager.record_local_activity(old_id);
7003        dst::time::sleep(Duration::from_millis(101)).await;
7004        manager.record_local_activity(fresh_id);
7005
7006        manager.prune_order_local_activity();
7007
7008        assert!(!manager.order_activity.contains_key(&old_id));
7009        assert!(manager.order_activity.contains_key(&fresh_id));
7010    }
7011
7012    #[rstest]
7013    #[cfg_attr(
7014        not(all(feature = "simulation", madsim)),
7015        tokio::test(start_paused = true)
7016    )]
7017    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
7018    async fn test_unapplied_fill_report_expires_after_position_check_threshold() {
7019        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7020        let cache = Rc::new(RefCell::new(Cache::default()));
7021
7022        let mut manager = ExecutionManager::new(
7023            clock,
7024            cache,
7025            ExecutionManagerConfig {
7026                position_check_threshold_ns: DurationNanos::from_millis(100),
7027                ..Default::default()
7028            },
7029        )
7030        .expect("valid config");
7031
7032        let report = unapplied_fill_report("T-UNAPPLIED");
7033        let mut expired = vec![manager.is_unapplied_fill_report_expired(&report)];
7034
7035        manager.record_unapplied_fill_report(&report);
7036        dst::time::sleep(Duration::from_millis(60)).await;
7037        manager.record_unapplied_fill_report(&report);
7038        expired.push(manager.is_unapplied_fill_report_expired(&report));
7039        dst::time::sleep(Duration::from_millis(40)).await;
7040        expired.push(manager.is_unapplied_fill_report_expired(&report));
7041
7042        assert_eq!(expired, vec![false, false, true]);
7043    }
7044
7045    #[rstest]
7046    #[cfg_attr(
7047        not(all(feature = "simulation", madsim)),
7048        tokio::test(start_paused = true)
7049    )]
7050    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
7051    async fn test_record_unapplied_fill_report_prunes_outside_position_check_lookback() {
7052        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7053        let cache = Rc::new(RefCell::new(Cache::default()));
7054
7055        let mut manager = ExecutionManager::new(
7056            clock,
7057            cache,
7058            ExecutionManagerConfig {
7059                position_check_lookback_mins: 1,
7060                position_check_threshold_ns: DurationNanos::from_secs(5),
7061                ..Default::default()
7062            },
7063        )
7064        .expect("valid config");
7065
7066        let old_report = unapplied_fill_report("T-UNAPPLIED-OLD");
7067        let recent_report = unapplied_fill_report("T-UNAPPLIED-RECENT");
7068        let fresh_report = unapplied_fill_report("T-UNAPPLIED-FRESH");
7069
7070        manager.record_unapplied_fill_report(&old_report);
7071        dst::time::sleep(Duration::from_secs(30)).await;
7072        manager.record_unapplied_fill_report(&recent_report);
7073        dst::time::sleep(Duration::from_secs(31)).await;
7074        manager.record_unapplied_fill_report(&fresh_report);
7075
7076        assert!(!manager.fills_unapplied.contains_key(&(
7077            old_report.account_id,
7078            old_report.instrument_id,
7079            old_report.trade_id,
7080        )));
7081        assert!(manager.fills_unapplied.contains_key(&(
7082            recent_report.account_id,
7083            recent_report.instrument_id,
7084            recent_report.trade_id,
7085        )));
7086        assert!(manager.fills_unapplied.contains_key(&(
7087            fresh_report.account_id,
7088            fresh_report.instrument_id,
7089            fresh_report.trade_id,
7090        )));
7091    }
7092
7093    fn unapplied_fill_report(trade_id: &str) -> FillReport {
7094        FillReport::new(
7095            AccountId::from("SIM-001"),
7096            InstrumentId::from("ETHUSDT-PERP.BINANCE"),
7097            VenueOrderId::from("V-UNAPPLIED"),
7098            TradeId::from(trade_id),
7099            OrderSide::Buy,
7100            Quantity::from("1.0"),
7101            Price::from("100.0"),
7102            Money::from("0.10 USDT"),
7103            LiquiditySide::Taker,
7104            None,
7105            None,
7106            UnixNanos::from(1),
7107            UnixNanos::from(1),
7108            None,
7109        )
7110    }
7111
7112    #[rstest]
7113    fn test_prepare_open_order_report_check_builds_bulk_command_with_config() {
7114        let lookback_mins = 5_u64;
7115        let lookback = DurationNanos::from_mins(lookback_mins);
7116        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7117        let cache = Rc::new(RefCell::new(Cache::default()));
7118
7119        let mut manager = ExecutionManager::new(
7120            clock.clone(),
7121            cache.clone(),
7122            ExecutionManagerConfig {
7123                open_check_lookback_mins: Some(lookback_mins),
7124                open_check_open_only: false,
7125                reconciliation_instrument_ids: IndexSet::from([crypto_perpetual_ethusdt().id()]),
7126                ..Default::default()
7127            },
7128        )
7129        .expect("valid config");
7130
7131        let included_id = ClientOrderId::from("O-REPORT-001");
7132        let excluded_id = ClientOrderId::from("O-REPORT-002");
7133        let included_instrument_id = crypto_perpetual_ethusdt().id();
7134        let excluded_instrument_id = btcusd_bybit().id();
7135
7136        cache
7137            .borrow_mut()
7138            .add_instrument(InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()))
7139            .unwrap();
7140        cache
7141            .borrow_mut()
7142            .add_instrument(InstrumentAny::CryptoPerpetual(btcusd_bybit()))
7143            .unwrap();
7144        insert_accepted_limit_order(
7145            &cache,
7146            included_id,
7147            VenueOrderId::from("V-REPORT-001"),
7148            included_instrument_id,
7149            ClientId::from("BINANCE"),
7150        );
7151        insert_accepted_limit_order(
7152            &cache,
7153            excluded_id,
7154            VenueOrderId::from("V-REPORT-002"),
7155            excluded_instrument_id,
7156            ClientId::from("BYBIT"),
7157        );
7158        clock
7159            .borrow_mut()
7160            .advance_time(UnixNanos::default().saturating_add(lookback * 2), true);
7161
7162        let ts_now = clock.borrow().timestamp_ns();
7163        let command_id = UUID4::new();
7164        let check = manager.prepare_open_order_report_check(command_id, &[]);
7165
7166        assert_eq!(check.command.command_id, command_id);
7167        assert_eq!(check.command.ts_init, ts_now);
7168        assert!(!check.command.open_only);
7169        assert_eq!(check.command.instrument_id, None);
7170        assert_eq!(check.command.start, Some(ts_now.saturating_sub(lookback)));
7171        assert_eq!(check.command.end, None);
7172        assert_eq!(check.command.log_receipt_level, LogLevel::Debug);
7173        assert_eq!(check.filtered_orders.len(), 1);
7174        assert_eq!(check.filtered_orders[0].client_order_id(), included_id);
7175    }
7176
7177    #[rstest]
7178    fn test_prepare_position_report_check_builds_bulk_command_with_coverage() {
7179        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7180        let cache = Rc::new(RefCell::new(Cache::default()));
7181
7182        let manager = ExecutionManager::new(
7183            clock.clone(),
7184            cache.clone(),
7185            ExecutionManagerConfig {
7186                reconciliation_instrument_ids: IndexSet::from([crypto_perpetual_ethusdt().id()]),
7187                ..Default::default()
7188            },
7189        )
7190        .expect("valid config");
7191
7192        let included_instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7193        let excluded_instrument = InstrumentAny::CryptoPerpetual(btcusd_bybit());
7194
7195        cache
7196            .borrow_mut()
7197            .add_instrument(included_instrument.clone())
7198            .unwrap();
7199        cache
7200            .borrow_mut()
7201            .add_instrument(excluded_instrument.clone())
7202            .unwrap();
7203        let included_position = insert_open_position(
7204            &cache,
7205            &included_instrument,
7206            PositionId::from("P-REPORT-001"),
7207            OrderSide::Buy,
7208            "5.0",
7209            "3000.00",
7210        );
7211        insert_open_position(
7212            &cache,
7213            &excluded_instrument,
7214            PositionId::from("P-REPORT-002"),
7215            OrderSide::Buy,
7216            "2.0",
7217            "40000.00",
7218        );
7219
7220        let ts_now = clock.borrow().timestamp_ns();
7221        let command_id = UUID4::new();
7222        let check = manager.prepare_position_report_check(command_id, &[]);
7223        let key = (
7224            included_position.instrument_id,
7225            included_position.account_id,
7226        );
7227
7228        assert_eq!(check.command.command_id, command_id);
7229        assert_eq!(check.command.ts_init, ts_now);
7230        assert_eq!(check.command.instrument_id, None);
7231        assert_eq!(check.command.start, None);
7232        assert_eq!(check.command.end, None);
7233        assert_eq!(check.command.log_receipt_level, LogLevel::Debug);
7234        assert_eq!(check.client_coverage.len(), 1);
7235        assert_eq!(
7236            check.client_coverage.get(&key),
7237            Some(&ReportClientCoverage::Unresolved)
7238        );
7239        assert_eq!(check.activity_revisions.get(&key), Some(&0));
7240    }
7241
7242    #[rstest]
7243    #[case::unresolved(ReportClientCoverage::Unresolved)]
7244    #[case::empty(ReportClientCoverage::Resolved(IndexSet::new()))]
7245    #[case::unqueried(ReportClientCoverage::Resolved(IndexSet::from([ClientId::from("BYBIT")])))]
7246    fn test_position_reconciliation_preserves_position_without_client_coverage(
7247        #[case] coverage: ReportClientCoverage,
7248    ) {
7249        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7250        let cache = Rc::new(RefCell::new(Cache::default()));
7251
7252        let mut manager = ExecutionManager::new(
7253            clock,
7254            cache.clone(),
7255            ExecutionManagerConfig {
7256                position_check_threshold_ns: DurationNanos::ZERO,
7257                ..Default::default()
7258            },
7259        )
7260        .expect("valid config");
7261
7262        let instrument = test_bybit_spot_instrument();
7263        cache
7264            .borrow_mut()
7265            .add_instrument(instrument.clone())
7266            .unwrap();
7267        let position = insert_open_position(
7268            &cache,
7269            &instrument,
7270            PositionId::from("P-COVERAGE-PRESERVED"),
7271            OrderSide::Buy,
7272            "5.0",
7273            "3000.00",
7274        );
7275        let key = (position.instrument_id, position.account_id);
7276        let mut check = manager.prepare_position_report_check(UUID4::new(), &[]);
7277        check.client_coverage.insert(key, coverage);
7278        manager.set_position_reconciliation_retries(key, PositionReportShape::Unambiguous, 1);
7279
7280        let events = manager.reconcile_position_reports(
7281            &check,
7282            Vec::new(),
7283            &IndexSet::new(),
7284            &IndexSet::new(),
7285        );
7286
7287        assert!(events.is_empty());
7288        assert_eq!(manager.position_recon_retry_count(&key), 1);
7289    }
7290
7291    #[rstest]
7292    fn test_position_reconciliation_preserves_unavailable_spot_coverage() {
7293        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7294        let cache = Rc::new(RefCell::new(Cache::default()));
7295        let mut manager =
7296            ExecutionManager::new(clock, cache.clone(), ExecutionManagerConfig::default())
7297                .expect("valid config");
7298        let derivative = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7299        let spot = test_bybit_spot_instrument();
7300        cache
7301            .borrow_mut()
7302            .add_instrument(derivative.clone())
7303            .unwrap();
7304        cache.borrow_mut().add_instrument(spot.clone()).unwrap();
7305        let derivative_position = insert_open_position(
7306            &cache,
7307            &derivative,
7308            PositionId::from("P-DERIVATIVE-RECONCILE"),
7309            OrderSide::Buy,
7310            "5.0",
7311            "3000.00",
7312        );
7313        let spot_position = insert_open_position(
7314            &cache,
7315            &spot,
7316            PositionId::from("P-SPOT-PRESERVED"),
7317            OrderSide::Buy,
7318            "2.0",
7319            "2000.00",
7320        );
7321        let client = PositionCoverageStubClient;
7322        let check = manager.prepare_position_report_check(UUID4::new(), &[&client]);
7323        let queried_clients = IndexSet::from([client.client_id()]);
7324
7325        let events = manager.reconcile_position_reports(
7326            &check,
7327            Vec::new(),
7328            &queried_clients,
7329            &IndexSet::new(),
7330        );
7331
7332        assert!(events.iter().any(|event| {
7333            matches!(event, OrderEventAny::Filled(fill) if fill.instrument_id == derivative_position.instrument_id)
7334        }));
7335        assert!(!events.iter().any(|event| {
7336            matches!(event, OrderEventAny::Filled(fill) if fill.instrument_id == spot_position.instrument_id)
7337        }));
7338    }
7339
7340    #[rstest]
7341    fn test_position_reconciliation_preserves_spot_position_when_client_query_fails() {
7342        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7343        let cache = Rc::new(RefCell::new(Cache::default()));
7344        let mut manager =
7345            ExecutionManager::new(clock, cache.clone(), ExecutionManagerConfig::default())
7346                .expect("valid config");
7347        let instrument = test_bybit_spot_instrument();
7348        cache
7349            .borrow_mut()
7350            .add_instrument(instrument.clone())
7351            .unwrap();
7352        let position = insert_open_position(
7353            &cache,
7354            &instrument,
7355            PositionId::from("P-SPOT-QUERY-FAILED"),
7356            OrderSide::Buy,
7357            "5.0",
7358            "3000.00",
7359        );
7360        let key = (position.instrument_id, position.account_id);
7361        let client_id = ClientId::from("BYBIT");
7362        let mut check = manager.prepare_position_report_check(UUID4::new(), &[]);
7363        check.client_coverage.insert(
7364            key,
7365            ReportClientCoverage::Resolved(IndexSet::from([client_id])),
7366        );
7367        let queried_clients = IndexSet::from([client_id]);
7368        let failed_clients = IndexSet::from([client_id]);
7369
7370        let events = manager.reconcile_position_reports(
7371            &check,
7372            Vec::new(),
7373            &queried_clients,
7374            &failed_clients,
7375        );
7376
7377        assert!(
7378            !events
7379                .iter()
7380                .any(|event| matches!(event, OrderEventAny::Filled(_))),
7381            "a failed bulk query must not generate a synthetic closing fill",
7382        );
7383        let cached_position = cache.borrow().position(&position.id).unwrap().clone();
7384        assert!(cached_position.is_open());
7385        assert_eq!(cached_position.quantity, Quantity::from("5.0"));
7386    }
7387
7388    #[rstest]
7389    #[cfg_attr(
7390        not(all(feature = "simulation", madsim)),
7391        tokio::test(start_paused = true)
7392    )]
7393    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
7394    async fn test_position_report_check_defers_activity_recorded_during_delayed_request() {
7395        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7396        let cache = Rc::new(RefCell::new(Cache::default()));
7397
7398        let mut manager = ExecutionManager::new(
7399            clock,
7400            cache.clone(),
7401            ExecutionManagerConfig {
7402                position_check_threshold_ns: DurationNanos::from_secs(5),
7403                ..Default::default()
7404            },
7405        )
7406        .expect("valid config");
7407
7408        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7409        let instrument_id = instrument.id();
7410        let position = insert_open_position(
7411            &cache,
7412            &instrument,
7413            PositionId::from("P-ACTIVITY-DURING-REQUEST"),
7414            OrderSide::Buy,
7415            "5.0",
7416            "3000.00",
7417        );
7418        cache
7419            .borrow_mut()
7420            .add_instrument(instrument.clone())
7421            .unwrap();
7422        let account_id = position.account_id;
7423        let check = manager.prepare_position_report_check(UUID4::new(), &[]);
7424
7425        let report = PositionStatusReport::new(
7426            account_id,
7427            instrument_id,
7428            PositionSide::Long,
7429            Quantity::from("5.0"),
7430            UnixNanos::from(1_000_000),
7431            UnixNanos::from(1_000_000),
7432            None,
7433            None,
7434            Some(Decimal::from(3000)),
7435        );
7436
7437        let closed_position = close_long_position(
7438            position,
7439            &instrument,
7440            TradeId::from("T-ACTIVITY-DURING-REQUEST"),
7441        );
7442        cache
7443            .borrow_mut()
7444            .update_position(&closed_position)
7445            .unwrap();
7446        manager.record_position_activity(instrument_id, account_id);
7447
7448        // Client A's report is already captured while client B holds the batch open.
7449        dst::time::sleep(Duration::from_secs(6)).await;
7450
7451        let events = manager.reconcile_position_reports(
7452            &check,
7453            vec![report],
7454            &IndexSet::new(),
7455            &IndexSet::new(),
7456        );
7457
7458        assert!(
7459            !events.iter().any(|event| {
7460                matches!(
7461                    event,
7462                    OrderEventAny::Filled(fill)
7463                        if fill.order_side == OrderSide::Buy
7464                            && fill.last_qty == Quantity::from("5.0")
7465                )
7466            }),
7467            "activity recorded after the request started must defer A's stale report",
7468        );
7469    }
7470
7471    #[rstest]
7472    fn test_position_report_check_does_not_defer_activity_recorded_before_request() {
7473        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7474        let cache = Rc::new(RefCell::new(Cache::default()));
7475
7476        let mut manager = ExecutionManager::new(
7477            clock,
7478            cache.clone(),
7479            ExecutionManagerConfig {
7480                position_check_threshold_ns: DurationNanos::ZERO,
7481                ..Default::default()
7482            },
7483        )
7484        .expect("valid config");
7485
7486        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7487        let instrument_id = instrument.id();
7488        let position = insert_open_position(
7489            &cache,
7490            &instrument,
7491            PositionId::from("P-ACTIVITY-BEFORE-REQUEST"),
7492            OrderSide::Buy,
7493            "5.0",
7494            "3000.00",
7495        );
7496        cache.borrow_mut().add_instrument(instrument).unwrap();
7497        let account_id = position.account_id;
7498        manager.record_position_activity(instrument_id, account_id);
7499        let check = manager.prepare_position_report_check(UUID4::new(), &[]);
7500
7501        let report = PositionStatusReport::new(
7502            account_id,
7503            instrument_id,
7504            PositionSide::Long,
7505            Quantity::from("10.0"),
7506            UnixNanos::from(1_000_000),
7507            UnixNanos::from(1_000_000),
7508            None,
7509            None,
7510            Some(Decimal::from(3000)),
7511        );
7512
7513        let events = manager.reconcile_position_reports(
7514            &check,
7515            vec![report],
7516            &IndexSet::new(),
7517            &IndexSet::new(),
7518        );
7519
7520        let fills: Vec<_> = events
7521            .iter()
7522            .filter_map(|event| match event {
7523                OrderEventAny::Filled(fill) => Some(fill),
7524                _ => None,
7525            })
7526            .collect();
7527
7528        assert_eq!(fills.len(), 1);
7529        assert_eq!(fills[0].order_side, OrderSide::Buy);
7530        assert_eq!(fills[0].last_qty, Quantity::from("5.0"));
7531        assert_eq!(fills[0].commission, None);
7532    }
7533
7534    #[rstest]
7535    fn test_mass_status_projects_companion_fill_before_void_correction() {
7536        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7537        let cache = Rc::new(RefCell::new(Cache::default()));
7538        let mut manager =
7539            ExecutionManager::new(clock, cache.clone(), ExecutionManagerConfig::default())
7540                .expect("valid config");
7541        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7542        let client_order_id = ClientOrderId::from("O-MASS-VOID-001");
7543        let venue_order_id = VenueOrderId::from("V-MASS-VOID-001");
7544        let account_id = AccountId::from("TEST-001");
7545        cache
7546            .borrow_mut()
7547            .add_instrument(instrument.clone())
7548            .unwrap();
7549        insert_accepted_limit_order(
7550            &cache,
7551            client_order_id,
7552            venue_order_id,
7553            instrument.id(),
7554            ClientId::from("BINANCE"),
7555        );
7556        let order = cache.borrow().order_owned(&client_order_id).unwrap();
7557        let initial_fill = TestOrderEventStubs::filled(
7558            &order,
7559            &instrument,
7560            Some(TradeId::from("T-MASS-VOID-INITIAL")),
7561            None,
7562            Some(Price::from("100.0")),
7563            Some(Quantity::from("6.0")),
7564            Some(LiquiditySide::Taker),
7565            None,
7566            None,
7567            Some(account_id),
7568        );
7569        cache.borrow_mut().update_order(&initial_fill).unwrap();
7570        let order = cache.borrow().order_owned(&client_order_id).unwrap();
7571
7572        let report = OrderStatusReport::new(
7573            account_id,
7574            instrument.id(),
7575            Some(client_order_id),
7576            venue_order_id,
7577            OrderSide::Buy.into(),
7578            OrderType::Limit,
7579            TimeInForce::Gtc,
7580            OrderStatus::Canceled,
7581            Quantity::from("10.0"),
7582            Quantity::from("5.0"),
7583            UnixNanos::from(1_000),
7584            UnixNanos::from(1_000),
7585            UnixNanos::from(1_000),
7586            None,
7587        );
7588
7589        let companion_fill = FillReport::new(
7590            account_id,
7591            instrument.id(),
7592            venue_order_id,
7593            TradeId::from("T-MASS-VOID-COMPANION"),
7594            OrderSide::Buy,
7595            Quantity::from("1.0"),
7596            Price::from("100.0"),
7597            Money::zero(instrument.quote_currency()),
7598            LiquiditySide::Taker,
7599            Some(client_order_id),
7600            None,
7601            UnixNanos::from(900),
7602            UnixNanos::from(1_000),
7603            None,
7604        );
7605
7606        let mut fill_queue = ReconciliationFillQueue::default();
7607        let events = manager.reconcile_order_with_fills(
7608            true,
7609            &order,
7610            &report,
7611            &[&companion_fill],
7612            Some(&instrument),
7613            &mut fill_queue,
7614            None,
7615        );
7616        let mut projected = order;
7617
7618        for event in &events {
7619            projected.apply(event.clone()).unwrap();
7620        }
7621
7622        assert!(matches!(events[0], OrderEventAny::Filled(_)));
7623        assert_eq!(
7624            events
7625                .iter()
7626                .filter(|event| matches!(event, OrderEventAny::FillVoided(_)))
7627                .count(),
7628            2
7629        );
7630        assert_eq!(projected.status(), OrderStatus::Canceled);
7631        assert_eq!(projected.filled_qty(), Quantity::from("5.0"));
7632        assert_eq!(projected.voided_qty(), Quantity::from("2.0"));
7633    }
7634
7635    fn insert_accepted_limit_order(
7636        cache: &Rc<RefCell<Cache>>,
7637        client_order_id: ClientOrderId,
7638        venue_order_id: VenueOrderId,
7639        instrument_id: InstrumentId,
7640        client_id: ClientId,
7641    ) {
7642        let account_id = AccountId::from("TEST-001");
7643        let order = OrderTestBuilder::new(OrderType::Limit)
7644            .client_order_id(client_order_id)
7645            .instrument_id(instrument_id)
7646            .quantity(Quantity::from("10.0"))
7647            .price(Price::from("100.0"))
7648            .build();
7649        let submitted = TestOrderEventStubs::submitted(&order, account_id);
7650        cache
7651            .borrow_mut()
7652            .add_order(order, None, Some(client_id), false)
7653            .unwrap();
7654        let order = cache.borrow_mut().update_order(&submitted).unwrap();
7655        let accepted = TestOrderEventStubs::accepted(&order, account_id, venue_order_id);
7656        cache.borrow_mut().update_order(&accepted).unwrap();
7657    }
7658
7659    fn test_bybit_spot_instrument() -> InstrumentAny {
7660        InstrumentAny::CurrencyPair(
7661            CurrencyPair::builder()
7662                .instrument_id(InstrumentId::from("ETHUSDT-SPOT.BYBIT"))
7663                .raw_symbol(Symbol::from("ETHUSDT"))
7664                .base_currency(Currency::from("ETH"))
7665                .quote_currency(Currency::from("USDT"))
7666                .price_precision(2)
7667                .size_precision(5)
7668                .price_increment(Price::from("0.01"))
7669                .size_increment(Quantity::from("0.00001"))
7670                .ts_event(UnixNanos::default())
7671                .ts_init(UnixNanos::default())
7672                .build()
7673                .unwrap(),
7674        )
7675    }
7676
7677    fn insert_open_position(
7678        cache: &Rc<RefCell<Cache>>,
7679        instrument: &InstrumentAny,
7680        position_id: PositionId,
7681        side: OrderSide,
7682        quantity: &str,
7683        price: &str,
7684    ) -> Position {
7685        let order = OrderTestBuilder::new(OrderType::Market)
7686            .instrument_id(instrument.id())
7687            .side(side)
7688            .quantity(Quantity::from(quantity))
7689            .build();
7690        let fill = TestOrderEventStubs::filled(
7691            &order,
7692            instrument,
7693            Some(TradeId::new("T-REPORT-001")),
7694            Some(position_id),
7695            Some(Price::from(price)),
7696            Some(Quantity::from(quantity)),
7697            None,
7698            None,
7699            None,
7700            Some(AccountId::from("TEST-001")),
7701        );
7702        let order_filled: OrderFilled = fill.into();
7703        let position = Position::new(instrument, order_filled);
7704        cache
7705            .borrow_mut()
7706            .add_position(&position, OmsType::Hedging)
7707            .unwrap();
7708        position
7709    }
7710
7711    fn close_long_position(
7712        mut position: Position,
7713        instrument: &InstrumentAny,
7714        trade_id: TradeId,
7715    ) -> Position {
7716        let order = OrderTestBuilder::new(OrderType::Market)
7717            .instrument_id(instrument.id())
7718            .side(OrderSide::Sell)
7719            .quantity(position.quantity)
7720            .build();
7721        let fill = TestOrderEventStubs::filled(
7722            &order,
7723            instrument,
7724            Some(trade_id),
7725            Some(position.id),
7726            Some(Price::from("3000.00")),
7727            Some(position.quantity),
7728            None,
7729            None,
7730            None,
7731            Some(position.account_id),
7732        );
7733        let order_filled: OrderFilled = fill.into();
7734        position.apply(&order_filled);
7735        position
7736    }
7737
7738    #[cfg(feature = "node")]
7739    mod node {
7740        use nautilus_model::events::order::spec::OrderFillVoidedSpec;
7741
7742        use super::*;
7743        use crate::execution::client::LiveExecutionClient;
7744
7745        #[rstest]
7746        fn test_position_fill_reports_contained_prefix_matches_full_report(
7747            #[values(
7748                "account",
7749                "instrument",
7750                "venue_order",
7751                "client_order",
7752                "trade",
7753                "position",
7754                "side",
7755                "price",
7756                "liquidity",
7757                "quantity",
7758                "commission",
7759                "currency"
7760            )]
7761            field: &str,
7762        ) {
7763            let (manager, _, report) = position_history_fixture();
7764            let mut changed = report.clone();
7765            match field {
7766                "account" => changed.account_id = AccountId::from("OTHER-002"),
7767                "instrument" => changed.instrument_id = InstrumentId::from("OTHER.SIM"),
7768                "venue_order" => changed.venue_order_id = VenueOrderId::from("V-OTHER"),
7769                "client_order" => changed.client_order_id = Some(ClientOrderId::from("O-OTHER")),
7770                "trade" => changed.trade_id = TradeId::from("T-OTHER"),
7771                "position" => changed.venue_position_id = Some(PositionId::from("P-OTHER")),
7772                "side" => changed.order_side = OrderSide::Sell,
7773                "price" => changed.last_px = Price::from("3101.00"),
7774                "liquidity" => changed.liquidity_side = LiquiditySide::Maker,
7775                "quantity" => changed.last_qty = Quantity::from("3.000"),
7776                "commission" => changed.commission = Money::from("0.17 USDT"),
7777                "currency" => changed.commission = Money::from("0.13 USD"),
7778                _ => unreachable!(),
7779            }
7780
7781            let reports = [report.clone(), changed, report.clone()];
7782            let scalar: Vec<_> = reports
7783                .iter()
7784                .map(|report| manager.position_contains_fill_report(report))
7785                .collect();
7786            let prefix = manager.position_fill_reports_contained_prefix(&reports);
7787            let repeated =
7788                manager.position_fill_reports_contained_prefix(&[report.clone(), report]);
7789
7790            assert_eq!(scalar, vec![true, false, true]);
7791            assert_eq!(prefix, 1);
7792            assert_eq!(repeated, 2);
7793            assert_eq!(manager.position_fill_reports_contained_prefix(&[]), 0);
7794        }
7795
7796        #[rstest]
7797        fn test_position_fill_reports_contained_prefix_indexes_only_routed_positions(
7798            #[values(false, true)] closed: bool,
7799            #[values(false, true)] missing: bool,
7800        ) {
7801            let (manager, cache, mut report) = position_history_fixture();
7802            let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
7803            let position = insert_open_position(
7804                &cache,
7805                &instrument,
7806                PositionId::from("P-UNRELATED"),
7807                OrderSide::Buy,
7808                "4.000",
7809                "3101.00",
7810            );
7811
7812            if closed {
7813                let position =
7814                    close_long_position(position, &instrument, TradeId::from("T-UNRELATED-CLOSE"));
7815                cache.borrow_mut().update_position(&position).unwrap();
7816            }
7817
7818            let trade_id = report.trade_id;
7819            if missing {
7820                report.trade_id = TradeId::from("T-MISSING");
7821            }
7822
7823            let prefix =
7824                manager.position_fill_reports_contained_prefix(&[report.clone(), report.clone()]);
7825            let cache = cache.borrow();
7826            let positions = cache.positions_refs(
7827                None,
7828                Some(&report.instrument_id),
7829                None,
7830                Some(&report.account_id),
7831                None,
7832            );
7833            let mut replay = IndexMap::new();
7834            let contained = position_fill_report_matches(
7835                &cache,
7836                &report,
7837                &positions,
7838                |id| cache.hedging_flip_position_ids(id),
7839                Some(&mut replay),
7840            );
7841
7842            assert_eq!(positions.len(), 2);
7843            assert_eq!(contained, !missing);
7844            assert_eq!(prefix, if missing { 0 } else { 2 });
7845            assert_eq!(
7846                replay,
7847                IndexMap::from([(
7848                    report.venue_position_id.unwrap(),
7849                    IndexMap::from([(trade_id, vec![0])]),
7850                )]),
7851            );
7852        }
7853
7854        #[rstest]
7855        fn test_position_fill_reports_contained_prefix_resolves_client_order() {
7856            let (manager, cache, mut report) = position_history_fixture();
7857            let position_id = report.venue_position_id.unwrap();
7858            cache
7859                .borrow_mut()
7860                .add_venue_order_id(
7861                    &report.client_order_id.unwrap(),
7862                    &report.venue_order_id,
7863                    false,
7864                )
7865                .unwrap();
7866            report.client_order_id = None;
7867            let before =
7868                manager.position_fill_reports_contained_prefix(&[report.clone(), report.clone()]);
7869            {
7870                let mut cache = cache.borrow_mut();
7871                let mut position = cache.position_mut(&position_id).unwrap();
7872
7873                let PositionReplayEvent::Filled(fill) = &mut position.replay_events[0] else {
7874                    unreachable!()
7875                };
7876
7877                fill.client_order_id = ClientOrderId::from("O-OTHER");
7878            }
7879
7880            let after = manager.position_fill_reports_contained_prefix(&[report.clone(), report]);
7881
7882            assert_eq!(before, 2);
7883            assert_eq!(after, 0);
7884        }
7885
7886        #[rstest]
7887        fn test_position_fill_reports_contained_prefix_retains_reordered_fragments(
7888            #[values(false, true)] reverse: bool,
7889        ) {
7890            let (manager, cache, mut report) = position_history_fixture();
7891            let position_id = report.venue_position_id.unwrap();
7892            {
7893                let mut cache = cache.borrow_mut();
7894                let mut position = cache.position_mut(&position_id).unwrap();
7895
7896                let PositionReplayEvent::Filled(mut fragment) = position.replay_events[0].clone()
7897                else {
7898                    unreachable!()
7899                };
7900
7901                fragment.last_qty = Quantity::from("3.000");
7902                fragment.commission = Some(Money::from("0.29 USDT"));
7903                let mut unrelated = fragment.clone();
7904                unrelated.trade_id = TradeId::from("T-UNRELATED");
7905                position
7906                    .replay_events
7907                    .push(PositionReplayEvent::Filled(unrelated));
7908                position
7909                    .replay_events
7910                    .push(PositionReplayEvent::Filled(fragment));
7911
7912                if reverse {
7913                    position.replay_events.reverse();
7914                }
7915            }
7916
7917            let original =
7918                manager.position_fill_reports_contained_prefix(&[report.clone(), report.clone()]);
7919            report.last_qty = Quantity::from("5.000");
7920            report.commission = Money::from("0.42 USDT");
7921            let combined =
7922                manager.position_fill_reports_contained_prefix(&[report.clone(), report.clone()]);
7923            let scalar = manager.position_contains_fill_report(&report);
7924
7925            assert_eq!(original, 0);
7926            assert_eq!(combined, 2);
7927            assert!(scalar);
7928        }
7929
7930        #[rstest]
7931        fn test_position_fill_reports_contained_prefix_rechecks_after_applied_fill() {
7932            let (manager, cache, first) = position_history_fixture();
7933            let position_id = first.venue_position_id.unwrap();
7934            let mut second = first.clone();
7935            second.trade_id = TradeId::from("T-NEXT");
7936            second.last_qty = Quantity::from("1.500");
7937            second.last_px = Price::from("3110.00");
7938            second.commission = Money::from("0.29 USDT");
7939            let reports = [first, second.clone()];
7940            let before = manager.position_fill_reports_contained_prefix(&reports);
7941            {
7942                let mut cache = cache.borrow_mut();
7943                let mut position = cache.position_mut(&position_id).unwrap();
7944
7945                let PositionReplayEvent::Filled(mut fill) = position.replay_events[0].clone()
7946                else {
7947                    unreachable!()
7948                };
7949
7950                fill.trade_id = second.trade_id;
7951                fill.last_qty = second.last_qty;
7952                fill.last_px = second.last_px;
7953                fill.commission = Some(second.commission);
7954                fill.event_id = UUID4::new();
7955                position.apply(&fill);
7956            }
7957
7958            let after = manager.position_fill_reports_contained_prefix(&reports);
7959            let cache = cache.borrow();
7960            let position = cache.position_ref(&position_id).unwrap();
7961
7962            assert_eq!(before, 1);
7963            assert_eq!(after, 2);
7964            assert_eq!(position.quantity, Quantity::from("3.500"));
7965            assert_eq!(position.commissions(), vec![Money::from("0.42 USDT")]);
7966            assert_eq!(position.replay_events.len(), 2);
7967            assert_eq!(
7968                manager.position_activity_revision(&(second.instrument_id, second.account_id)),
7969                0
7970            );
7971        }
7972
7973        #[rstest]
7974        fn test_position_fill_reports_contained_prefix_rebuilds_after_history_changes(
7975            #[values("missing_commission", "currency", "payload", "removal")] mutation: &str,
7976        ) {
7977            let (manager, cache, report) = position_history_fixture();
7978            let reports = [report.clone(), report.clone()];
7979            let before = manager.position_fill_reports_contained_prefix(&reports);
7980            let revision =
7981                manager.position_activity_revision(&(report.instrument_id, report.account_id));
7982            {
7983                let mut cache = cache.borrow_mut();
7984
7985                if mutation == "removal" {
7986                    let position_id = report.venue_position_id.unwrap();
7987                    {
7988                        let mut position = cache.position_mut(&position_id).unwrap();
7989
7990                        let PositionReplayEvent::Filled(mut closing) =
7991                            position.replay_events[0].clone()
7992                        else {
7993                            unreachable!()
7994                        };
7995
7996                        closing.trade_id = TradeId::from("T-HISTORY-CLOSE");
7997                        closing.order_side = OrderSide::Sell;
7998                        closing.event_id = UUID4::new();
7999                        position.apply(&closing);
8000                    }
8001
8002                    cache.purge_position(position_id);
8003                } else {
8004                    let mut position = cache
8005                        .position_mut(&report.venue_position_id.unwrap())
8006                        .unwrap();
8007
8008                    let PositionReplayEvent::Filled(fill) = &mut position.replay_events[0] else {
8009                        unreachable!()
8010                    };
8011
8012                    match mutation {
8013                        "missing_commission" => fill.commission = None,
8014                        "currency" => fill.commission = Some(Money::from("0.13 USD")),
8015                        "payload" => fill.last_px = Price::from("3101.00"),
8016                        _ => unreachable!(),
8017                    }
8018                }
8019            }
8020
8021            let after = manager.position_fill_reports_contained_prefix(&reports);
8022            let revision_after =
8023                manager.position_activity_revision(&(report.instrument_id, report.account_id));
8024
8025            assert_eq!(before, 2);
8026            assert_eq!(after, 0);
8027            assert_eq!(revision, 0);
8028            assert_eq!(revision_after, 0);
8029        }
8030
8031        #[rstest]
8032        fn test_position_fill_reports_contained_prefix_preserves_virtual_flip_proof(
8033            #[values(
8034                "unchanged",
8035                "missing_source",
8036                "conflicting_fragment",
8037                "opening_void",
8038                "closing_void"
8039            )]
8040            change: &str,
8041        ) {
8042            let (manager, cache, first) = position_history_fixture();
8043            let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8044            let source_id = first.venue_position_id.unwrap();
8045            let destination_id = PositionId::from("P-HISTORY-DESTINATION");
8046            let mut order = OrderTestBuilder::new(OrderType::Market)
8047                .instrument_id(instrument.id())
8048                .client_order_id(ClientOrderId::from("O-HISTORY-FLIP"))
8049                .side(OrderSide::Sell)
8050                .quantity(Quantity::from("5.000"))
8051                .build();
8052            order
8053                .apply(TestOrderEventStubs::submitted(&order, first.account_id))
8054                .unwrap();
8055            order
8056                .apply(TestOrderEventStubs::accepted(
8057                    &order,
8058                    first.account_id,
8059                    VenueOrderId::from("V-HISTORY-FLIP"),
8060                ))
8061                .unwrap();
8062            let event = TestOrderEventStubs::filled(
8063                &order,
8064                &instrument,
8065                Some(TradeId::from("T-HISTORY-FLIP")),
8066                Some(source_id),
8067                Some(Price::from("3100.00")),
8068                Some(Quantity::from("3.000")),
8069                Some(LiquiditySide::Taker),
8070                Some(Money::from("0.75 USDT")),
8071                None,
8072                Some(first.account_id),
8073            );
8074
8075            let OrderEventAny::Filled(fill) = event.clone() else {
8076                unreachable!()
8077            };
8078
8079            order.apply(event).unwrap();
8080            let (closing, opening) = fill
8081                .split_for_position_flip(
8082                    Quantity::from("2.000"),
8083                    Some(destination_id),
8084                    UUID4::new(),
8085                )
8086                .unwrap();
8087            {
8088                let mut cache = cache.borrow_mut();
8089                cache.position_mut(&source_id).unwrap().apply(&closing);
8090                cache
8091                    .add_order(order, Some(source_id), None, false)
8092                    .unwrap();
8093                cache
8094                    .add_position(&Position::new(&instrument, opening), OmsType::Hedging)
8095                    .unwrap();
8096            }
8097
8098            let mut report = FillReport::new(
8099                fill.account_id,
8100                fill.instrument_id,
8101                fill.venue_order_id,
8102                fill.trade_id,
8103                fill.order_side,
8104                fill.last_qty,
8105                fill.last_px,
8106                fill.commission.unwrap(),
8107                fill.liquidity_side,
8108                Some(fill.client_order_id),
8109                Some(source_id),
8110                fill.ts_event,
8111                fill.ts_init,
8112                None,
8113            );
8114            let before =
8115                manager.position_fill_reports_contained_prefix(&[report.clone(), report.clone()]);
8116            {
8117                let mut cache = cache.borrow_mut();
8118
8119                match change {
8120                    "unchanged" => {}
8121                    "missing_source" => cache.purge_position(source_id),
8122                    "conflicting_fragment" => {
8123                        let mut position = cache.position_mut(&destination_id).unwrap();
8124
8125                        let PositionReplayEvent::Filled(fragment) = &mut position.replay_events[0]
8126                        else {
8127                            unreachable!()
8128                        };
8129
8130                        fragment.last_px = Price::from("3101.00");
8131                    }
8132                    "opening_void" | "closing_void" => void_position_history_flip(
8133                        &mut cache,
8134                        &fill,
8135                        source_id,
8136                        destination_id,
8137                        change == "closing_void",
8138                    ),
8139                    _ => unreachable!(),
8140                }
8141            }
8142
8143            let original_after =
8144                manager.position_fill_reports_contained_prefix(&[report.clone(), report.clone()]);
8145            if change == "missing_source" {
8146                report.venue_position_id = None;
8147                report.last_qty = Quantity::from("1.000");
8148                report.commission = Money::from("0.25 USDT");
8149            } else if change == "conflicting_fragment" {
8150                report.last_qty = Quantity::from("2.000");
8151                report.commission = Money::from("0.50 USDT");
8152            } else if change == "opening_void" {
8153                report.last_qty = Quantity::from("2.500");
8154                report.commission = Money::from("0.625 USDT");
8155            } else if change == "closing_void" {
8156                report.last_qty = Quantity::from("1.500");
8157                report.commission = Money::from("0.375 USDT");
8158            }
8159
8160            let after =
8161                manager.position_fill_reports_contained_prefix(&[report.clone(), report.clone()]);
8162            let scalar = manager.position_contains_fill_report(&report);
8163            let mixed = manager.position_fill_reports_contained_prefix(&[
8164                first,
8165                report.clone(),
8166                report.clone(),
8167            ]);
8168            let expected = !matches!(change, "missing_source" | "conflicting_fragment");
8169
8170            assert_eq!(before, 2);
8171            assert_eq!(original_after, if change == "unchanged" { 2 } else { 0 });
8172            assert_eq!(after, if expected { 2 } else { 0 });
8173            assert_eq!(scalar, expected);
8174
8175            let expected_mixed = match change {
8176                "missing_source" => 0,
8177                "conflicting_fragment" => 1,
8178                _ => 3,
8179            };
8180
8181            assert_eq!(mixed, expected_mixed);
8182            assert_eq!(
8183                manager.position_activity_revision(&(report.instrument_id, report.account_id)),
8184                0
8185            );
8186
8187            if expected {
8188                let cache = cache.borrow();
8189                let source = cache.position_ref(&source_id).unwrap();
8190                let destination = cache.position_ref(&destination_id).unwrap();
8191                assert_eq!(
8192                    source.quantity,
8193                    Quantity::from(if change == "closing_void" {
8194                        "0.500"
8195                    } else {
8196                        "0.000"
8197                    })
8198                );
8199                assert_eq!(
8200                    destination.quantity,
8201                    Quantity::from(match change {
8202                        "closing_void" => "0.000",
8203                        "opening_void" => "0.500",
8204                        _ => "1.000",
8205                    })
8206                );
8207                assert_eq!(
8208                    source.commissions(),
8209                    vec![Money::from(if change == "closing_void" {
8210                        "0.505 USDT"
8211                    } else {
8212                        "0.63 USDT"
8213                    })]
8214                );
8215
8216                let destination_commissions = if change == "closing_void" {
8217                    Vec::new()
8218                } else {
8219                    vec![Money::from(if change == "opening_void" {
8220                        "0.125 USDT"
8221                    } else {
8222                        "0.25 USDT"
8223                    })]
8224                };
8225
8226                assert_eq!(destination.commissions(), destination_commissions);
8227            }
8228        }
8229
8230        fn void_position_history_flip(
8231            cache: &mut Cache,
8232            fill: &OrderFilled,
8233            source_id: PositionId,
8234            destination_id: PositionId,
8235            closing_void: bool,
8236        ) {
8237            let correction = OrderFillVoidedSpec::builder()
8238                .trader_id(fill.trader_id)
8239                .strategy_id(fill.strategy_id)
8240                .instrument_id(fill.instrument_id)
8241                .client_order_id(fill.client_order_id)
8242                .venue_order_id(fill.venue_order_id)
8243                .account_id(fill.account_id)
8244                .trade_id(fill.trade_id)
8245                .voided_qty(Quantity::from(if closing_void { "1.500" } else { "0.500" }))
8246                .order_side(fill.order_side)
8247                .order_type(fill.order_type)
8248                .last_px(fill.last_px)
8249                .currency(fill.currency)
8250                .liquidity_side(fill.liquidity_side)
8251                .position_id(source_id)
8252                .commission_voided(Money::from(if closing_void {
8253                    "0.375 USDT"
8254                } else {
8255                    "0.125 USDT"
8256                }))
8257                .is_reopened(true)
8258                .build();
8259
8260            cache
8261                .position_mut(&destination_id)
8262                .unwrap()
8263                .apply_fill_void(
8264                    correction.clone(),
8265                    Quantity::from(if closing_void { "1.000" } else { "0.500" }),
8266                    Some(Money::from(if closing_void {
8267                        "0.25 USDT"
8268                    } else {
8269                        "0.125 USDT"
8270                    })),
8271                )
8272                .unwrap();
8273
8274            if closing_void {
8275                cache
8276                    .position_mut(&source_id)
8277                    .unwrap()
8278                    .apply_fill_void(
8279                        correction,
8280                        Quantity::from("0.500"),
8281                        Some(Money::from("0.125 USDT")),
8282                    )
8283                    .unwrap();
8284            }
8285        }
8286
8287        fn position_history_fixture() -> (ExecutionManager, Rc<RefCell<Cache>>, FillReport) {
8288            let cache = Rc::new(RefCell::new(Cache::default()));
8289            let manager = ExecutionManager::new(
8290                Rc::new(RefCell::new(VirtualClock::new())),
8291                cache.clone(),
8292                ExecutionManagerConfig::default(),
8293            )
8294            .unwrap();
8295            let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8296            let order = OrderTestBuilder::new(OrderType::Market)
8297                .instrument_id(instrument.id())
8298                .client_order_id(ClientOrderId::from("O-HISTORY"))
8299                .side(OrderSide::Buy)
8300                .quantity(Quantity::from("10.000"))
8301                .build();
8302            let position_id = PositionId::from("P-HISTORY");
8303            let event = TestOrderEventStubs::filled(
8304                &order,
8305                &instrument,
8306                Some(TradeId::from("T-HISTORY")),
8307                Some(position_id),
8308                Some(Price::from("3100.00")),
8309                Some(Quantity::from("2.000")),
8310                Some(LiquiditySide::Taker),
8311                Some(Money::from("0.13 USDT")),
8312                None,
8313                Some(AccountId::from("TEST-001")),
8314            );
8315
8316            let OrderEventAny::Filled(fill) = event else {
8317                unreachable!()
8318            };
8319
8320            let report = FillReport::new(
8321                fill.account_id,
8322                fill.instrument_id,
8323                fill.venue_order_id,
8324                fill.trade_id,
8325                fill.order_side,
8326                fill.last_qty,
8327                fill.last_px,
8328                fill.commission.unwrap(),
8329                fill.liquidity_side,
8330                Some(fill.client_order_id),
8331                Some(position_id),
8332                fill.ts_event,
8333                fill.ts_init,
8334                None,
8335            );
8336            let position = Position::new(&instrument, fill);
8337            cache
8338                .borrow_mut()
8339                .add_position(&position, OmsType::Hedging)
8340                .unwrap();
8341            (manager, cache, report)
8342        }
8343
8344        #[rstest]
8345        fn test_plan_position_fill_reports_uses_configured_lookback() {
8346            let lookback_mins = 7_u64;
8347            let lookback = DurationNanos::from_mins(lookback_mins);
8348            let clock = Rc::new(RefCell::new(VirtualClock::new()));
8349            let cache = Rc::new(RefCell::new(Cache::default()));
8350
8351            let mut manager = ExecutionManager::new(
8352                clock.clone(),
8353                cache.clone(),
8354                ExecutionManagerConfig {
8355                    position_check_lookback_mins: lookback_mins,
8356                    position_check_threshold_ns: DurationNanos::ZERO,
8357                    ..Default::default()
8358                },
8359            )
8360            .expect("valid config");
8361
8362            let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8363            cache
8364                .borrow_mut()
8365                .add_instrument(instrument.clone())
8366                .unwrap();
8367            let position = insert_open_position(
8368                &cache,
8369                &instrument,
8370                PositionId::from("P-FILL-LOOKBACK"),
8371                OrderSide::Buy,
8372                "1.0",
8373                "3000.00",
8374            );
8375            clock
8376                .borrow_mut()
8377                .advance_time(UnixNanos::default().saturating_add(lookback * 2), true);
8378            let client = PositionCoverageStubClient;
8379            let clients: [&dyn ExecutionClient; 1] = [&client];
8380            let mut check = manager.prepare_position_report_check(UUID4::new(), &clients);
8381            let query_end = clock.borrow().timestamp_ns();
8382
8383            let report = PositionStatusReport::new(
8384                position.account_id,
8385                position.instrument_id,
8386                PositionSide::Long,
8387                Quantity::from("2.0"),
8388                query_end,
8389                query_end,
8390                None,
8391                None,
8392                Some(dec!(3000.00)),
8393            );
8394            let queried_clients = IndexSet::from([client.client_id()]);
8395
8396            let plan = manager.plan_position_fill_reports(
8397                &mut check,
8398                &[report],
8399                &queried_clients,
8400                &IndexSet::new(),
8401                &clients,
8402            );
8403
8404            assert_eq!(
8405                plan.discrepancy_keys,
8406                IndexSet::from([(position.instrument_id, position.account_id)])
8407            );
8408            assert_eq!(plan.queries.len(), 1);
8409            let query = &plan.queries[0];
8410            assert_eq!(
8411                (query.key, query.client_id),
8412                (
8413                    (position.instrument_id, position.account_id),
8414                    client.client_id()
8415                )
8416            );
8417            assert_eq!(query.command.instrument_id, Some(position.instrument_id));
8418            assert_eq!(query.command.venue_order_id, None);
8419            assert_eq!(
8420                query.command.start,
8421                Some(query_end.saturating_sub(lookback))
8422            );
8423            assert_eq!(query.command.end, Some(query_end));
8424            assert_eq!(query.command.correlation_id, Some(check.command.command_id));
8425            assert_eq!(query.command.log_receipt_level, LogLevel::Debug);
8426        }
8427
8428        #[rstest]
8429        #[case::reported(Some("2.0"), true)]
8430        #[case::unreported(None, false)]
8431        fn test_plan_position_fill_reports_unavailable_coverage_requires_venue_report(
8432            #[case] venue_qty: Option<&str>,
8433            #[case] expect_query: bool,
8434        ) {
8435            let clock = Rc::new(RefCell::new(VirtualClock::new()));
8436            let cache = Rc::new(RefCell::new(Cache::default()));
8437
8438            let mut manager = ExecutionManager::new(
8439                clock.clone(),
8440                cache.clone(),
8441                ExecutionManagerConfig {
8442                    position_check_threshold_ns: DurationNanos::ZERO,
8443                    ..Default::default()
8444                },
8445            )
8446            .expect("valid config");
8447
8448            let spot = test_bybit_spot_instrument();
8449            cache.borrow_mut().add_instrument(spot.clone()).unwrap();
8450            let position = insert_open_position(
8451                &cache,
8452                &spot,
8453                PositionId::from("P-SPOT-UNAVAILABLE"),
8454                OrderSide::Buy,
8455                "1.0",
8456                "2000.00",
8457            );
8458            let key = (position.instrument_id, position.account_id);
8459            let client = PositionCoverageStubClient;
8460            let clients: [&dyn ExecutionClient; 1] = [&client];
8461            let mut check = manager.prepare_position_report_check(UUID4::new(), &clients);
8462            let ts_now = clock.borrow().timestamp_ns();
8463
8464            let reports: Vec<PositionStatusReport> = venue_qty
8465                .into_iter()
8466                .map(|qty| {
8467                    PositionStatusReport::new(
8468                        position.account_id,
8469                        position.instrument_id,
8470                        PositionSide::Long,
8471                        Quantity::from(qty),
8472                        ts_now,
8473                        ts_now,
8474                        None,
8475                        None,
8476                        Some(dec!(2000.00)),
8477                    )
8478                })
8479                .collect();
8480
8481            let queried_clients = IndexSet::from([client.client_id()]);
8482
8483            let plan = manager.plan_position_fill_reports(
8484                &mut check,
8485                &reports,
8486                &queried_clients,
8487                &IndexSet::new(),
8488                &clients,
8489            );
8490
8491            let expected_queries = if expect_query {
8492                vec![(key, client.client_id())]
8493            } else {
8494                Vec::new()
8495            };
8496
8497            assert_eq!(
8498                check.client_coverage.get(&key),
8499                Some(&ReportClientCoverage::Unavailable(IndexSet::from([
8500                    client.client_id()
8501                ])))
8502            );
8503            assert_eq!(plan.discrepancy_keys, IndexSet::from([key]));
8504            assert_eq!(
8505                plan.queries
8506                    .iter()
8507                    .map(|query| (query.key, query.client_id))
8508                    .collect::<Vec<_>>(),
8509                expected_queries
8510            );
8511        }
8512
8513        #[rstest]
8514        fn test_plan_position_fill_reports_defers_position_opened_during_request() {
8515            let clock = Rc::new(RefCell::new(VirtualClock::new()));
8516            let cache = Rc::new(RefCell::new(Cache::default()));
8517
8518            let mut manager = ExecutionManager::new(
8519                clock,
8520                cache.clone(),
8521                ExecutionManagerConfig {
8522                    position_check_threshold_ns: DurationNanos::ZERO,
8523                    ..Default::default()
8524                },
8525            )
8526            .expect("valid config");
8527
8528            let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8529            cache
8530                .borrow_mut()
8531                .add_instrument(instrument.clone())
8532                .unwrap();
8533            let client = PositionCoverageStubClient;
8534            let clients: [&dyn ExecutionClient; 1] = [&client];
8535            let mut check = manager.prepare_position_report_check(UUID4::new(), &clients);
8536            let position = insert_open_position(
8537                &cache,
8538                &instrument,
8539                PositionId::from("P-FILL-DURING-REQUEST"),
8540                OrderSide::Buy,
8541                "1.0",
8542                "3000.00",
8543            );
8544            manager.record_position_activity(position.instrument_id, position.account_id);
8545
8546            let report = PositionStatusReport::new(
8547                position.account_id,
8548                position.instrument_id,
8549                PositionSide::Long,
8550                Quantity::from("2.0"),
8551                UnixNanos::from(1_000_000),
8552                UnixNanos::from(1_000_000),
8553                None,
8554                None,
8555                Some(dec!(3000.00)),
8556            );
8557
8558            let plan = manager.plan_position_fill_reports(
8559                &mut check,
8560                &[report],
8561                &IndexSet::from([client.client_id()]),
8562                &IndexSet::new(),
8563                &clients,
8564            );
8565
8566            assert_eq!(
8567                plan.discrepancy_keys,
8568                IndexSet::from([(position.instrument_id, position.account_id)])
8569            );
8570            assert!(plan.queries.is_empty());
8571            assert_eq!(
8572                check
8573                    .activity_revisions
8574                    .get(&(position.instrument_id, position.account_id)),
8575                Some(&0)
8576            );
8577        }
8578
8579        #[rstest]
8580        fn test_prepare_position_report_check_uses_live_client_bulk_coverage() {
8581            let clock = Rc::new(RefCell::new(VirtualClock::new()));
8582            let cache = Rc::new(RefCell::new(Cache::default()));
8583            let manager =
8584                ExecutionManager::new(clock, cache.clone(), ExecutionManagerConfig::default())
8585                    .expect("valid config");
8586            let derivative = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
8587            let spot = test_bybit_spot_instrument();
8588            cache
8589                .borrow_mut()
8590                .add_instrument(derivative.clone())
8591                .unwrap();
8592            cache.borrow_mut().add_instrument(spot.clone()).unwrap();
8593            let derivative_position = insert_open_position(
8594                &cache,
8595                &derivative,
8596                PositionId::from("P-DERIVATIVE-COVERAGE"),
8597                OrderSide::Buy,
8598                "5.0",
8599                "3000.00",
8600            );
8601            let spot_position = insert_open_position(
8602                &cache,
8603                &spot,
8604                PositionId::from("P-SPOT-COVERAGE"),
8605                OrderSide::Buy,
8606                "2.0",
8607                "2000.00",
8608            );
8609            let client = LiveExecutionClient::new(Box::new(PositionCoverageStubClient));
8610            let client: &dyn ExecutionClient = &client;
8611
8612            let check = manager.prepare_position_report_check(UUID4::new(), &[client]);
8613            let client_id = ClientId::from("BYBIT");
8614
8615            assert_eq!(
8616                check.client_coverage.get(&(
8617                    derivative_position.instrument_id,
8618                    derivative_position.account_id
8619                )),
8620                Some(&ReportClientCoverage::Resolved(IndexSet::from([client_id])))
8621            );
8622            assert_eq!(
8623                check
8624                    .client_coverage
8625                    .get(&(spot_position.instrument_id, spot_position.account_id)),
8626                Some(&ReportClientCoverage::Unavailable(IndexSet::from([
8627                    client_id
8628                ])))
8629            );
8630        }
8631    }
8632}