Skip to main content

nautilus_common/clients/
execution.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 client trait definition.
17
18use anyhow::Context;
19use async_trait::async_trait;
20use nautilus_core::{
21    Params, UnixNanos, datetime::checked_mins_to_nanos, time::get_atomic_clock_realtime,
22};
23use nautilus_model::{
24    accounts::AccountAny,
25    enums::{LiquiditySide, OmsType},
26    identifiers::{
27        AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
28    },
29    instruments::InstrumentAny,
30    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
31    types::{AccountBalance, MarginBalance, Money, Price, Quantity},
32};
33use rust_decimal::Decimal;
34
35use super::log_not_implemented;
36use crate::messages::execution::{
37    BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
38    GenerateFillReportsBuilder, GenerateOrderStatusReport, GenerateOrderStatusReports,
39    GenerateOrderStatusReportsBuilder, GeneratePositionStatusReports,
40    GeneratePositionStatusReportsBuilder, ModifyOrder, QueryAccount, QueryOrder, SubmitOrder,
41    SubmitOrderList,
42};
43
44/// Default maximum absolute position difference tolerated during reconciliation.
45pub const DEFAULT_POSITION_RECONCILIATION_TOLERANCE: Decimal =
46    Decimal::from_parts(1, 0, 0, false, 8);
47
48/// Defines the interface for an execution client managing order operations.
49///
50/// # Thread Safety
51///
52/// Client instances are not intended to be sent across threads. The `?Send` bound
53/// allows implementations to hold non-Send state for any Python interop.
54#[async_trait(?Send)]
55pub trait ExecutionClient {
56    fn is_connected(&self) -> bool;
57    fn client_id(&self) -> ClientId;
58    fn account_id(&self) -> AccountId;
59    fn venue(&self) -> Venue;
60    fn oms_type(&self) -> OmsType;
61    fn get_account(&self) -> Option<AccountAny>;
62
63    /// Returns the maximum absolute position difference tolerated during reconciliation.
64    fn position_reconciliation_tolerance(&self) -> Decimal {
65        DEFAULT_POSITION_RECONCILIATION_TOLERANCE
66    }
67
68    /// Returns whether this client can execute orders for the given instrument venue.
69    ///
70    /// Single-venue clients should use the default behavior. Routing brokers can
71    /// override this when their client venue identifies the broker rather than
72    /// the instrument's exchange venue.
73    fn handles_order_venue(&self, venue: Venue) -> bool {
74        self.venue() == venue
75    }
76
77    /// Returns whether a bulk position status report request provides complete coverage for the
78    /// given instrument, so that an absent report is evidence the position is flat.
79    fn provides_bulk_position_coverage(&self, _instrument_id: InstrumentId) -> bool {
80        true
81    }
82
83    /// Generates and publishes the account state event.
84    ///
85    /// Implementations may publish synchronously. Callers must release shared state borrows,
86    /// including clock and cache borrows, before calling this method because subscribers may
87    /// access the same state.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if generating the account state fails.
92    fn generate_account_state(
93        &self,
94        balances: Vec<AccountBalance>,
95        margins: Vec<MarginBalance>,
96        reported: bool,
97        ts_event: UnixNanos,
98        info: Option<Params>,
99    ) -> anyhow::Result<()>;
100
101    /// Starts the execution client.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the client fails to start.
106    fn start(&mut self) -> anyhow::Result<()>;
107
108    /// Stops the execution client.
109    ///
110    /// Implementations must be idempotent: the engine and node teardown paths
111    /// (e.g. backtest `end` -> `reset` -> `dispose`) may call `stop()` more
112    /// than once per run. Guard with an internal `is_stopped` check or
113    /// equivalent so repeated calls are safe.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the client fails to stop.
118    fn stop(&mut self) -> anyhow::Result<()>;
119
120    /// Resets the execution client to its initial state.
121    ///
122    /// The default implementation is a no-op. Adapters with reconnectable state
123    /// (caches, sequence counters, in-flight orders) should override this.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the client fails to reset.
128    fn reset(&mut self) -> anyhow::Result<()> {
129        Ok(())
130    }
131
132    /// Disposes of client resources and cleans up.
133    ///
134    /// The default implementation is a no-op. Adapters that hold async tasks,
135    /// background threads, or external handles should override this.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the client fails to dispose.
140    fn dispose(&mut self) -> anyhow::Result<()> {
141        Ok(())
142    }
143
144    /// Connects the client to the execution venue.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if connection fails.
149    async fn connect(&mut self) -> anyhow::Result<()> {
150        Ok(())
151    }
152
153    /// Disconnects the client from the execution venue.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if disconnection fails.
158    async fn disconnect(&mut self) -> anyhow::Result<()> {
159        Ok(())
160    }
161
162    /// Submits a single order command to the execution venue.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if submission fails.
167    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
168        log_not_implemented(&cmd);
169        Ok(())
170    }
171
172    /// Submits a list of orders to the execution venue.
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if submission fails.
177    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
178        log_not_implemented(&cmd);
179        Ok(())
180    }
181
182    /// Modifies an existing order.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if modification fails.
187    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
188        log_not_implemented(&cmd);
189        Ok(())
190    }
191
192    /// Modifies a batch of orders.
193    ///
194    /// The default implementation fans out to [`Self::modify_order`] so existing execution
195    /// clients remain compatible until they add native batch support.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if any child modification fails.
200    fn batch_modify_orders(&self, cmd: BatchModifyOrders) -> anyhow::Result<()> {
201        for modify in cmd.modifies {
202            self.modify_order(modify)?;
203        }
204        Ok(())
205    }
206
207    /// Cancels a specific order.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if cancellation fails.
212    fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
213        log_not_implemented(&cmd);
214        Ok(())
215    }
216
217    /// Cancels all orders.
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if cancellation fails.
222    fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
223        log_not_implemented(&cmd);
224        Ok(())
225    }
226
227    /// Cancels a batch of orders.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error if batch cancellation fails.
232    fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
233        log_not_implemented(&cmd);
234        Ok(())
235    }
236
237    /// Queries the status of an account.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if the query fails.
242    fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
243        log_not_implemented(&cmd);
244        Ok(())
245    }
246
247    /// Queries the status of an order.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if the query fails.
252    fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
253        log_not_implemented(&cmd);
254        Ok(())
255    }
256
257    /// Generates a single order status report.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if report generation fails.
262    async fn generate_order_status_report(
263        &self,
264        cmd: &GenerateOrderStatusReport,
265    ) -> anyhow::Result<Option<OrderStatusReport>> {
266        log_not_implemented(cmd);
267        Ok(None)
268    }
269
270    /// Generates multiple order status reports.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if report generation fails.
275    async fn generate_order_status_reports(
276        &self,
277        cmd: &GenerateOrderStatusReports,
278    ) -> anyhow::Result<Vec<OrderStatusReport>> {
279        log_not_implemented(cmd);
280        Ok(Vec::new())
281    }
282
283    /// Generates fill reports based on execution results.
284    ///
285    /// # Errors
286    ///
287    /// Returns an error if fill report generation fails.
288    async fn generate_fill_reports(
289        &self,
290        cmd: GenerateFillReports,
291    ) -> anyhow::Result<Vec<FillReport>> {
292        log_not_implemented(&cmd);
293        Ok(Vec::new())
294    }
295
296    /// Generates position status reports.
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if generation fails.
301    async fn generate_position_status_reports(
302        &self,
303        cmd: &GeneratePositionStatusReports,
304    ) -> anyhow::Result<Vec<PositionStatusReport>> {
305        log_not_implemented(cmd);
306        Ok(Vec::new())
307    }
308
309    /// Generates mass status for executions.
310    ///
311    /// The default composes the granular report generators using the realtime atomic clock.
312    /// This is clock-correct only for live/realtime clients; clients using a mocked or backtest
313    /// clock must override this method to compose reports with their own clock.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if status generation fails.
318    async fn generate_mass_status(
319        &self,
320        lookback_mins: Option<u64>,
321    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
322        let ts_init = get_atomic_clock_realtime().get_time_ns();
323        let start = lookback_mins
324            .map(|mins| {
325                checked_mins_to_nanos(mins)
326                    .map(|lookback_ns| {
327                        UnixNanos::from(ts_init.as_u64().saturating_sub(lookback_ns))
328                    })
329                    .ok_or_else(|| anyhow::anyhow!("lookback minutes overflow nanoseconds: {mins}"))
330            })
331            .transpose()?;
332
333        let order_cmd = GenerateOrderStatusReportsBuilder::default()
334            .ts_init(ts_init)
335            .open_only(false)
336            .start(start)
337            .build()
338            .context("failed to build order status reports command")?;
339        let fill_cmd = GenerateFillReportsBuilder::default()
340            .ts_init(ts_init)
341            .start(start)
342            .build()
343            .context("failed to build fill reports command")?;
344        let position_cmd = GeneratePositionStatusReportsBuilder::default()
345            .ts_init(ts_init)
346            .start(start)
347            .build()
348            .context("failed to build position status reports command")?;
349
350        let (order_reports, fill_reports, position_reports) = futures::try_join!(
351            async {
352                self.generate_order_status_reports(&order_cmd)
353                    .await
354                    .context("failed to generate order status reports")
355            },
356            async {
357                self.generate_fill_reports(fill_cmd)
358                    .await
359                    .context("failed to generate fill reports")
360            },
361            async {
362                self.generate_position_status_reports(&position_cmd)
363                    .await
364                    .context("failed to generate position status reports")
365            },
366        )?;
367
368        let mut mass_status = ExecutionMassStatus::new(
369            self.client_id(),
370            self.account_id(),
371            self.venue(),
372            ts_init,
373            None,
374        );
375        mass_status.add_order_reports(order_reports);
376        mass_status.add_fill_reports(fill_reports);
377        mass_status.add_position_reports(position_reports);
378
379        Ok(Some(mass_status))
380    }
381
382    /// Registers an external order for tracking by the execution client.
383    ///
384    /// This is called after reconciliation creates an external order, allowing the
385    /// execution client to track it for subsequent events (e.g., cancellations).
386    fn register_external_order(
387        &self,
388        _client_order_id: ClientOrderId,
389        _venue_order_id: VenueOrderId,
390        _instrument_id: InstrumentId,
391        _strategy_id: StrategyId,
392        _ts_init: UnixNanos,
393    ) {
394        // Default no-op implementation
395    }
396
397    /// Handles an instrument update received via the message bus.
398    ///
399    /// Exec clients that need live instrument updates (e.g. for internal maps)
400    /// can override this to process instruments for their venue.
401    fn on_instrument(&mut self, _instrument: InstrumentAny) {
402        // Default no-op
403    }
404
405    /// Calculates the commission for a reconciliation fill.
406    ///
407    /// Override this method to provide venue-specific commission logic
408    /// for inferred fills generated during reconciliation.
409    /// The quantity, price, and liquidity side match the inferred fill event,
410    /// including any price derived for only the unbooked incremental quantity.
411    ///
412    /// Returns `Ok(None)` by default, signaling callers to use their own
413    /// generic commission formula. An error means the venue formula applies
414    /// but its result could not be represented, so callers must not substitute
415    /// a zero or generic commission for it.
416    ///
417    /// # Errors
418    ///
419    /// Returns an error if the venue commission cannot be calculated or represented.
420    #[expect(unused_variables)]
421    fn calculate_commission(
422        &self,
423        instrument: &InstrumentAny,
424        last_qty: Quantity,
425        last_px: Price,
426        liquidity_side: LiquiditySide,
427    ) -> anyhow::Result<Option<Money>> {
428        Ok(None)
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use std::{cell::RefCell, rc::Rc};
435
436    use nautilus_core::UUID4;
437    use nautilus_model::{
438        enums::{
439            LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce,
440        },
441        identifiers::{PositionId, TradeId, TraderId, Venue},
442        types::Currency,
443    };
444    use rstest::rstest;
445
446    use super::*;
447
448    struct RecordingExecutionClient {
449        modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>,
450    }
451
452    impl RecordingExecutionClient {
453        fn new(modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>) -> Self {
454            Self { modified_order_ids }
455        }
456    }
457
458    #[async_trait(?Send)]
459    impl ExecutionClient for RecordingExecutionClient {
460        fn is_connected(&self) -> bool {
461            true
462        }
463
464        fn client_id(&self) -> ClientId {
465            ClientId::from("TEST")
466        }
467
468        fn account_id(&self) -> AccountId {
469            AccountId::from("TEST-001")
470        }
471
472        fn venue(&self) -> Venue {
473            Venue::from("SIM")
474        }
475
476        fn oms_type(&self) -> OmsType {
477            OmsType::Netting
478        }
479
480        fn get_account(&self) -> Option<AccountAny> {
481            None
482        }
483
484        fn generate_account_state(
485            &self,
486            _balances: Vec<AccountBalance>,
487            _margins: Vec<MarginBalance>,
488            _reported: bool,
489            _ts_event: UnixNanos,
490            _info: Option<Params>,
491        ) -> anyhow::Result<()> {
492            Ok(())
493        }
494
495        fn start(&mut self) -> anyhow::Result<()> {
496            Ok(())
497        }
498
499        fn stop(&mut self) -> anyhow::Result<()> {
500            Ok(())
501        }
502
503        fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
504            self.modified_order_ids
505                .borrow_mut()
506                .push(cmd.client_order_id);
507
508            Ok(())
509        }
510    }
511
512    struct MassStatusExecutionClient {
513        order_commands: RefCell<Vec<GenerateOrderStatusReports>>,
514        fill_requests: RefCell<Vec<GenerateFillReports>>,
515        position_queries: RefCell<Vec<GeneratePositionStatusReports>>,
516        fail_fill: bool,
517    }
518
519    impl MassStatusExecutionClient {
520        fn new(fail_fill: bool) -> Self {
521            Self {
522                order_commands: RefCell::new(Vec::new()),
523                fill_requests: RefCell::new(Vec::new()),
524                position_queries: RefCell::new(Vec::new()),
525                fail_fill,
526            }
527        }
528    }
529
530    #[async_trait(?Send)]
531    impl ExecutionClient for MassStatusExecutionClient {
532        fn is_connected(&self) -> bool {
533            true
534        }
535
536        fn client_id(&self) -> ClientId {
537            ClientId::from("MASS-STATUS")
538        }
539
540        fn account_id(&self) -> AccountId {
541            AccountId::from("MASS-STATUS-001")
542        }
543
544        fn venue(&self) -> Venue {
545            Venue::from("SIM")
546        }
547
548        fn oms_type(&self) -> OmsType {
549            OmsType::Netting
550        }
551
552        fn get_account(&self) -> Option<AccountAny> {
553            None
554        }
555
556        fn generate_account_state(
557            &self,
558            _balances: Vec<AccountBalance>,
559            _margins: Vec<MarginBalance>,
560            _reported: bool,
561            _ts_event: UnixNanos,
562            _info: Option<Params>,
563        ) -> anyhow::Result<()> {
564            Ok(())
565        }
566
567        fn start(&mut self) -> anyhow::Result<()> {
568            Ok(())
569        }
570
571        fn stop(&mut self) -> anyhow::Result<()> {
572            Ok(())
573        }
574
575        async fn generate_order_status_reports(
576            &self,
577            cmd: &GenerateOrderStatusReports,
578        ) -> anyhow::Result<Vec<OrderStatusReport>> {
579            self.order_commands.borrow_mut().push(cmd.clone());
580            Ok(vec![test_order_report()])
581        }
582
583        async fn generate_fill_reports(
584            &self,
585            cmd: GenerateFillReports,
586        ) -> anyhow::Result<Vec<FillReport>> {
587            self.fill_requests.borrow_mut().push(cmd);
588
589            if self.fail_fill {
590                anyhow::bail!("sentinel fill report failure");
591            }
592            Ok(vec![test_fill_report()])
593        }
594
595        async fn generate_position_status_reports(
596            &self,
597            cmd: &GeneratePositionStatusReports,
598        ) -> anyhow::Result<Vec<PositionStatusReport>> {
599            self.position_queries.borrow_mut().push(cmd.clone());
600            Ok(vec![test_position_report()])
601        }
602    }
603
604    fn test_order_report() -> OrderStatusReport {
605        OrderStatusReport::new(
606            AccountId::from("MASS-STATUS-001"),
607            InstrumentId::from("AUD/USD.SIM"),
608            None,
609            VenueOrderId::from("ORDER-001"),
610            OrderSide::Buy.into(),
611            OrderType::Limit,
612            TimeInForce::Gtc,
613            OrderStatus::Accepted,
614            Quantity::from("10"),
615            Quantity::from("0"),
616            UnixNanos::from(1_000_000_000),
617            UnixNanos::from(2_000_000_000),
618            UnixNanos::from(3_000_000_000),
619            None,
620        )
621    }
622
623    fn test_fill_report() -> FillReport {
624        FillReport::new(
625            AccountId::from("MASS-STATUS-001"),
626            InstrumentId::from("AUD/USD.SIM"),
627            VenueOrderId::from("ORDER-001"),
628            TradeId::from("TRADE-001"),
629            OrderSide::Buy,
630            Quantity::from("5"),
631            Price::from("1.00010"),
632            Money::new(1.0, Currency::USD()),
633            LiquiditySide::Taker,
634            None,
635            None,
636            UnixNanos::from(4_000_000_000),
637            UnixNanos::from(5_000_000_000),
638            None,
639        )
640    }
641
642    fn test_position_report() -> PositionStatusReport {
643        PositionStatusReport::new(
644            AccountId::from("MASS-STATUS-001"),
645            InstrumentId::from("AUD/USD.SIM"),
646            PositionSide::Long,
647            Quantity::from("5"),
648            UnixNanos::from(6_000_000_000),
649            UnixNanos::from(7_000_000_000),
650            None,
651            Some(PositionId::from("POSITION-001")),
652            None,
653        )
654    }
655
656    #[rstest]
657    fn batch_modify_orders_default_fans_out_to_modify_order() {
658        let modified_order_ids = Rc::new(RefCell::new(Vec::new()));
659        let client = RecordingExecutionClient::new(modified_order_ids.clone());
660        let instrument_id = InstrumentId::from("AUD/USD.SIM");
661        let order1 = ClientOrderId::from("O-DEFAULT-BATCH-001");
662        let order2 = ClientOrderId::from("O-DEFAULT-BATCH-002");
663        let command = BatchModifyOrders::new(
664            TraderId::from("TRADER-001"),
665            Some(ClientId::from("TEST")),
666            StrategyId::from("S-001"),
667            instrument_id,
668            vec![
669                ModifyOrder::new(
670                    TraderId::from("TRADER-001"),
671                    Some(ClientId::from("TEST")),
672                    StrategyId::from("S-001"),
673                    instrument_id,
674                    order1,
675                    None,
676                    Some(Quantity::from("10")),
677                    Some(Price::from("1.00010")),
678                    None,
679                    UUID4::new(),
680                    UnixNanos::default(),
681                    None,
682                    None,
683                ),
684                ModifyOrder::new(
685                    TraderId::from("TRADER-001"),
686                    Some(ClientId::from("TEST")),
687                    StrategyId::from("S-001"),
688                    instrument_id,
689                    order2,
690                    None,
691                    Some(Quantity::from("20")),
692                    Some(Price::from("1.00020")),
693                    None,
694                    UUID4::new(),
695                    UnixNanos::default(),
696                    None,
697                    None,
698                ),
699            ],
700            UUID4::new(),
701            UnixNanos::default(),
702            None,
703            None,
704        );
705
706        client.batch_modify_orders(command).unwrap();
707
708        assert_eq!(modified_order_ids.borrow().as_slice(), &[order1, order2]);
709    }
710
711    #[rstest]
712    fn generate_mass_status_default_composes_granular_reports() {
713        let client = MassStatusExecutionClient::new(false);
714
715        let mass_status = futures::executor::block_on(client.generate_mass_status(Some(5)))
716            .unwrap()
717            .unwrap();
718
719        assert_eq!(mass_status.client_id, ClientId::from("MASS-STATUS"));
720        assert_eq!(mass_status.account_id, AccountId::from("MASS-STATUS-001"));
721        assert_eq!(mass_status.venue, Venue::from("SIM"));
722
723        let order_reports = mass_status.order_reports();
724        let fill_reports = mass_status.fill_reports();
725        let position_reports = mass_status.position_reports();
726        let order_report = order_reports.get(&VenueOrderId::from("ORDER-001")).unwrap();
727        let fill_report = &fill_reports.get(&VenueOrderId::from("ORDER-001")).unwrap()[0];
728        let position_report = &position_reports
729            .get(&InstrumentId::from("AUD/USD.SIM"))
730            .unwrap()[0];
731        assert_eq!(order_reports.len(), 1);
732        assert_eq!(fill_reports.len(), 1);
733        assert_eq!(position_reports.len(), 1);
734        assert_eq!(
735            order_report.instrument_id,
736            InstrumentId::from("AUD/USD.SIM")
737        );
738        assert_eq!(fill_report.trade_id, TradeId::from("TRADE-001"));
739        assert_eq!(
740            position_report.venue_position_id,
741            Some(PositionId::from("POSITION-001")),
742        );
743
744        let order_commands = client.order_commands.borrow();
745        let fill_requests = client.fill_requests.borrow();
746        let position_queries = client.position_queries.borrow();
747        assert_eq!(order_commands.len(), 1);
748        assert_eq!(fill_requests.len(), 1);
749        assert_eq!(position_queries.len(), 1);
750
751        let order_cmd = &order_commands[0];
752        let fill_cmd = &fill_requests[0];
753        let position_cmd = &position_queries[0];
754        assert_eq!(order_cmd.ts_init, mass_status.ts_init);
755        assert_eq!(fill_cmd.ts_init, mass_status.ts_init);
756        assert_eq!(position_cmd.ts_init, mass_status.ts_init);
757        assert_ne!(test_order_report().ts_init, mass_status.ts_init);
758        assert_ne!(test_fill_report().ts_init, mass_status.ts_init);
759        assert_ne!(test_position_report().ts_init, mass_status.ts_init);
760
761        let expected_start = UnixNanos::from(
762            mass_status
763                .ts_init
764                .as_u64()
765                .saturating_sub(checked_mins_to_nanos(5).unwrap()),
766        );
767        assert_eq!(order_cmd.start, Some(expected_start));
768        assert_eq!(fill_cmd.start, Some(expected_start));
769        assert_eq!(position_cmd.start, Some(expected_start));
770        assert!(!order_cmd.open_only);
771        assert!(order_cmd.instrument_id.is_none());
772        assert!(order_cmd.end.is_none());
773        assert!(order_cmd.params.is_none());
774        assert!(fill_cmd.instrument_id.is_none());
775        assert!(fill_cmd.venue_order_id.is_none());
776        assert!(fill_cmd.end.is_none());
777        assert!(fill_cmd.params.is_none());
778        assert!(position_cmd.instrument_id.is_none());
779        assert!(position_cmd.end.is_none());
780        assert!(position_cmd.params.is_none());
781    }
782
783    #[rstest]
784    fn generate_mass_status_default_propagates_granular_error() {
785        let client = MassStatusExecutionClient::new(true);
786
787        let error = futures::executor::block_on(client.generate_mass_status(Some(5))).unwrap_err();
788
789        let error_chain = format!("{error:#}");
790        assert!(error_chain.contains("failed to generate fill reports"));
791        assert!(error_chain.contains("sentinel fill report failure"));
792    }
793}