Skip to main content

nautilus_live/python/client/
mod.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//! Connects custom Python adapters to the Rust live node through PyO3.
17//!
18//! Implements the Rust data and execution client traits by forwarding operations to Python
19//! clients, and manages factory construction, event-loop binding, and client lifetimes. Adapters
20//! receive a read-only cache view and submit typed output to the live runner for processing
21//! inside the synchronous core boundary.
22
23pub mod requests;
24pub mod responses;
25
26use std::{
27    any::Any,
28    cell::RefCell,
29    collections::HashMap,
30    rc::Rc,
31    sync::{
32        Arc,
33        atomic::{AtomicU64, Ordering},
34    },
35    thread::{self, ThreadId},
36};
37
38use async_trait::async_trait;
39use nautilus_common::{
40    cache::{Cache, CacheView},
41    clients::{
42        DEFAULT_POSITION_RECONCILIATION_TOLERANCE, DataClient, ExecutionClient,
43        generate_mass_status,
44    },
45    clock::Clock,
46    factories::{ClientConfig, DataClientFactory, ExecutionClientFactory, OrderEventFactory},
47    live::{
48        runner::{get_data_event_sender, get_exec_event_sender},
49        sender::EventSender,
50    },
51    messages::{
52        DataEvent, ExecutionEvent, ExecutionReport,
53        data::{
54            DataRequestFailed, DataResponseEnvelope, RequestBars, RequestBookDeltas,
55            RequestBookDepth, RequestBookSnapshot, RequestCustomData, RequestFundingRates,
56            RequestInstrument, RequestInstruments, RequestOptionChainReferencePrice, RequestQuotes,
57            RequestTrades, SubscribeBars, SubscribeBookDeltas, SubscribeBookDepth,
58            SubscribeCustomData, SubscribeFundingRates, SubscribeIndexPrices, SubscribeInstrument,
59            SubscribeInstrumentClose, SubscribeInstrumentStatus, SubscribeInstruments,
60            SubscribeMarkPrices, SubscribeOptionGreeks, SubscribeQuotes, SubscribeTrades,
61            UnsubscribeBars, UnsubscribeBookDeltas, UnsubscribeBookDepth, UnsubscribeCustomData,
62            UnsubscribeFundingRates, UnsubscribeIndexPrices, UnsubscribeInstrument,
63            UnsubscribeInstrumentClose, UnsubscribeInstrumentStatus, UnsubscribeInstruments,
64            UnsubscribeMarkPrices, UnsubscribeOptionGreeks, UnsubscribeQuotes, UnsubscribeTrades,
65        },
66        execution::{
67            BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder,
68            GenerateFillReports, GenerateOrderStatusReport, GenerateOrderStatusReports,
69            GeneratePositionStatusReports, ModifyOrder, QueryAccount, QueryOrder, SubmitOrder,
70            SubmitOrderList,
71        },
72    },
73    python::clock::PyClock,
74};
75use nautilus_core::{
76    Params, UnixNanos,
77    python::{params::pydict_to_params, to_pyruntime_err, to_pytype_err},
78};
79use nautilus_model::{
80    accounts::AccountAny,
81    data::{
82        Bar, CustomData, Data, FundingRateUpdate, IndexPriceUpdate, InstrumentClose,
83        InstrumentStatus, MarkPriceUpdate, OptionGreeks, OrderBookDelta, OrderBookDeltas,
84        OrderBookDepth, QuoteTick, TradeTick,
85    },
86    enums::{AccountType, LiquiditySide, OmsType, OrderSide, PositionSide},
87    events::{
88        AccountState, OrderAccepted, OrderAcceptedBatch, OrderCanceled, OrderCanceledBatch,
89        OrderSubmitted, OrderSubmittedBatch,
90    },
91    identifiers::{
92        AccountId, ClientId, ClientOrderId, InstrumentId, OrderListId, PositionId, StrategyId,
93        TradeId, TraderId, Venue, VenueOrderId,
94    },
95    instruments::InstrumentAny,
96    orderbook::OrderBook,
97    orders::OrderList,
98    position::Position,
99    python::{
100        account::account_any_to_pyobject,
101        events::order::pyobject_to_order_event,
102        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
103        orders::{order_any_to_pyobject, pyobject_to_order_any},
104    },
105    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
106    types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
107};
108use parking_lot::Mutex;
109use pyo3::{
110    prelude::*,
111    types::{PyCFunction, PyDict, PyTuple},
112};
113use rust_decimal::Decimal;
114
115use self::{
116    requests::{
117        PyRequestBars, PyRequestBookDeltas, PyRequestBookDepth, PyRequestBookSnapshot,
118        PyRequestCustomData, PyRequestFundingRates, PyRequestInstrument, PyRequestInstruments,
119        PyRequestOptionChainReferencePrice, PyRequestQuotes, PyRequestTrades,
120    },
121    responses::extract_response,
122};
123use super::runtime::PythonOperation;
124
125static CACHE_ID: AtomicU64 = AtomicU64::new(1);
126
127thread_local! {
128    static CLIENT_CACHES: RefCell<HashMap<u64, CacheView>> = RefCell::new(HashMap::new());
129}
130
131/// Read-only access to the owning node's cache.
132#[pyclass(name = "ClientCache", module = "nautilus_trader.live", frozen)]
133#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
134#[derive(Debug)]
135pub struct PyClientCache {
136    id: u64,
137}
138
139#[pymethods]
140#[pyo3_stub_gen::derive::gen_stub_pymethods]
141impl PyClientCache {
142    #[pyo3(name = "instrument")]
143    fn py_instrument(
144        &self,
145        py: Python<'_>,
146        instrument_id: InstrumentId,
147    ) -> PyResult<Option<Py<PyAny>>> {
148        let view = self.view()?;
149        let instrument = view
150            .try_borrow()
151            .map_err(to_pyruntime_err)?
152            .instrument(&instrument_id)
153            .cloned();
154        instrument
155            .map(|instrument| instrument_any_to_pyobject(py, instrument))
156            .transpose()
157    }
158
159    #[pyo3(name = "quote", signature = (instrument_id, index=0))]
160    fn py_quote(&self, instrument_id: InstrumentId, index: usize) -> PyResult<Option<QuoteTick>> {
161        let view = self.view()?;
162        Ok(view
163            .try_borrow()
164            .map_err(to_pyruntime_err)?
165            .quote_at_index(&instrument_id, index)
166            .copied())
167    }
168    #[pyo3(name = "get")]
169    fn py_get(&self, key: &str) -> PyResult<Option<Vec<u8>>> {
170        self.read(|cache| {
171            cache
172                .get(key)
173                .map(|value| value.map(|bytes| bytes.to_vec()))
174        })?
175        .map_err(to_pyruntime_err)
176    }
177
178    #[pyo3(name = "instruments", signature = (venue=None))]
179    fn py_instruments(&self, py: Python<'_>, venue: Option<Venue>) -> PyResult<Vec<Py<PyAny>>> {
180        let instruments = self.read(|cache| {
181            cache
182                .instrument_ids(venue.as_ref())
183                .into_iter()
184                .filter_map(|id| cache.instrument(id).cloned())
185                .collect::<Vec<_>>()
186        })?;
187
188        instruments
189            .into_iter()
190            .map(|instrument| instrument_any_to_pyobject(py, instrument))
191            .collect()
192    }
193
194    #[pyo3(name = "instrument_ids", signature = (venue=None))]
195    fn py_instrument_ids(&self, venue: Option<Venue>) -> PyResult<Vec<InstrumentId>> {
196        self.read(|cache| {
197            cache
198                .instrument_ids(venue.as_ref())
199                .into_iter()
200                .copied()
201                .collect()
202        })
203    }
204
205    #[pyo3(name = "account")]
206    fn py_account(&self, py: Python<'_>, account_id: AccountId) -> PyResult<Option<Py<PyAny>>> {
207        self.read(|cache| cache.account_owned(&account_id))?
208            .map(|account| account_any_to_pyobject(py, account))
209            .transpose()
210    }
211
212    #[pyo3(name = "order")]
213    fn py_order(
214        &self,
215        py: Python<'_>,
216        client_order_id: ClientOrderId,
217    ) -> PyResult<Option<Py<PyAny>>> {
218        self.read(|cache| cache.order_owned(&client_order_id))?
219            .map(|order| order_any_to_pyobject(py, order))
220            .transpose()
221    }
222
223    #[pyo3(name = "client_order_id")]
224    fn py_client_order_id(&self, venue_order_id: VenueOrderId) -> PyResult<Option<ClientOrderId>> {
225        self.read(|cache| cache.client_order_id(&venue_order_id).copied())
226    }
227
228    #[pyo3(name = "venue_order_id")]
229    fn py_venue_order_id(&self, client_order_id: ClientOrderId) -> PyResult<Option<VenueOrderId>> {
230        self.read(|cache| cache.venue_order_id(&client_order_id).copied())
231    }
232
233    #[pyo3(name = "position_id")]
234    fn py_position_id(&self, client_order_id: ClientOrderId) -> PyResult<Option<PositionId>> {
235        self.read(|cache| cache.position_id(&client_order_id).copied())
236    }
237
238    #[pyo3(name = "strategy_id_for_order")]
239    fn py_strategy_id_for_order(
240        &self,
241        client_order_id: ClientOrderId,
242    ) -> PyResult<Option<StrategyId>> {
243        self.read(|cache| cache.strategy_id_for_order(&client_order_id).copied())
244    }
245
246    #[pyo3(name = "order_list")]
247    fn py_order_list(&self, order_list_id: OrderListId) -> PyResult<Option<OrderList>> {
248        self.read(|cache| cache.order_list(&order_list_id).cloned())
249    }
250
251    #[pyo3(name = "order_book")]
252    fn py_order_book(&self, instrument_id: InstrumentId) -> PyResult<Option<OrderBook>> {
253        self.read(|cache| cache.order_book(&instrument_id).cloned())
254    }
255
256    #[pyo3(name = "position")]
257    fn py_position(&self, position_id: PositionId) -> PyResult<Option<Position>> {
258        self.read(|cache| cache.position_owned(&position_id))
259    }
260
261    #[pyo3(name = "orders", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
262    fn py_orders(
263        &self,
264        py: Python<'_>,
265        venue: Option<Venue>,
266        instrument_id: Option<InstrumentId>,
267        strategy_id: Option<StrategyId>,
268        account_id: Option<AccountId>,
269        side: Option<OrderSide>,
270    ) -> PyResult<Vec<Py<PyAny>>> {
271        let orders = self.read(|cache| {
272            cache
273                .orders(
274                    venue.as_ref(),
275                    instrument_id.as_ref(),
276                    strategy_id.as_ref(),
277                    account_id.as_ref(),
278                    side,
279                )
280                .into_iter()
281                .map(|value| value.clone())
282                .collect::<Vec<_>>()
283        })?;
284
285        orders
286            .into_iter()
287            .map(|order| order_any_to_pyobject(py, order))
288            .collect()
289    }
290
291    #[pyo3(name = "orders_open", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
292    fn py_orders_open(
293        &self,
294        py: Python<'_>,
295        venue: Option<Venue>,
296        instrument_id: Option<InstrumentId>,
297        strategy_id: Option<StrategyId>,
298        account_id: Option<AccountId>,
299        side: Option<OrderSide>,
300    ) -> PyResult<Vec<Py<PyAny>>> {
301        let orders = self.read(|cache| {
302            cache
303                .orders_open(
304                    venue.as_ref(),
305                    instrument_id.as_ref(),
306                    strategy_id.as_ref(),
307                    account_id.as_ref(),
308                    side,
309                )
310                .into_iter()
311                .map(|value| value.clone())
312                .collect::<Vec<_>>()
313        })?;
314
315        orders
316            .into_iter()
317            .map(|order| order_any_to_pyobject(py, order))
318            .collect()
319    }
320
321    #[pyo3(name = "orders_inflight", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
322    fn py_orders_inflight(
323        &self,
324        py: Python<'_>,
325        venue: Option<Venue>,
326        instrument_id: Option<InstrumentId>,
327        strategy_id: Option<StrategyId>,
328        account_id: Option<AccountId>,
329        side: Option<OrderSide>,
330    ) -> PyResult<Vec<Py<PyAny>>> {
331        let orders = self.read(|cache| {
332            cache
333                .orders_inflight(
334                    venue.as_ref(),
335                    instrument_id.as_ref(),
336                    strategy_id.as_ref(),
337                    account_id.as_ref(),
338                    side,
339                )
340                .into_iter()
341                .map(|value| value.clone())
342                .collect::<Vec<_>>()
343        })?;
344
345        orders
346            .into_iter()
347            .map(|order| order_any_to_pyobject(py, order))
348            .collect()
349    }
350
351    #[pyo3(name = "orders_open_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
352    fn py_orders_open_count(
353        &self,
354        venue: Option<Venue>,
355        instrument_id: Option<InstrumentId>,
356        strategy_id: Option<StrategyId>,
357        account_id: Option<AccountId>,
358        side: Option<OrderSide>,
359    ) -> PyResult<usize> {
360        self.read(|cache| {
361            cache.orders_open_count(
362                venue.as_ref(),
363                instrument_id.as_ref(),
364                strategy_id.as_ref(),
365                account_id.as_ref(),
366                side,
367            )
368        })
369    }
370
371    #[pyo3(name = "client_order_ids_open", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
372    fn py_client_order_ids_open(
373        &self,
374        venue: Option<Venue>,
375        instrument_id: Option<InstrumentId>,
376        strategy_id: Option<StrategyId>,
377        account_id: Option<AccountId>,
378    ) -> PyResult<Vec<ClientOrderId>> {
379        self.read(|cache| {
380            cache
381                .client_order_ids_open(
382                    venue.as_ref(),
383                    instrument_id.as_ref(),
384                    strategy_id.as_ref(),
385                    account_id.as_ref(),
386                )
387                .into_iter()
388                .collect()
389        })
390    }
391
392    #[pyo3(name = "positions_open", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
393    fn py_positions_open(
394        &self,
395        venue: Option<Venue>,
396        instrument_id: Option<InstrumentId>,
397        strategy_id: Option<StrategyId>,
398        account_id: Option<AccountId>,
399        side: Option<PositionSide>,
400    ) -> PyResult<Vec<Position>> {
401        self.read(|cache| {
402            cache
403                .positions_open(
404                    venue.as_ref(),
405                    instrument_id.as_ref(),
406                    strategy_id.as_ref(),
407                    account_id.as_ref(),
408                    side,
409                )
410                .into_iter()
411                .map(|value| value.clone())
412                .collect()
413        })
414    }
415}
416
417impl PyClientCache {
418    fn read<T>(&self, read: impl FnOnce(&Cache) -> T) -> PyResult<T> {
419        let view = self.view()?;
420        let cache = view.try_borrow().map_err(to_pyruntime_err)?;
421        Ok(read(&cache))
422    }
423
424    fn view(&self) -> PyResult<CacheView> {
425        CLIENT_CACHES
426            .with_borrow(|caches| caches.get(&self.id).cloned())
427            .ok_or_else(|| {
428                to_pyruntime_err("Client cache is disposed or accessed from a foreign thread")
429            })
430    }
431}
432
433/// Queues typed client output on its owner thread.
434#[pyclass(
435    name = "_ClientOutput",
436    module = "nautilus_trader.live",
437    frozen,
438    from_py_object
439)]
440#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
441#[derive(Debug, Clone)]
442pub struct ClientOutput {
443    state: Arc<Mutex<OutputState>>,
444    owner: ThreadId,
445}
446
447#[derive(Debug, Default)]
448struct OutputState {
449    sender: Option<EventSender<DataEvent>>,
450    bound: bool,
451    disposed: bool,
452    claimed: bool,
453    client_id: Option<ClientId>,
454    exec_sender: Option<EventSender<ExecutionEvent>>,
455    event_factory: Option<OrderEventFactory>,
456    venue: Option<Venue>,
457}
458
459#[pymethods]
460#[pyo3_stub_gen::derive::gen_stub_pymethods]
461impl ClientOutput {
462    #[new]
463    fn py_new() -> Self {
464        Self {
465            state: Arc::default(),
466            owner: thread::current().id(),
467        }
468    }
469
470    #[pyo3(name = "instrument")]
471    fn py_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
472        self.sender()?;
473        self.send(DataEvent::Data(
474            pyobject_to_instrument_any(py, instrument)?.into(),
475        ))
476    }
477
478    #[pyo3(name = "data")]
479    fn py_data(&self, data: &Bound<'_, PyAny>) -> PyResult<()> {
480        self.sender()?;
481        self.send(DataEvent::Data(extract_data(data)?))
482    }
483
484    #[pyo3(name = "response", signature = (response, completed=None, sub_correlation_id=None))]
485    fn py_response(
486        &self,
487        response: &Bound<'_, PyAny>,
488        completed: Option<bool>,
489        sub_correlation_id: Option<nautilus_core::UUID4>,
490    ) -> PyResult<()> {
491        self.sender()?;
492        let (client_id, response) = extract_response(response)?;
493        if self.state.lock().client_id != Some(client_id) {
494            return Err(to_pytype_err(
495                "Response client identity does not match its owner",
496            ));
497        }
498
499        let envelope =
500            DataResponseEnvelope::new(response, completed.unwrap_or(true), sub_correlation_id);
501        self.send(DataEvent::ResponseEnvelope(envelope))
502    }
503
504    /// Completes a failed data request, preserving an optional partial response.
505    #[pyo3(name = "request_failed", signature = (request_id, reason, response=None))]
506    fn py_request_failed(
507        &self,
508        request_id: nautilus_core::UUID4,
509        reason: String,
510        response: Option<&Bound<'_, PyAny>>,
511    ) -> PyResult<()> {
512        self.sender()?;
513        let response = response.map(extract_response).transpose()?;
514        if let Some((client_id, response)) = &response
515            && (self.state.lock().client_id != Some(*client_id)
516                || response.correlation_id() != &request_id)
517        {
518            return Err(to_pytype_err(
519                "Partial response identity does not match the failed request",
520            ));
521        }
522
523        self.send(DataEvent::RequestFailed(DataRequestFailed {
524            request_id,
525            reason,
526            response: response.map(|(_, response)| response),
527        }))
528    }
529
530    #[pyo3(name = "event")]
531    fn py_event(&self, py: Python<'_>, event: Py<PyAny>) -> PyResult<()> {
532        self.exec_sender()?;
533
534        let event = if event.bind(py).is_instance_of::<AccountState>() {
535            ExecutionEvent::Account(event.extract(py)?)
536        } else {
537            ExecutionEvent::Order(pyobject_to_order_event(py, event)?)
538        };
539
540        self.send_exec(event)
541    }
542
543    #[pyo3(name = "report", signature = (report, fills=None))]
544    fn py_report(&self, report: &Bound<'_, PyAny>, fills: Option<Vec<FillReport>>) -> PyResult<()> {
545        self.exec_sender()?;
546
547        let report = if report.is_instance_of::<OrderStatusReport>() {
548            let order = Box::new(report.extract()?);
549
550            match fills {
551                Some(fills) => ExecutionReport::OrderWithFills(order, fills),
552                None => ExecutionReport::Order(order),
553            }
554        } else if fills.is_some() {
555            return Err(to_pytype_err(
556                "Associated fills require an OrderStatusReport",
557            ));
558        } else if report.is_instance_of::<FillReport>() {
559            ExecutionReport::Fill(Box::new(report.extract()?))
560        } else if report.is_instance_of::<PositionStatusReport>() {
561            ExecutionReport::Position(Box::new(report.extract()?))
562        } else if report.is_instance_of::<ExecutionMassStatus>() {
563            ExecutionReport::MassStatus(Box::new(report.extract()?))
564        } else {
565            return Err(to_pytype_err(
566                "Expected a Nautilus execution report from the installed wheel",
567            ));
568        };
569
570        self.send_exec(ExecutionEvent::Report(report))
571    }
572
573    #[pyo3(name = "account_state", signature = (balances, margins, reported, ts_event, ts_init, info=None))]
574    #[expect(
575        clippy::too_many_arguments,
576        clippy::needless_pass_by_value,
577        reason = "typed event fields cross the PyO3 boundary together"
578    )]
579    fn py_account_state(
580        &self,
581        py: Python<'_>,
582        balances: Vec<AccountBalance>,
583        margins: Vec<MarginBalance>,
584        reported: bool,
585        ts_event: u64,
586        ts_init: u64,
587        info: Option<Py<PyDict>>,
588    ) -> PyResult<()> {
589        let factory = self.event_factory()?;
590        let info = info
591            .as_ref()
592            .map(|info| pydict_to_params(py, info))
593            .transpose()?
594            .flatten();
595        let state = factory.generate_account_state(
596            balances,
597            margins,
598            reported,
599            ts_event.into(),
600            ts_init.into(),
601            info,
602        );
603        self.send_exec(ExecutionEvent::Account(state))
604    }
605
606    #[pyo3(name = "order_submitted_batch")]
607    fn py_order_submitted_batch(&self, events: Vec<OrderSubmitted>) -> PyResult<()> {
608        self.send_exec(ExecutionEvent::OrderSubmittedBatch(
609            OrderSubmittedBatch::new(events),
610        ))
611    }
612
613    #[pyo3(name = "order_accepted_batch")]
614    fn py_order_accepted_batch(&self, events: Vec<OrderAccepted>) -> PyResult<()> {
615        self.send_exec(ExecutionEvent::OrderAcceptedBatch(OrderAcceptedBatch::new(
616            events,
617        )))
618    }
619
620    #[pyo3(name = "order_canceled_batch")]
621    fn py_order_canceled_batch(&self, events: Vec<OrderCanceled>) -> PyResult<()> {
622        self.send_exec(ExecutionEvent::OrderCanceledBatch(OrderCanceledBatch::new(
623            events,
624        )))
625    }
626
627    #[pyo3(name = "order_denied", signature = (order, reason, ts_init))]
628    fn py_order_denied(
629        &self,
630        py: Python<'_>,
631        order: Py<PyAny>,
632        reason: &str,
633        ts_init: u64,
634    ) -> PyResult<()> {
635        let factory = self.event_factory()?;
636        let order = pyobject_to_order_any(py, order)?;
637        let event = factory.generate_order_denied(&order, reason, ts_init.into());
638        self.send_exec(ExecutionEvent::Order(event))
639    }
640
641    #[pyo3(name = "order_submitted", signature = (order, ts_init))]
642    fn py_order_submitted(&self, py: Python<'_>, order: Py<PyAny>, ts_init: u64) -> PyResult<()> {
643        let factory = self.event_factory()?;
644        let order = pyobject_to_order_any(py, order)?;
645        let event = factory.generate_order_submitted(&order, ts_init.into());
646        self.send_exec(ExecutionEvent::Order(event))
647    }
648
649    #[pyo3(name = "order_rejected", signature = (order, reason, ts_event, ts_init, due_post_only))]
650    fn py_order_rejected(
651        &self,
652        py: Python<'_>,
653        order: Py<PyAny>,
654        reason: &str,
655        ts_event: u64,
656        ts_init: u64,
657        due_post_only: bool,
658    ) -> PyResult<()> {
659        let factory = self.event_factory()?;
660        let order = pyobject_to_order_any(py, order)?;
661        let event = factory.generate_order_rejected(
662            &order,
663            reason,
664            ts_event.into(),
665            ts_init.into(),
666            due_post_only,
667        );
668        self.send_exec(ExecutionEvent::Order(event))
669    }
670
671    #[pyo3(name = "order_accepted", signature = (order, venue_order_id, ts_event, ts_init))]
672    fn py_order_accepted(
673        &self,
674        py: Python<'_>,
675        order: Py<PyAny>,
676        venue_order_id: VenueOrderId,
677        ts_event: u64,
678        ts_init: u64,
679    ) -> PyResult<()> {
680        let factory = self.event_factory()?;
681        let order = pyobject_to_order_any(py, order)?;
682        let event = factory.generate_order_accepted(
683            &order,
684            venue_order_id,
685            ts_event.into(),
686            ts_init.into(),
687        );
688        self.send_exec(ExecutionEvent::Order(event))
689    }
690
691    #[pyo3(name = "order_modify_rejected", signature = (order, venue_order_id, reason, ts_event, ts_init))]
692    fn py_order_modify_rejected(
693        &self,
694        py: Python<'_>,
695        order: Py<PyAny>,
696        venue_order_id: Option<VenueOrderId>,
697        reason: &str,
698        ts_event: u64,
699        ts_init: u64,
700    ) -> PyResult<()> {
701        let factory = self.event_factory()?;
702        let order = pyobject_to_order_any(py, order)?;
703        let event = factory.generate_order_modify_rejected(
704            &order,
705            venue_order_id,
706            reason,
707            ts_event.into(),
708            ts_init.into(),
709        );
710        self.send_exec(ExecutionEvent::Order(event))
711    }
712
713    #[pyo3(name = "order_cancel_rejected", signature = (order, venue_order_id, reason, ts_event, ts_init))]
714    fn py_order_cancel_rejected(
715        &self,
716        py: Python<'_>,
717        order: Py<PyAny>,
718        venue_order_id: Option<VenueOrderId>,
719        reason: &str,
720        ts_event: u64,
721        ts_init: u64,
722    ) -> PyResult<()> {
723        let factory = self.event_factory()?;
724        let order = pyobject_to_order_any(py, order)?;
725        let event = factory.generate_order_cancel_rejected(
726            &order,
727            venue_order_id,
728            reason,
729            ts_event.into(),
730            ts_init.into(),
731        );
732        self.send_exec(ExecutionEvent::Order(event))
733    }
734
735    #[pyo3(name = "order_updated", signature = (order, venue_order_id, quantity, price, trigger_price, protection_price, ts_event, ts_init))]
736    #[expect(
737        clippy::too_many_arguments,
738        reason = "typed event fields cross the PyO3 boundary together"
739    )]
740    fn py_order_updated(
741        &self,
742        py: Python<'_>,
743        order: Py<PyAny>,
744        venue_order_id: VenueOrderId,
745        quantity: Quantity,
746        price: Option<Price>,
747        trigger_price: Option<Price>,
748        protection_price: Option<Price>,
749        ts_event: u64,
750        ts_init: u64,
751    ) -> PyResult<()> {
752        let factory = self.event_factory()?;
753        let order = pyobject_to_order_any(py, order)?;
754        let event = factory.generate_order_updated(
755            &order,
756            venue_order_id,
757            quantity,
758            price,
759            trigger_price,
760            protection_price,
761            ts_event.into(),
762            ts_init.into(),
763        );
764        self.send_exec(ExecutionEvent::Order(event))
765    }
766
767    #[pyo3(name = "order_canceled", signature = (order, venue_order_id, ts_event, ts_init))]
768    fn py_order_canceled(
769        &self,
770        py: Python<'_>,
771        order: Py<PyAny>,
772        venue_order_id: Option<VenueOrderId>,
773        ts_event: u64,
774        ts_init: u64,
775    ) -> PyResult<()> {
776        let factory = self.event_factory()?;
777        let order = pyobject_to_order_any(py, order)?;
778        let event = factory.generate_order_canceled(
779            &order,
780            venue_order_id,
781            ts_event.into(),
782            ts_init.into(),
783        );
784        self.send_exec(ExecutionEvent::Order(event))
785    }
786
787    #[pyo3(name = "order_triggered", signature = (order, venue_order_id, ts_event, ts_init))]
788    fn py_order_triggered(
789        &self,
790        py: Python<'_>,
791        order: Py<PyAny>,
792        venue_order_id: Option<VenueOrderId>,
793        ts_event: u64,
794        ts_init: u64,
795    ) -> PyResult<()> {
796        let factory = self.event_factory()?;
797        let order = pyobject_to_order_any(py, order)?;
798        let event = factory.generate_order_triggered(
799            &order,
800            venue_order_id,
801            ts_event.into(),
802            ts_init.into(),
803        );
804        self.send_exec(ExecutionEvent::Order(event))
805    }
806
807    #[pyo3(name = "order_expired", signature = (order, venue_order_id, ts_event, ts_init))]
808    fn py_order_expired(
809        &self,
810        py: Python<'_>,
811        order: Py<PyAny>,
812        venue_order_id: Option<VenueOrderId>,
813        ts_event: u64,
814        ts_init: u64,
815    ) -> PyResult<()> {
816        let factory = self.event_factory()?;
817        let order = pyobject_to_order_any(py, order)?;
818        let event =
819            factory.generate_order_expired(&order, venue_order_id, ts_event.into(), ts_init.into());
820        self.send_exec(ExecutionEvent::Order(event))
821    }
822
823    #[pyo3(name = "order_filled", signature = (order, venue_order_id, venue_position_id, trade_id, last_qty, last_px, quote_currency, commission, liquidity_side, ts_event, ts_init))]
824    #[expect(
825        clippy::too_many_arguments,
826        reason = "typed event fields cross the PyO3 boundary together"
827    )]
828    fn py_order_filled(
829        &self,
830        py: Python<'_>,
831        order: Py<PyAny>,
832        venue_order_id: VenueOrderId,
833        venue_position_id: Option<PositionId>,
834        trade_id: TradeId,
835        last_qty: Quantity,
836        last_px: Price,
837        quote_currency: Currency,
838        commission: Option<Money>,
839        liquidity_side: LiquiditySide,
840        ts_event: u64,
841        ts_init: u64,
842    ) -> PyResult<()> {
843        let factory = self.event_factory()?;
844        let order = pyobject_to_order_any(py, order)?;
845        let event = factory.generate_order_filled(
846            &order,
847            venue_order_id,
848            venue_position_id,
849            trade_id,
850            last_qty,
851            last_px,
852            quote_currency,
853            commission,
854            liquidity_side,
855            ts_event.into(),
856            ts_init.into(),
857        );
858        self.send_exec(ExecutionEvent::Order(event))
859    }
860}
861
862impl ClientOutput {
863    fn bind(&self) -> PyResult<()> {
864        if thread::current().id() != self.owner {
865            return Err(to_pyruntime_err("Client output requires its owner thread"));
866        }
867
868        let mut state = self.state.lock();
869        if state.bound || state.disposed {
870            return Err(to_pyruntime_err("Client output cannot be rebound"));
871        }
872
873        state.sender = Some(get_data_event_sender());
874        if state.event_factory.is_some() {
875            state.exec_sender = Some(get_exec_event_sender());
876        }
877
878        state.bound = true;
879        Ok(())
880    }
881
882    fn send(&self, event: DataEvent) -> PyResult<()> {
883        self.sender()?.send(event).map_err(to_pyruntime_err)
884    }
885
886    fn sender(&self) -> PyResult<EventSender<DataEvent>> {
887        if thread::current().id() != self.owner {
888            return Err(to_pyruntime_err("Client output requires its owner thread"));
889        }
890
891        self.state
892            .lock()
893            .sender
894            .clone()
895            .ok_or_else(|| to_pyruntime_err("Client output is not bound to an active node"))
896    }
897
898    fn event_factory(&self) -> PyResult<OrderEventFactory> {
899        self.exec_sender()?;
900        self.state
901            .lock()
902            .event_factory
903            .clone()
904            .ok_or_else(|| to_pyruntime_err("Client has no execution identity"))
905    }
906
907    fn exec_sender(&self) -> PyResult<EventSender<ExecutionEvent>> {
908        if thread::current().id() != self.owner {
909            return Err(to_pyruntime_err("Client output requires its owner thread"));
910        }
911
912        self.state
913            .lock()
914            .exec_sender
915            .clone()
916            .ok_or_else(|| to_pyruntime_err("Execution output is not bound to an active node"))
917    }
918
919    fn send_exec(&self, event: ExecutionEvent) -> PyResult<()> {
920        let factory = self.event_factory()?;
921        let account_id = factory.account_id();
922        let trader_id = factory.trader_id();
923
924        let valid = match &event {
925            ExecutionEvent::Account(event) => event.account_id == account_id,
926            ExecutionEvent::Order(event) => {
927                event.trader_id() == trader_id
928                    && event.account_id().is_none_or(|id| id == account_id)
929            }
930            ExecutionEvent::OrderSubmittedBatch(batch) => batch
931                .events
932                .iter()
933                .all(|event| event.trader_id == trader_id && event.account_id == account_id),
934            ExecutionEvent::OrderAcceptedBatch(batch) => batch
935                .events
936                .iter()
937                .all(|event| event.trader_id == trader_id && event.account_id == account_id),
938            ExecutionEvent::OrderCanceledBatch(batch) => batch.events.iter().all(|event| {
939                event.trader_id == trader_id && event.account_id.is_none_or(|id| id == account_id)
940            }),
941            ExecutionEvent::Report(report) => match report {
942                ExecutionReport::Order(report) => report.account_id == account_id,
943                ExecutionReport::Fill(report) => report.account_id == account_id,
944                ExecutionReport::Position(report) => report.account_id == account_id,
945                ExecutionReport::OrderWithFills(report, fills) => {
946                    report.account_id == account_id
947                        && fills.iter().all(|fill| fill.account_id == account_id)
948                }
949                ExecutionReport::MassStatus(report) => {
950                    let state = self.state.lock();
951                    mass_status_matches(report, state.client_id, account_id, state.venue)
952                }
953            },
954        };
955
956        if !valid {
957            return Err(to_pytype_err(
958                "Execution output identity does not match its owner",
959            ));
960        }
961
962        self.exec_sender()?.send(event).map_err(to_pyruntime_err)
963    }
964
965    fn invalidate(&self) {
966        let mut state = self.state.lock();
967        state.sender = None;
968        state.exec_sender = None;
969        state.disposed = true;
970    }
971}
972
973#[derive(Debug, Default, Clone)]
974pub(crate) struct PythonClients(Rc<RefCell<Vec<ClientOwner>>>);
975
976#[derive(Debug)]
977struct ClientOwner {
978    client: Py<PyAny>,
979    runtime: Py<PyAny>,
980    output: ClientOutput,
981    cache_id: u64,
982}
983
984impl Drop for ClientOwner {
985    fn drop(&mut self) {
986        self.output.invalidate();
987        CLIENT_CACHES.with_borrow_mut(|caches| caches.remove(&self.cache_id));
988        Python::attach(|py| {
989            if let Err(e) = self.runtime.call_method0(py, "dispose") {
990                log::error!("Failed to dispose Python client: {e}");
991            }
992        });
993    }
994}
995
996impl PythonClients {
997    pub(crate) fn take(&self) -> Self {
998        Self(Rc::new(RefCell::new(self.0.take())))
999    }
1000
1001    pub(crate) fn is_empty(&self) -> bool {
1002        self.0.borrow().is_empty()
1003    }
1004
1005    pub(crate) fn bind(&self, py: Python<'_>, event_loop: &Bound<'_, PyAny>) -> PyResult<()> {
1006        let runtimes: Vec<_> = self
1007            .0
1008            .borrow()
1009            .iter()
1010            .map(|owner| owner.runtime.clone_ref(py))
1011            .collect();
1012
1013        for runtime in runtimes {
1014            runtime.call_method1(py, "bind", (event_loop,))?;
1015        }
1016
1017        Ok(())
1018    }
1019
1020    pub(crate) fn finish(&self, py: Python<'_>) -> PyResult<()> {
1021        let owners = self.0.take();
1022
1023        let mut incomplete = Vec::new();
1024        let mut retained = Vec::new();
1025        let mut failure = None;
1026
1027        for owner in owners {
1028            owner.output.invalidate();
1029            CLIENT_CACHES.with_borrow_mut(|caches| caches.remove(&owner.cache_id));
1030
1031            let result = (|| {
1032                owner.runtime.call_method0(py, "dispose")?;
1033                if owner.runtime.getattr(py, "complete")?.extract::<bool>(py)? {
1034                    return Ok(true);
1035                }
1036
1037                incomplete.push(
1038                    owner
1039                        .client
1040                        .getattr(py, "client_id")?
1041                        .bind(py)
1042                        .str()?
1043                        .to_string(),
1044                );
1045                Ok::<bool, PyErr>(false)
1046            })();
1047
1048            match result {
1049                Ok(true) => {}
1050                Ok(false) => retained.push(owner),
1051                Err(e) => {
1052                    failure.get_or_insert(e);
1053                    retained.push(owner);
1054                }
1055            }
1056        }
1057
1058        self.0.borrow_mut().extend(retained);
1059
1060        if let Some(e) = failure {
1061            return Err(e);
1062        }
1063
1064        if incomplete.is_empty() {
1065            Ok(())
1066        } else {
1067            Err(to_pyruntime_err(format!(
1068                "Python client cleanup is incomplete: {}",
1069                incomplete.join(", ")
1070            )))
1071        }
1072    }
1073}
1074
1075#[derive(Debug)]
1076pub(crate) struct PythonClientConfig(pub(crate) Py<PyAny>);
1077
1078impl ClientConfig for PythonClientConfig {
1079    fn as_any(&self) -> &dyn Any {
1080        self
1081    }
1082}
1083
1084#[derive(Debug)]
1085pub(crate) struct PythonDataFactory {
1086    pub(crate) factory: Py<PyAny>,
1087    pub(crate) clients: PythonClients,
1088}
1089
1090impl DataClientFactory for PythonDataFactory {
1091    fn name(&self) -> &'static str {
1092        "Python"
1093    }
1094    fn config_type(&self) -> &'static str {
1095        "DataClientConfig"
1096    }
1097
1098    fn create(
1099        &self,
1100        name: &str,
1101        config: &dyn ClientConfig,
1102        cache: CacheView,
1103        clock: Rc<RefCell<dyn Clock>>,
1104    ) -> anyhow::Result<Box<dyn DataClient>> {
1105        Ok(Box::new(self.clients.create(
1106            &self.factory,
1107            name,
1108            config,
1109            cache,
1110            clock,
1111            None,
1112        )?))
1113    }
1114}
1115
1116#[derive(Debug)]
1117pub(crate) struct PythonExecutionFactory {
1118    pub(crate) factory: Py<PyAny>,
1119    pub(crate) clients: PythonClients,
1120}
1121
1122impl ExecutionClientFactory for PythonExecutionFactory {
1123    fn name(&self) -> &'static str {
1124        "Python"
1125    }
1126    fn config_type(&self) -> &'static str {
1127        "ExecutionClientConfig"
1128    }
1129    fn create(
1130        &self,
1131        trader_id: TraderId,
1132        name: &str,
1133        config: &dyn ClientConfig,
1134        cache: CacheView,
1135        clock: Rc<RefCell<dyn Clock>>,
1136    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
1137        let mut client = self.clients.create(
1138            &self.factory,
1139            name,
1140            config,
1141            cache.clone(),
1142            clock,
1143            Some(trader_id),
1144        )?;
1145        let identity = client
1146            .execution
1147            .take()
1148            .ok_or_else(|| anyhow::anyhow!("Missing execution client identity"))?;
1149        Ok(Box::new(PythonExecutionClient {
1150            client,
1151            identity,
1152            cache,
1153        }))
1154    }
1155}
1156
1157impl PythonClients {
1158    fn create(
1159        &self,
1160        factory: &Py<PyAny>,
1161        name: &str,
1162        config: &dyn ClientConfig,
1163        cache: CacheView,
1164        clock: Rc<RefCell<dyn Clock>>,
1165        trader_id: Option<TraderId>,
1166    ) -> anyhow::Result<PythonClient> {
1167        let config = config
1168            .as_any()
1169            .downcast_ref::<PythonClientConfig>()
1170            .ok_or_else(|| anyhow::anyhow!("Expected Python client configuration"))?;
1171        Python::attach(|py| {
1172            let cache_id = CACHE_ID.fetch_add(1, Ordering::Relaxed);
1173            CLIENT_CACHES.with_borrow_mut(|caches| caches.insert(cache_id, cache));
1174
1175            let result = (|| -> PyResult<PythonClient> {
1176                let kwargs = PyDict::new(py);
1177                kwargs.set_item("name", name)?;
1178                kwargs.set_item("config", &config.0)?;
1179                kwargs.set_item("cache", PyClientCache { id: cache_id })?;
1180                kwargs.set_item("clock", PyClock::from_rc(clock.clone()))?;
1181
1182                if let Some(trader_id) = trader_id {
1183                    kwargs.set_item("trader_id", trader_id)?;
1184                }
1185
1186                let client = py
1187                    .import("nautilus_trader.live.clients")?
1188                    .getattr("_create_client")?
1189                    .call1((factory, kwargs))?
1190                    .unbind();
1191
1192                let base_name = if trader_id.is_some() {
1193                    "ExecutionClient"
1194                } else {
1195                    "DataClient"
1196                };
1197
1198                let base = py
1199                    .import("nautilus_trader.live.clients")?
1200                    .getattr(base_name)?;
1201
1202                if !client.bind(py).is_instance(&base)? {
1203                    return Err(to_pytype_err(format!("Factory must return a {base_name}")));
1204                }
1205
1206                let client_id = client.getattr(py, "client_id")?.extract::<ClientId>(py)?;
1207                if client_id.as_str() != name {
1208                    return Err(to_pytype_err(
1209                        "Factory client identity does not match registration name",
1210                    ));
1211                }
1212
1213                if !client.getattr(py, "config")?.is(&config.0) {
1214                    return Err(to_pytype_err(
1215                        "Client must retain the original factory config",
1216                    ));
1217                }
1218
1219                let venue = client.getattr(py, "venue")?.extract::<Option<Venue>>(py)?;
1220                let output = client.getattr(py, "_output")?.extract::<ClientOutput>(py)?;
1221                if client
1222                    .getattr(py, "cache")?
1223                    .extract::<PyRef<'_, PyClientCache>>(py)?
1224                    .id
1225                    != cache_id
1226                {
1227                    return Err(to_pytype_err(
1228                        "Client must retain the owning factory cache view",
1229                    ));
1230                }
1231
1232                let runtime = client.getattr(py, "_runtime")?;
1233
1234                let execution = trader_id
1235                    .map(|trader_id| -> PyResult<ExecutionIdentity> {
1236                        if client.getattr(py, "trader_id")?.extract::<TraderId>(py)? != trader_id {
1237                            return Err(to_pytype_err(
1238                                "Execution client trader identity does not match its node",
1239                            ));
1240                        }
1241
1242                        let tolerance = client
1243                            .getattr(py, "position_reconciliation_tolerance")?
1244                            .extract::<Option<Decimal>>(py)?
1245                            .unwrap_or(DEFAULT_POSITION_RECONCILIATION_TOLERANCE);
1246
1247                        if tolerance.is_sign_negative() {
1248                            return Err(to_pytype_err(
1249                                "Position reconciliation tolerance must be nonnegative",
1250                            ));
1251                        }
1252
1253                        Ok(ExecutionIdentity {
1254                            trader_id,
1255                            tolerance,
1256                            account_id: client.getattr(py, "account_id")?.extract(py)?,
1257                            account_type: client.getattr(py, "account_type")?.extract(py)?,
1258                            base_currency: client.getattr(py, "base_currency")?.extract(py)?,
1259                            oms_type: client.getattr(py, "oms_type")?.extract(py)?,
1260                            venue: venue.ok_or_else(|| {
1261                                to_pytype_err("Execution clients require a venue")
1262                            })?,
1263                        })
1264                    })
1265                    .transpose()?;
1266
1267                {
1268                    let mut state = output.state.lock();
1269                    if state.claimed || state.disposed {
1270                        return Err(to_pytype_err(
1271                            "A client instance cannot be registered twice",
1272                        ));
1273                    }
1274
1275                    state.claimed = true;
1276                    state.client_id = Some(client_id);
1277                    state.venue = venue;
1278                    state.event_factory = execution.as_ref().map(|identity| {
1279                        OrderEventFactory::new(
1280                            identity.trader_id,
1281                            identity.account_id,
1282                            identity.account_type,
1283                            identity.base_currency,
1284                        )
1285                    });
1286                }
1287
1288                self.0.borrow_mut().push(ClientOwner {
1289                    client: client.clone_ref(py),
1290                    runtime: runtime.clone_ref(py),
1291                    output: output.clone(),
1292                    cache_id,
1293                });
1294
1295                Ok(PythonClient {
1296                    client_id,
1297                    venue,
1298                    runtime,
1299                    output,
1300                    instance: client,
1301                    execution,
1302                    clock,
1303                })
1304            })();
1305
1306            if result.is_err() {
1307                CLIENT_CACHES.with_borrow_mut(|caches| caches.remove(&cache_id));
1308            }
1309
1310            result.map_err(Into::into)
1311        })
1312    }
1313}
1314
1315struct ExecutionIdentity {
1316    tolerance: Decimal,
1317    trader_id: TraderId,
1318    account_id: AccountId,
1319    account_type: AccountType,
1320    base_currency: Option<Currency>,
1321    oms_type: OmsType,
1322    venue: Venue,
1323}
1324
1325struct PythonClient {
1326    client_id: ClientId,
1327    venue: Option<Venue>,
1328    runtime: Py<PyAny>,
1329    output: ClientOutput,
1330    instance: Py<PyAny>,
1331    execution: Option<ExecutionIdentity>,
1332    clock: Rc<RefCell<dyn Clock>>,
1333}
1334
1335#[async_trait(?Send)]
1336impl DataClient for PythonClient {
1337    fn client_id(&self) -> ClientId {
1338        self.client_id
1339    }
1340    fn venue(&self) -> Option<Venue> {
1341        self.venue
1342    }
1343    fn start(&mut self) -> anyhow::Result<()> {
1344        self.output.bind().map_err(Into::into)
1345    }
1346    fn stop(&mut self) -> anyhow::Result<()> {
1347        Ok(())
1348    }
1349    fn reset(&mut self) -> anyhow::Result<()> {
1350        anyhow::bail!("Python clients are single-use")
1351    }
1352    fn dispose(&mut self) -> anyhow::Result<()> {
1353        Ok(())
1354    }
1355    fn is_connected(&self) -> bool {
1356        Python::attach(|py| {
1357            self.runtime
1358                .getattr(py, "connected")
1359                .and_then(|v| v.extract(py))
1360                .unwrap_or(false)
1361        })
1362    }
1363    fn is_disconnected(&self) -> bool {
1364        !self.is_connected()
1365    }
1366    async fn connect(&mut self) -> anyhow::Result<()> {
1367        self.lifecycle("connect").await
1368    }
1369    async fn disconnect(&mut self) -> anyhow::Result<()> {
1370        self.lifecycle("disconnect").await
1371    }
1372    fn subscribe(&mut self, command: SubscribeCustomData) -> anyhow::Result<()> {
1373        Python::attach(|py| {
1374            self.runtime
1375                .call_method1(py, "admit", ("_subscribe", (command,)))
1376        })?;
1377
1378        Ok(())
1379    }
1380    fn subscribe_instruments(&mut self, command: SubscribeInstruments) -> anyhow::Result<()> {
1381        Python::attach(|py| {
1382            self.runtime
1383                .call_method1(py, "admit", ("_subscribe_instruments", (command,)))
1384        })?;
1385
1386        Ok(())
1387    }
1388    fn subscribe_instrument(&mut self, command: SubscribeInstrument) -> anyhow::Result<()> {
1389        Python::attach(|py| {
1390            self.runtime
1391                .call_method1(py, "admit", ("_subscribe_instrument", (command,)))
1392        })?;
1393
1394        Ok(())
1395    }
1396    fn subscribe_book_deltas(&mut self, command: SubscribeBookDeltas) -> anyhow::Result<()> {
1397        Python::attach(|py| {
1398            self.runtime
1399                .call_method1(py, "admit", ("_subscribe_book_deltas", (command,)))
1400        })?;
1401
1402        Ok(())
1403    }
1404    fn subscribe_book_depth(&mut self, command: SubscribeBookDepth) -> anyhow::Result<()> {
1405        Python::attach(|py| {
1406            self.runtime
1407                .call_method1(py, "admit", ("_subscribe_book_depth", (command,)))
1408        })?;
1409
1410        Ok(())
1411    }
1412    fn subscribe_quotes(&mut self, command: SubscribeQuotes) -> anyhow::Result<()> {
1413        Python::attach(|py| {
1414            self.runtime
1415                .call_method1(py, "admit", ("_subscribe_quotes", (command,)))
1416        })?;
1417
1418        Ok(())
1419    }
1420    fn subscribe_trades(&mut self, command: SubscribeTrades) -> anyhow::Result<()> {
1421        Python::attach(|py| {
1422            self.runtime
1423                .call_method1(py, "admit", ("_subscribe_trades", (command,)))
1424        })?;
1425
1426        Ok(())
1427    }
1428    fn subscribe_mark_prices(&mut self, command: SubscribeMarkPrices) -> anyhow::Result<()> {
1429        Python::attach(|py| {
1430            self.runtime
1431                .call_method1(py, "admit", ("_subscribe_mark_prices", (command,)))
1432        })?;
1433
1434        Ok(())
1435    }
1436    fn subscribe_index_prices(&mut self, command: SubscribeIndexPrices) -> anyhow::Result<()> {
1437        Python::attach(|py| {
1438            self.runtime
1439                .call_method1(py, "admit", ("_subscribe_index_prices", (command,)))
1440        })?;
1441
1442        Ok(())
1443    }
1444    fn subscribe_funding_rates(&mut self, command: SubscribeFundingRates) -> anyhow::Result<()> {
1445        Python::attach(|py| {
1446            self.runtime
1447                .call_method1(py, "admit", ("_subscribe_funding_rates", (command,)))
1448        })?;
1449
1450        Ok(())
1451    }
1452    fn subscribe_bars(&mut self, command: SubscribeBars) -> anyhow::Result<()> {
1453        Python::attach(|py| {
1454            self.runtime
1455                .call_method1(py, "admit", ("_subscribe_bars", (command,)))
1456        })?;
1457
1458        Ok(())
1459    }
1460    fn subscribe_instrument_status(
1461        &mut self,
1462        command: SubscribeInstrumentStatus,
1463    ) -> anyhow::Result<()> {
1464        Python::attach(|py| {
1465            self.runtime
1466                .call_method1(py, "admit", ("_subscribe_instrument_status", (command,)))
1467        })?;
1468
1469        Ok(())
1470    }
1471    fn subscribe_instrument_close(
1472        &mut self,
1473        command: SubscribeInstrumentClose,
1474    ) -> anyhow::Result<()> {
1475        Python::attach(|py| {
1476            self.runtime
1477                .call_method1(py, "admit", ("_subscribe_instrument_close", (command,)))
1478        })?;
1479
1480        Ok(())
1481    }
1482    fn subscribe_option_greeks(&mut self, command: SubscribeOptionGreeks) -> anyhow::Result<()> {
1483        Python::attach(|py| {
1484            self.runtime
1485                .call_method1(py, "admit", ("_subscribe_option_greeks", (command,)))
1486        })?;
1487
1488        Ok(())
1489    }
1490    fn unsubscribe(&mut self, command: &UnsubscribeCustomData) -> anyhow::Result<()> {
1491        Python::attach(|py| {
1492            self.runtime
1493                .call_method1(py, "admit", ("_unsubscribe", (command.clone(),)))
1494        })?;
1495
1496        Ok(())
1497    }
1498    fn unsubscribe_instruments(&mut self, command: &UnsubscribeInstruments) -> anyhow::Result<()> {
1499        Python::attach(|py| {
1500            self.runtime.call_method1(
1501                py,
1502                "admit",
1503                ("_unsubscribe_instruments", (command.clone(),)),
1504            )
1505        })?;
1506
1507        Ok(())
1508    }
1509    fn unsubscribe_instrument(&mut self, command: &UnsubscribeInstrument) -> anyhow::Result<()> {
1510        Python::attach(|py| {
1511            self.runtime
1512                .call_method1(py, "admit", ("_unsubscribe_instrument", (command.clone(),)))
1513        })?;
1514
1515        Ok(())
1516    }
1517    fn unsubscribe_book_deltas(&mut self, command: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
1518        Python::attach(|py| {
1519            self.runtime.call_method1(
1520                py,
1521                "admit",
1522                ("_unsubscribe_book_deltas", (command.clone(),)),
1523            )
1524        })?;
1525
1526        Ok(())
1527    }
1528    fn unsubscribe_book_depth(&mut self, command: &UnsubscribeBookDepth) -> anyhow::Result<()> {
1529        Python::attach(|py| {
1530            self.runtime
1531                .call_method1(py, "admit", ("_unsubscribe_book_depth", (command.clone(),)))
1532        })?;
1533
1534        Ok(())
1535    }
1536    fn unsubscribe_quotes(&mut self, command: &UnsubscribeQuotes) -> anyhow::Result<()> {
1537        Python::attach(|py| {
1538            self.runtime
1539                .call_method1(py, "admit", ("_unsubscribe_quotes", (command.clone(),)))
1540        })?;
1541
1542        Ok(())
1543    }
1544    fn unsubscribe_trades(&mut self, command: &UnsubscribeTrades) -> anyhow::Result<()> {
1545        Python::attach(|py| {
1546            self.runtime
1547                .call_method1(py, "admit", ("_unsubscribe_trades", (command.clone(),)))
1548        })?;
1549
1550        Ok(())
1551    }
1552    fn unsubscribe_mark_prices(&mut self, command: &UnsubscribeMarkPrices) -> anyhow::Result<()> {
1553        Python::attach(|py| {
1554            self.runtime.call_method1(
1555                py,
1556                "admit",
1557                ("_unsubscribe_mark_prices", (command.clone(),)),
1558            )
1559        })?;
1560
1561        Ok(())
1562    }
1563    fn unsubscribe_index_prices(&mut self, command: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
1564        Python::attach(|py| {
1565            self.runtime.call_method1(
1566                py,
1567                "admit",
1568                ("_unsubscribe_index_prices", (command.clone(),)),
1569            )
1570        })?;
1571
1572        Ok(())
1573    }
1574    fn unsubscribe_funding_rates(
1575        &mut self,
1576        command: &UnsubscribeFundingRates,
1577    ) -> anyhow::Result<()> {
1578        Python::attach(|py| {
1579            self.runtime.call_method1(
1580                py,
1581                "admit",
1582                ("_unsubscribe_funding_rates", (command.clone(),)),
1583            )
1584        })?;
1585
1586        Ok(())
1587    }
1588    fn unsubscribe_bars(&mut self, command: &UnsubscribeBars) -> anyhow::Result<()> {
1589        Python::attach(|py| {
1590            self.runtime
1591                .call_method1(py, "admit", ("_unsubscribe_bars", (command.clone(),)))
1592        })?;
1593
1594        Ok(())
1595    }
1596    fn unsubscribe_instrument_status(
1597        &mut self,
1598        command: &UnsubscribeInstrumentStatus,
1599    ) -> anyhow::Result<()> {
1600        Python::attach(|py| {
1601            self.runtime.call_method1(
1602                py,
1603                "admit",
1604                ("_unsubscribe_instrument_status", (command.clone(),)),
1605            )
1606        })?;
1607
1608        Ok(())
1609    }
1610    fn unsubscribe_instrument_close(
1611        &mut self,
1612        command: &UnsubscribeInstrumentClose,
1613    ) -> anyhow::Result<()> {
1614        Python::attach(|py| {
1615            self.runtime.call_method1(
1616                py,
1617                "admit",
1618                ("_unsubscribe_instrument_close", (command.clone(),)),
1619            )
1620        })?;
1621
1622        Ok(())
1623    }
1624    fn unsubscribe_option_greeks(
1625        &mut self,
1626        command: &UnsubscribeOptionGreeks,
1627    ) -> anyhow::Result<()> {
1628        Python::attach(|py| {
1629            self.runtime.call_method1(
1630                py,
1631                "admit",
1632                ("_unsubscribe_option_greeks", (command.clone(),)),
1633            )
1634        })?;
1635
1636        Ok(())
1637    }
1638
1639    fn request_data(&self, request: RequestCustomData) -> anyhow::Result<()> {
1640        let request_id = request.request_id;
1641        Python::attach(|py| {
1642            let argument = Py::new(py, PyRequestCustomData { request })?.into_any();
1643            self.call_request(py, "_request_data", request_id, argument)
1644        })?;
1645
1646        Ok(())
1647    }
1648
1649    fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
1650        let request_id = request.request_id;
1651        Python::attach(|py| {
1652            let argument = Py::new(py, PyRequestInstruments { request })?.into_any();
1653            self.call_request(py, "_request_instruments", request_id, argument)
1654        })?;
1655
1656        Ok(())
1657    }
1658
1659    fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
1660        let request_id = request.request_id;
1661        Python::attach(|py| {
1662            let argument = Py::new(py, PyRequestInstrument { request })?.into_any();
1663            self.call_request(py, "_request_instrument", request_id, argument)
1664        })?;
1665
1666        Ok(())
1667    }
1668
1669    fn request_book_snapshot(&self, request: RequestBookSnapshot) -> anyhow::Result<()> {
1670        let request_id = request.request_id;
1671        Python::attach(|py| {
1672            let argument = Py::new(py, PyRequestBookSnapshot { request })?.into_any();
1673            self.call_request(py, "_request_book_snapshot", request_id, argument)
1674        })?;
1675
1676        Ok(())
1677    }
1678
1679    fn request_quotes(&self, request: RequestQuotes) -> anyhow::Result<()> {
1680        let request_id = request.request_id;
1681        Python::attach(|py| {
1682            let argument = Py::new(py, PyRequestQuotes { request })?.into_any();
1683            self.call_request(py, "_request_quotes", request_id, argument)
1684        })?;
1685
1686        Ok(())
1687    }
1688
1689    fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
1690        let request_id = request.request_id;
1691        Python::attach(|py| {
1692            let argument = Py::new(py, PyRequestTrades { request })?.into_any();
1693            self.call_request(py, "_request_trades", request_id, argument)
1694        })?;
1695
1696        Ok(())
1697    }
1698
1699    fn request_funding_rates(&self, request: RequestFundingRates) -> anyhow::Result<()> {
1700        let request_id = request.request_id;
1701        Python::attach(|py| {
1702            let argument = Py::new(py, PyRequestFundingRates { request })?.into_any();
1703            self.call_request(py, "_request_funding_rates", request_id, argument)
1704        })?;
1705
1706        Ok(())
1707    }
1708
1709    fn request_option_chain_reference_price(
1710        &self,
1711        request: RequestOptionChainReferencePrice,
1712    ) -> anyhow::Result<()> {
1713        let request_id = request.request_id;
1714        Python::attach(|py| {
1715            let argument = Py::new(py, PyRequestOptionChainReferencePrice { request })?.into_any();
1716            self.call_request(
1717                py,
1718                "_request_option_chain_reference_price",
1719                request_id,
1720                argument,
1721            )
1722        })?;
1723
1724        Ok(())
1725    }
1726
1727    fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
1728        let request_id = request.request_id;
1729        Python::attach(|py| {
1730            let argument = Py::new(py, PyRequestBars { request })?.into_any();
1731            self.call_request(py, "_request_bars", request_id, argument)
1732        })?;
1733
1734        Ok(())
1735    }
1736
1737    fn request_book_depth(&self, request: RequestBookDepth) -> anyhow::Result<()> {
1738        let request_id = request.request_id;
1739        Python::attach(|py| {
1740            let argument = Py::new(py, PyRequestBookDepth { request })?.into_any();
1741            self.call_request(py, "_request_book_depth", request_id, argument)
1742        })?;
1743
1744        Ok(())
1745    }
1746
1747    fn request_book_deltas(&self, request: RequestBookDeltas) -> anyhow::Result<()> {
1748        let request_id = request.request_id;
1749        Python::attach(|py| {
1750            let argument = Py::new(py, PyRequestBookDeltas { request })?.into_any();
1751            self.call_request(py, "_request_book_deltas", request_id, argument)
1752        })?;
1753
1754        Ok(())
1755    }
1756}
1757
1758impl PythonClient {
1759    async fn lifecycle(&self, operation: &str) -> anyhow::Result<()> {
1760        let future = Python::attach(|py| {
1761            let task = self.runtime.call_method1(py, "lifecycle", (operation,))?;
1762
1763            PythonOperation::new(py, task, self.runtime.clone_ref(py))
1764        })?;
1765
1766        future.await.map(|_| ()).map_err(Into::into)
1767    }
1768}
1769
1770fn extract_data(data: &Bound<'_, PyAny>) -> PyResult<Data> {
1771    macro_rules! extract {
1772        ($($ty:ty),+ $(,)?) => {
1773            $(if data.is_instance_of::<$ty>() {
1774                return Ok(data.extract::<$ty>()?.into());
1775            })+
1776        };
1777    }
1778    extract!(
1779        QuoteTick,
1780        TradeTick,
1781        Bar,
1782        OrderBookDelta,
1783        OrderBookDeltas,
1784        OrderBookDepth,
1785        MarkPriceUpdate,
1786        IndexPriceUpdate,
1787        FundingRateUpdate,
1788        OptionGreeks,
1789        InstrumentStatus,
1790        InstrumentClose
1791    );
1792
1793    if data.is_instance_of::<CustomData>() {
1794        return Ok(Data::Custom(data.extract()?));
1795    }
1796
1797    Err(to_pytype_err(
1798        "Expected a Nautilus data object from the installed wheel",
1799    ))
1800}
1801
1802struct PythonExecutionClient {
1803    client: PythonClient,
1804    identity: ExecutionIdentity,
1805    cache: CacheView,
1806}
1807
1808impl PythonClient {
1809    fn call_request(
1810        &self,
1811        py: Python<'_>,
1812        operation: &str,
1813        request_id: nautilus_core::UUID4,
1814        argument: Py<PyAny>,
1815    ) -> PyResult<()> {
1816        let task = self
1817            .runtime
1818            .call_method1(py, "call", (operation, (argument,)))?;
1819        let output = self.output.clone();
1820
1821        let callback = PyCFunction::new_closure(
1822            py,
1823            None,
1824            None,
1825            move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
1826                let task = args.get_item(0)?;
1827
1828                let reason = if task.call_method0("cancelled")?.extract::<bool>()? {
1829                    Some("Python data request canceled".to_owned())
1830                } else {
1831                    let exception = task.call_method0("exception")?;
1832                    if exception.is_none() {
1833                        None
1834                    } else {
1835                        Some(exception.str()?.to_str()?.to_owned())
1836                    }
1837                };
1838
1839                if let Some(reason) = reason {
1840                    if output.state.lock().disposed {
1841                        return Ok(());
1842                    }
1843
1844                    if let Err(e) = output.send(DataEvent::RequestFailed(DataRequestFailed {
1845                        request_id,
1846                        reason,
1847                        response: None,
1848                    })) {
1849                        log::error!("Failed to deliver Python data request failure: {e}");
1850                    }
1851                }
1852
1853                Ok::<(), PyErr>(())
1854            },
1855        )?;
1856
1857        task.call_method1(py, "add_done_callback", (callback,))?;
1858        Ok(())
1859    }
1860}
1861
1862#[async_trait(?Send)]
1863impl ExecutionClient for PythonExecutionClient {
1864    fn client_id(&self) -> ClientId {
1865        self.client.client_id
1866    }
1867    fn account_id(&self) -> AccountId {
1868        self.identity.account_id
1869    }
1870    fn venue(&self) -> Venue {
1871        self.identity.venue
1872    }
1873    fn oms_type(&self) -> OmsType {
1874        self.identity.oms_type
1875    }
1876    fn position_reconciliation_tolerance(&self) -> Decimal {
1877        self.identity.tolerance
1878    }
1879    fn handles_order_venue(&self, venue: Venue) -> bool {
1880        Python::attach(|py| {
1881            self.client
1882                .instance
1883                .call_method1(py, "_handles_order_venue", (venue,))?
1884                .extract::<bool>(py)
1885        })
1886        .unwrap_or_else(|e| {
1887            log::error!(
1888                "Client {} venue coverage failed: {e}",
1889                self.client.client_id
1890            );
1891            false
1892        })
1893    }
1894    fn provides_bulk_position_coverage(&self, instrument_id: InstrumentId) -> bool {
1895        Python::attach(|py| {
1896            self.client
1897                .instance
1898                .call_method1(py, "_provides_bulk_position_coverage", (instrument_id,))?
1899                .extract::<bool>(py)
1900        })
1901        .unwrap_or_else(|e| {
1902            log::error!(
1903                "Client {} bulk position coverage failed: {e}",
1904                self.client.client_id
1905            );
1906            false
1907        })
1908    }
1909    fn calculate_commission(
1910        &self,
1911        instrument: &InstrumentAny,
1912        last_qty: Quantity,
1913        last_px: Price,
1914        liquidity_side: LiquiditySide,
1915    ) -> anyhow::Result<Option<Money>> {
1916        Python::attach(|py| -> PyResult<Option<Money>> {
1917            let instrument = instrument_any_to_pyobject(py, instrument.clone())?;
1918            Ok(self
1919                .client
1920                .instance
1921                .call_method1(
1922                    py,
1923                    "_calculate_commission",
1924                    (instrument, last_qty, last_px, liquidity_side),
1925                )?
1926                .extract(py)?)
1927        })
1928        .map_err(Into::into)
1929    }
1930    fn register_external_order(
1931        &self,
1932        client_order_id: ClientOrderId,
1933        venue_order_id: VenueOrderId,
1934        instrument_id: InstrumentId,
1935        strategy_id: StrategyId,
1936        ts_init: UnixNanos,
1937    ) {
1938        if let Err(e) = Python::attach(|py| {
1939            self.client.runtime.call_method1(
1940                py,
1941                "admit",
1942                (
1943                    "_register_external_order",
1944                    (
1945                        client_order_id,
1946                        venue_order_id,
1947                        instrument_id,
1948                        strategy_id,
1949                        ts_init.as_u64(),
1950                    ),
1951                ),
1952            )
1953        }) {
1954            log::error!(
1955                "Client {} external order registration failed: {e}",
1956                self.client.client_id
1957            );
1958        }
1959    }
1960    fn on_instrument(&mut self, instrument: InstrumentAny) {
1961        if let Err(e) = Python::attach(|py| -> PyResult<()> {
1962            let instrument = instrument_any_to_pyobject(py, instrument)?;
1963            self.client
1964                .runtime
1965                .call_method1(py, "admit", ("_on_instrument", (instrument,)))?;
1966            Ok(())
1967        }) {
1968            log::error!(
1969                "Client {} instrument update failed: {e}",
1970                self.client.client_id
1971            );
1972        }
1973    }
1974    fn get_account(&self) -> Option<AccountAny> {
1975        match self.cache.try_borrow() {
1976            Ok(cache) => cache.account_owned(&self.identity.account_id),
1977            Err(e) => {
1978                log::error!("Cannot read Python execution client account: {e}");
1979                None
1980            }
1981        }
1982    }
1983    fn generate_account_state(
1984        &self,
1985        balances: Vec<AccountBalance>,
1986        margins: Vec<MarginBalance>,
1987        reported: bool,
1988        ts_event: UnixNanos,
1989        info: Option<Params>,
1990    ) -> anyhow::Result<()> {
1991        let ts_init = self.client.clock.borrow().timestamp_ns();
1992        let factory = self.client.output.event_factory()?;
1993        let event =
1994            factory.generate_account_state(balances, margins, reported, ts_event, ts_init, info);
1995        self.client
1996            .output
1997            .send_exec(ExecutionEvent::Account(event))
1998            .map_err(Into::into)
1999    }
2000    fn start(&mut self) -> anyhow::Result<()> {
2001        self.client.output.bind().map_err(Into::into)
2002    }
2003    fn stop(&mut self) -> anyhow::Result<()> {
2004        Ok(())
2005    }
2006    fn reset(&mut self) -> anyhow::Result<()> {
2007        anyhow::bail!("Python clients are single-use")
2008    }
2009    fn dispose(&mut self) -> anyhow::Result<()> {
2010        Ok(())
2011    }
2012    fn is_connected(&self) -> bool {
2013        DataClient::is_connected(&self.client)
2014    }
2015    async fn connect(&mut self) -> anyhow::Result<()> {
2016        self.client.lifecycle("connect").await
2017    }
2018    async fn disconnect(&mut self) -> anyhow::Result<()> {
2019        self.client.lifecycle("disconnect").await
2020    }
2021    fn submit_order(&self, command: SubmitOrder) -> anyhow::Result<()> {
2022        Python::attach(|py| {
2023            self.client
2024                .runtime
2025                .call_method1(py, "admit", ("_submit_order", (command,)))
2026        })?;
2027
2028        Ok(())
2029    }
2030    fn submit_order_list(&self, command: SubmitOrderList) -> anyhow::Result<()> {
2031        Python::attach(|py| {
2032            self.client
2033                .runtime
2034                .call_method1(py, "admit", ("_submit_order_list", (command,)))
2035        })?;
2036
2037        Ok(())
2038    }
2039    fn modify_order(&self, command: ModifyOrder) -> anyhow::Result<()> {
2040        Python::attach(|py| {
2041            self.client
2042                .runtime
2043                .call_method1(py, "admit", ("_modify_order", (command,)))
2044        })?;
2045
2046        Ok(())
2047    }
2048    fn batch_modify_orders(&self, command: BatchModifyOrders) -> anyhow::Result<()> {
2049        Python::attach(|py| {
2050            self.client
2051                .runtime
2052                .call_method1(py, "admit", ("_batch_modify_orders", (command,)))
2053        })?;
2054
2055        Ok(())
2056    }
2057    fn cancel_order(&self, command: CancelOrder) -> anyhow::Result<()> {
2058        Python::attach(|py| {
2059            self.client
2060                .runtime
2061                .call_method1(py, "admit", ("_cancel_order", (command,)))
2062        })?;
2063
2064        Ok(())
2065    }
2066    fn cancel_all_orders(&self, command: CancelAllOrders) -> anyhow::Result<()> {
2067        Python::attach(|py| {
2068            self.client
2069                .runtime
2070                .call_method1(py, "admit", ("_cancel_all_orders", (command,)))
2071        })?;
2072
2073        Ok(())
2074    }
2075    fn batch_cancel_orders(&self, command: BatchCancelOrders) -> anyhow::Result<()> {
2076        Python::attach(|py| {
2077            self.client
2078                .runtime
2079                .call_method1(py, "admit", ("_batch_cancel_orders", (command,)))
2080        })?;
2081
2082        Ok(())
2083    }
2084    fn query_account(&self, command: QueryAccount) -> anyhow::Result<()> {
2085        Python::attach(|py| {
2086            self.client
2087                .runtime
2088                .call_method1(py, "admit", ("_query_account", (command,)))
2089        })?;
2090
2091        Ok(())
2092    }
2093    fn query_order(&self, command: QueryOrder) -> anyhow::Result<()> {
2094        Python::attach(|py| {
2095            self.client
2096                .runtime
2097                .call_method1(py, "admit", ("_query_order", (command,)))
2098        })?;
2099
2100        Ok(())
2101    }
2102    async fn generate_mass_status(
2103        &self,
2104        lookback_mins: Option<u64>,
2105    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
2106        let operation = Python::attach(|py| {
2107            let task = self.client.runtime.call_method1(
2108                py,
2109                "call_awaited",
2110                ("_generate_mass_status", (lookback_mins,)),
2111            )?;
2112
2113            PythonOperation::new(py, task, self.client.runtime.clone_ref(py))
2114        })?;
2115
2116        let result = operation.await?;
2117        let default = Python::attach(|py| result.is(py.NotImplemented()));
2118        if default {
2119            let ts_init = self.client.clock.borrow().timestamp_ns();
2120            generate_mass_status(self, lookback_mins, ts_init).await
2121        } else {
2122            let report = Python::attach(|py| -> PyResult<Option<ExecutionMassStatus>> {
2123                Ok(result.extract(py)?)
2124            })?;
2125
2126            if report.as_ref().is_some_and(|report| {
2127                !mass_status_matches(
2128                    report,
2129                    Some(self.client.client_id),
2130                    self.identity.account_id,
2131                    Some(self.identity.venue),
2132                )
2133            }) {
2134                anyhow::bail!("Mass status identity does not match its client");
2135            }
2136
2137            Ok(report)
2138        }
2139    }
2140    async fn generate_order_status_report(
2141        &self,
2142        command: &GenerateOrderStatusReport,
2143    ) -> anyhow::Result<Option<OrderStatusReport>> {
2144        let operation = Python::attach(|py| {
2145            let task = self.client.runtime.call_method1(
2146                py,
2147                "call_awaited",
2148                ("_generate_order_status_report", (command.clone(),)),
2149            )?;
2150
2151            PythonOperation::new(py, task, self.client.runtime.clone_ref(py))
2152        })?;
2153
2154        let result = operation.await?;
2155
2156        let report = Python::attach(|py| -> PyResult<Option<OrderStatusReport>> {
2157            Ok(result.extract(py)?)
2158        })?;
2159
2160        if report
2161            .as_ref()
2162            .is_some_and(|report| report.account_id != self.identity.account_id)
2163        {
2164            anyhow::bail!("Report account identity does not match its client");
2165        }
2166
2167        Ok(report)
2168    }
2169    async fn generate_order_status_reports(
2170        &self,
2171        command: &GenerateOrderStatusReports,
2172    ) -> anyhow::Result<Vec<OrderStatusReport>> {
2173        let operation = Python::attach(|py| {
2174            let task = self.client.runtime.call_method1(
2175                py,
2176                "call_awaited",
2177                ("_generate_order_status_reports", (command.clone(),)),
2178            )?;
2179
2180            PythonOperation::new(py, task, self.client.runtime.clone_ref(py))
2181        })?;
2182
2183        let result = operation.await?;
2184        let reports = Python::attach(|py| result.extract::<Vec<OrderStatusReport>>(py))?;
2185        if reports
2186            .iter()
2187            .any(|report| report.account_id != self.identity.account_id)
2188        {
2189            anyhow::bail!("Report account identity does not match its client");
2190        }
2191
2192        Ok(reports)
2193    }
2194    async fn generate_fill_reports(
2195        &self,
2196        command: GenerateFillReports,
2197    ) -> anyhow::Result<Vec<FillReport>> {
2198        let operation = Python::attach(|py| {
2199            let task = self.client.runtime.call_method1(
2200                py,
2201                "call_awaited",
2202                ("_generate_fill_reports", (command,)),
2203            )?;
2204
2205            PythonOperation::new(py, task, self.client.runtime.clone_ref(py))
2206        })?;
2207
2208        let result = operation.await?;
2209        let reports = Python::attach(|py| result.extract::<Vec<FillReport>>(py))?;
2210        if reports
2211            .iter()
2212            .any(|report| report.account_id != self.identity.account_id)
2213        {
2214            anyhow::bail!("Report account identity does not match its client");
2215        }
2216
2217        Ok(reports)
2218    }
2219    async fn generate_position_status_reports(
2220        &self,
2221        command: &GeneratePositionStatusReports,
2222    ) -> anyhow::Result<Vec<PositionStatusReport>> {
2223        let operation = Python::attach(|py| {
2224            let task = self.client.runtime.call_method1(
2225                py,
2226                "call_awaited",
2227                ("_generate_position_status_reports", (command.clone(),)),
2228            )?;
2229
2230            PythonOperation::new(py, task, self.client.runtime.clone_ref(py))
2231        })?;
2232
2233        let result = operation.await?;
2234        let reports = Python::attach(|py| result.extract::<Vec<PositionStatusReport>>(py))?;
2235        if reports
2236            .iter()
2237            .any(|report| report.account_id != self.identity.account_id)
2238        {
2239            anyhow::bail!("Report account identity does not match its client");
2240        }
2241
2242        Ok(reports)
2243    }
2244}
2245
2246fn mass_status_matches(
2247    report: &ExecutionMassStatus,
2248    client_id: Option<ClientId>,
2249    account_id: AccountId,
2250    venue: Option<Venue>,
2251) -> bool {
2252    report.account_id == account_id
2253        && Some(report.client_id) == client_id
2254        && Some(report.venue) == venue
2255        && report
2256            .order_reports()
2257            .values()
2258            .all(|report| report.account_id == account_id)
2259        && report
2260            .fill_reports()
2261            .values()
2262            .flatten()
2263            .all(|report| report.account_id == account_id)
2264        && report
2265            .position_reports()
2266            .values()
2267            .flatten()
2268            .all(|report| report.account_id == account_id)
2269}
2270
2271#[cfg(test)]
2272mod tests {
2273    use nautilus_common::{clock::VirtualClock, enums::LogLevel, messages::data::DataResponse};
2274    use nautilus_core::python::to_pyvalue_err;
2275    use nautilus_model::{
2276        enums::{OrderStatus, OrderType, TimeInForce},
2277        events::OrderEventAny,
2278    };
2279    use rstest::{fixture, rstest};
2280
2281    use super::*;
2282
2283    #[fixture]
2284    fn execution_output() -> (
2285        ClientOutput,
2286        tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
2287    ) {
2288        Python::initialize();
2289        let output = ClientOutput::py_new();
2290        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
2291        {
2292            let mut state = output.state.lock();
2293            state.exec_sender = Some(sender.into());
2294            state.bound = true;
2295            state.event_factory = Some(OrderEventFactory::new(
2296                TraderId::from("TESTER-001"),
2297                AccountId::from("SIM-001"),
2298                AccountType::Cash,
2299                None,
2300            ));
2301        }
2302
2303        (output, receiver)
2304    }
2305
2306    #[fixture]
2307    fn native_execution_client() -> PythonExecutionClient {
2308        Python::initialize();
2309        Python::attach(|py| {
2310            let module = PyModule::from_code(
2311                py,
2312                c"import asyncio
2313class Runtime:
2314    def __init__(self):
2315        self.loop = asyncio.new_event_loop()
2316        self.result = None
2317        self.failure = None
2318        self.cancelled = False
2319        self.calls = []
2320    def call_awaited(self, operation, args):
2321        self.calls.append((operation, args))
2322        task = self.loop.create_future()
2323        if self.cancelled:
2324            task.cancel()
2325        elif self.failure is not None:
2326            task.set_exception(self.failure)
2327        else:
2328            task.set_result(self.result)
2329        return task
2330    def call(self, operation, args):
2331        return self.call_awaited(operation, args)
2332    def _handles_order_venue(self, venue):
2333        self.calls.append(('venue', venue))
2334        if self.failure is not None:
2335            raise self.failure
2336        return self.result
2337    def _provides_bulk_position_coverage(self, instrument_id):
2338        self.calls.append(('positions', instrument_id))
2339        if self.failure is not None:
2340            raise self.failure
2341        return self.result
2342    def _calculate_commission(self, *args):
2343        self.calls.append(('commission', args))
2344        if self.failure is not None:
2345            raise self.failure
2346        return self.result
2347    def abandon(self, task):
2348        task.cancel()
2349    def __del__(self):
2350        self.loop.close()
2351",
2352                c"adapter_native_tests.py",
2353                c"adapter_native_tests",
2354            )
2355            .unwrap();
2356            let runtime = module.getattr("Runtime").unwrap().call0().unwrap().unbind();
2357
2358            let cache = CacheView::new(Rc::new(RefCell::new(Cache::default())));
2359
2360            PythonExecutionClient {
2361                client: PythonClient {
2362                    client_id: ClientId::from("SIM"),
2363                    venue: Some(Venue::from("SIM")),
2364                    instance: runtime.clone_ref(py),
2365                    runtime,
2366                    output: ClientOutput::py_new(),
2367                    execution: None,
2368                    clock: Rc::new(RefCell::new(VirtualClock::default())),
2369                },
2370                identity: ExecutionIdentity {
2371                    tolerance: Decimal::ZERO,
2372                    trader_id: TraderId::from("TESTER-001"),
2373                    account_id: AccountId::from("SIM-001"),
2374                    account_type: AccountType::Cash,
2375                    base_currency: None,
2376                    oms_type: OmsType::Netting,
2377                    venue: Venue::from("SIM"),
2378                },
2379                cache,
2380            }
2381        })
2382    }
2383
2384    #[rstest]
2385    #[case("missing")]
2386    #[case("valid")]
2387    #[case("foreign")]
2388    #[case("wrong_type")]
2389    #[case("exception")]
2390    #[case("cancelled")]
2391    #[tokio::test]
2392    async fn test_single_order_report_validates_result_and_preserves_command(
2393        native_execution_client: PythonExecutionClient,
2394        #[case] result_kind: &str,
2395    ) {
2396        let client = native_execution_client;
2397        let mut expected = order_report();
2398
2399        if result_kind == "foreign" {
2400            expected.account_id = AccountId::from("OTHER-001");
2401        }
2402
2403        let command = GenerateOrderStatusReport::new(
2404            nautilus_core::UUID4::new(),
2405            163.into(),
2406            Some(InstrumentId::from("AUD/USD.SIM")),
2407            Some(ClientOrderId::from("ORDER-167")),
2408            Some(VenueOrderId::from("VENUE-173")),
2409            None,
2410            Some(nautilus_core::UUID4::new()),
2411        );
2412        Python::attach(|py| {
2413            let runtime = client.client.runtime.bind(py);
2414
2415            match result_kind {
2416                "valid" | "foreign" => runtime
2417                    .setattr("result", Py::new(py, expected.clone()).unwrap())
2418                    .unwrap(),
2419                "wrong_type" => runtime.setattr("result", "not a report").unwrap(),
2420                "exception" => runtime
2421                    .setattr("failure", to_pyvalue_err("Report rejected").value(py))
2422                    .unwrap(),
2423                "cancelled" => runtime.setattr("cancelled", true).unwrap(),
2424                _ => {}
2425            }
2426        });
2427
2428        let result = client.generate_order_status_report(&command).await;
2429
2430        match result_kind {
2431            "missing" => assert_eq!(result.unwrap(), None),
2432            "valid" => assert_eq!(result.unwrap(), Some(expected)),
2433            "foreign" => assert_eq!(
2434                result.unwrap_err().to_string(),
2435                "Report account identity does not match its client"
2436            ),
2437            "wrong_type" => assert!(
2438                result
2439                    .unwrap_err()
2440                    .to_string()
2441                    .contains("OrderStatusReport")
2442            ),
2443            "exception" => assert_eq!(
2444                result.unwrap_err().to_string(),
2445                "ValueError: Report rejected"
2446            ),
2447            "cancelled" => assert!(result.unwrap_err().to_string().contains("CancelledError")),
2448            _ => unreachable!(),
2449        }
2450
2451        Python::attach(|py| {
2452            let calls = client.client.runtime.getattr(py, "calls").unwrap();
2453            assert_eq!(calls.bind(py).len().unwrap(), 1);
2454            let call = calls.bind(py).get_item(0).unwrap();
2455            assert_eq!(
2456                call.get_item(0).unwrap().extract::<String>().unwrap(),
2457                "_generate_order_status_report"
2458            );
2459            let args = call.get_item(1).unwrap();
2460            assert_eq!(args.len().unwrap(), 1);
2461            let forwarded = args
2462                .get_item(0)
2463                .unwrap()
2464                .extract::<PyRef<'_, GenerateOrderStatusReport>>()
2465                .unwrap();
2466            assert_eq!(*forwarded, command);
2467        });
2468    }
2469
2470    #[rstest]
2471    #[case::success(false, false, false, None)]
2472    #[case::failure(true, false, false, Some("History unavailable"))]
2473    #[case::canceled(false, true, false, Some("Python data request canceled"))]
2474    #[case::disposed(true, false, true, None)]
2475    fn test_data_request_task_delivers_terminal_failure(
2476        native_execution_client: PythonExecutionClient,
2477        #[case] failed: bool,
2478        #[case] canceled: bool,
2479        #[case] disposed: bool,
2480        #[case] expected_reason: Option<&str>,
2481    ) {
2482        let client = native_execution_client.client;
2483        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2484        client.output.state.lock().sender = Some(sender.into());
2485        let request_id = nautilus_core::UUID4::new();
2486        Python::attach(|py| {
2487            let runtime = client.runtime.bind(py);
2488            if failed {
2489                runtime
2490                    .setattr("failure", to_pyvalue_err("History unavailable").value(py))
2491                    .unwrap();
2492            }
2493
2494            runtime.setattr("cancelled", canceled).unwrap();
2495
2496            let request = RequestQuotes::new(
2497                InstrumentId::from("AUD/USD.SIM"),
2498                None,
2499                None,
2500                None,
2501                Some(client.client_id),
2502                request_id,
2503                UnixNanos::from(179),
2504                None,
2505            );
2506            client.request_quotes(request).unwrap();
2507            if disposed {
2508                client.output.invalidate();
2509            }
2510
2511            let event_loop = runtime.getattr("loop").unwrap();
2512            let tick = py
2513                .import("asyncio")
2514                .unwrap()
2515                .call_method1("sleep", (0,))
2516                .unwrap();
2517            event_loop
2518                .call_method1("run_until_complete", (tick,))
2519                .unwrap();
2520        });
2521
2522        if let Some(reason) = expected_reason {
2523            let DataEvent::RequestFailed(failure) = receiver.try_recv().unwrap() else {
2524                panic!("expected request failure");
2525            };
2526
2527            assert_eq!(failure.request_id, request_id);
2528            assert_eq!(failure.reason, reason);
2529            assert!(failure.response.is_none());
2530        }
2531
2532        assert!(matches!(
2533            receiver.try_recv(),
2534            Err(tokio::sync::mpsc::error::TryRecvError::Empty
2535                | tokio::sync::mpsc::error::TryRecvError::Disconnected)
2536        ));
2537    }
2538
2539    #[rstest]
2540    #[case(false)]
2541    #[case(true)]
2542    fn test_output_rebinding_is_rejected(#[case] disposed: bool) {
2543        Python::initialize();
2544        let output = ClientOutput::py_new();
2545
2546        if disposed {
2547            output.invalidate();
2548        } else {
2549            output.state.lock().bound = true;
2550        }
2551
2552        assert_eq!(
2553            output.bind().unwrap_err().to_string(),
2554            "RuntimeError: Client output cannot be rebound"
2555        );
2556    }
2557
2558    #[rstest]
2559    fn test_output_owner_thread_guards(
2560        execution_output: (
2561            ClientOutput,
2562            tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
2563        ),
2564    ) {
2565        let (output, _) = execution_output;
2566        let other = output.clone();
2567        let unbound = ClientOutput::py_new();
2568
2569        let errors = std::thread::spawn(move || {
2570            [
2571                other.sender().unwrap_err().to_string(),
2572                other.exec_sender().unwrap_err().to_string(),
2573                other.bind().unwrap_err().to_string(),
2574                unbound.bind().unwrap_err().to_string(),
2575            ]
2576        })
2577        .join()
2578        .unwrap();
2579
2580        assert_eq!(
2581            errors,
2582            [
2583                "RuntimeError: Client output requires its owner thread",
2584                "RuntimeError: Client output requires its owner thread",
2585                "RuntimeError: Client output requires its owner thread",
2586                "RuntimeError: Client output requires its owner thread",
2587            ]
2588        );
2589        assert!(output.exec_sender().is_ok());
2590    }
2591
2592    #[rstest]
2593    fn test_output_invalidation_removes_both_senders(
2594        execution_output: (
2595            ClientOutput,
2596            tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
2597        ),
2598    ) {
2599        let (output, _) = execution_output;
2600        let (sender, _) = tokio::sync::mpsc::unbounded_channel();
2601        output.state.lock().sender = Some(sender.into());
2602        output.invalidate();
2603        output.invalidate();
2604        assert_eq!(
2605            output.sender().unwrap_err().to_string(),
2606            "RuntimeError: Client output is not bound to an active node"
2607        );
2608        assert_eq!(
2609            output.exec_sender().unwrap_err().to_string(),
2610            "RuntimeError: Execution output is not bound to an active node"
2611        );
2612    }
2613
2614    #[rstest]
2615    #[case("submitted")]
2616    #[case("accepted")]
2617    #[case("canceled")]
2618    fn test_output_batches_preserve_events_and_reject_foreign_identity(
2619        execution_output: (
2620            ClientOutput,
2621            tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
2622        ),
2623        #[case] kind: &str,
2624    ) {
2625        let (output, mut receiver) = execution_output;
2626
2627        match kind {
2628            "submitted" => {
2629                let first = OrderSubmitted {
2630                    trader_id: TraderId::from("TESTER-001"),
2631                    account_id: AccountId::from("SIM-001"),
2632                    ..Default::default()
2633                };
2634
2635                let mut second = first.clone();
2636                second.client_order_id = ClientOrderId::from("SECOND");
2637                second.ts_event = 113.into();
2638                let events = vec![first, second];
2639                output.py_order_submitted_batch(events.clone()).unwrap();
2640
2641                let ExecutionEvent::OrderSubmittedBatch(batch) = receiver.try_recv().unwrap()
2642                else {
2643                    panic!("Expected submitted batch");
2644                };
2645
2646                assert_eq!(batch.events, events);
2647                let mut foreign = events[0].clone();
2648                foreign.account_id = AccountId::from("OTHER-001");
2649                assert_eq!(
2650                    output
2651                        .py_order_submitted_batch(vec![events[0].clone(), foreign])
2652                        .unwrap_err()
2653                        .to_string(),
2654                    "TypeError: Execution output identity does not match its owner"
2655                );
2656            }
2657            "accepted" => {
2658                let first = OrderAccepted {
2659                    trader_id: TraderId::from("TESTER-001"),
2660                    account_id: AccountId::from("SIM-001"),
2661                    ..Default::default()
2662                };
2663
2664                let mut second = first.clone();
2665                second.client_order_id = ClientOrderId::from("SECOND");
2666                second.ts_event = 127.into();
2667                let events = vec![first, second];
2668                output.py_order_accepted_batch(events.clone()).unwrap();
2669
2670                let ExecutionEvent::OrderAcceptedBatch(batch) = receiver.try_recv().unwrap() else {
2671                    panic!("Expected accepted batch");
2672                };
2673
2674                assert_eq!(batch.events, events);
2675                let mut foreign = events[0].clone();
2676                foreign.trader_id = TraderId::from("OTHER-001");
2677                assert_eq!(
2678                    output
2679                        .py_order_accepted_batch(vec![events[0].clone(), foreign])
2680                        .unwrap_err()
2681                        .to_string(),
2682                    "TypeError: Execution output identity does not match its owner"
2683                );
2684            }
2685            "canceled" => {
2686                let first = OrderCanceled {
2687                    trader_id: TraderId::from("TESTER-001"),
2688                    account_id: Some(AccountId::from("SIM-001")),
2689                    ..Default::default()
2690                };
2691
2692                let mut second = first.clone();
2693                second.client_order_id = ClientOrderId::from("SECOND");
2694                second.account_id = None;
2695                second.ts_event = 131.into();
2696                let events = vec![first, second];
2697                output.py_order_canceled_batch(events.clone()).unwrap();
2698
2699                let ExecutionEvent::OrderCanceledBatch(batch) = receiver.try_recv().unwrap() else {
2700                    panic!("Expected canceled batch");
2701                };
2702
2703                assert_eq!(batch.events, events);
2704                let mut foreign = events[0].clone();
2705                foreign.account_id = Some(AccountId::from("OTHER-001"));
2706                assert_eq!(
2707                    output
2708                        .py_order_canceled_batch(vec![events[0].clone(), foreign])
2709                        .unwrap_err()
2710                        .to_string(),
2711                    "TypeError: Execution output identity does not match its owner"
2712                );
2713            }
2714            _ => unreachable!(),
2715        }
2716
2717        assert!(matches!(
2718            receiver.try_recv(),
2719            Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2720        ));
2721    }
2722
2723    #[rstest]
2724    fn test_closed_execution_channel_reports_failure(
2725        execution_output: (
2726            ClientOutput,
2727            tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
2728        ),
2729    ) {
2730        let (output, receiver) = execution_output;
2731        drop(receiver);
2732
2733        let event = OrderSubmitted {
2734            trader_id: TraderId::from("TESTER-001"),
2735            account_id: AccountId::from("SIM-001"),
2736            ..Default::default()
2737        };
2738
2739        let error = output
2740            .send_exec(ExecutionEvent::Order(OrderEventAny::Submitted(event)))
2741            .unwrap_err();
2742        assert_eq!(error.to_string(), "RuntimeError: channel closed");
2743    }
2744
2745    #[rstest]
2746    fn test_cache_view_rejects_incompatible_borrow_and_recovers() {
2747        Python::initialize();
2748
2749        let cache = Rc::new(RefCell::new(Cache::default()));
2750        let id = CACHE_ID.fetch_add(1, Ordering::Relaxed);
2751        CLIENT_CACHES.with_borrow_mut(|caches| caches.insert(id, CacheView::new(cache.clone())));
2752
2753        let view = PyClientCache { id };
2754        let held = cache.borrow_mut();
2755        assert!(view.py_instrument_ids(None).is_err());
2756        drop(held);
2757        assert_eq!(
2758            view.py_instrument_ids(None).unwrap(),
2759            Vec::<InstrumentId>::new()
2760        );
2761        CLIENT_CACHES.with_borrow_mut(|caches| caches.remove(&id));
2762        assert_eq!(
2763            view.py_instrument_ids(None).unwrap_err().to_string(),
2764            "RuntimeError: Client cache is disposed or accessed from a foreign thread"
2765        );
2766    }
2767    #[rstest]
2768    #[case(None)]
2769    #[case(Some(Price::from("123.456")))]
2770    fn test_option_reference_request_and_response_preserve_identity(
2771        native_execution_client: PythonExecutionClient,
2772        #[case] price: Option<Price>,
2773    ) {
2774        use nautilus_common::messages::data::DataResponse;
2775        use nautilus_model::identifiers::OptionSeriesId;
2776
2777        use crate::python::client::responses::PyOptionChainReferencePriceResponse;
2778
2779        let client = native_execution_client.client;
2780
2781        let series =
2782            OptionSeriesId::new_derived(Venue::from("SIM"), "BTC".into(), "USD".into(), 197.into());
2783
2784        let request = RequestOptionChainReferencePrice::new(
2785            series,
2786            InstrumentId::from("BTC/USD.SIM"),
2787            Some(ClientId::from("SIM")),
2788            nautilus_core::UUID4::new(),
2789            199.into(),
2790            None,
2791        );
2792        client
2793            .request_option_chain_reference_price(request.clone())
2794            .unwrap();
2795        Python::attach(|py| {
2796            let calls = client.runtime.getattr(py, "calls").unwrap();
2797            assert_eq!(calls.bind(py).len().unwrap(), 1);
2798            let call = calls.bind(py).get_item(0).unwrap();
2799            assert_eq!(
2800                call.get_item(0).unwrap().extract::<String>().unwrap(),
2801                "_request_option_chain_reference_price"
2802            );
2803            let args = call.get_item(1).unwrap();
2804            assert_eq!(args.len().unwrap(), 1);
2805            let forwarded = args
2806                .get_item(0)
2807                .unwrap()
2808                .extract::<PyRef<'_, PyRequestOptionChainReferencePrice>>()
2809                .unwrap();
2810            assert_eq!(
2811                args.get_item(0)
2812                    .unwrap()
2813                    .getattr("ts_init")
2814                    .unwrap()
2815                    .extract::<u64>()
2816                    .unwrap(),
2817                199
2818            );
2819            assert_eq!(forwarded.request.series_id, request.series_id);
2820            assert_eq!(forwarded.request.instrument_id, request.instrument_id);
2821            assert_eq!(forwarded.request.client_id, request.client_id);
2822            assert_eq!(forwarded.request.request_id, request.request_id);
2823            assert_eq!(forwarded.request.ts_init, request.ts_init);
2824            assert_eq!(forwarded.request.params, request.params);
2825
2826            let output = ClientOutput::py_new();
2827            let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2828            {
2829                let mut state = output.state.lock();
2830                state.sender = Some(sender.into());
2831                state.client_id = Some(ClientId::from("SIM"));
2832            }
2833
2834            let response = py
2835                .get_type::<PyOptionChainReferencePriceResponse>()
2836                .call1((
2837                    ClientId::from("SIM"),
2838                    series,
2839                    price,
2840                    request.request_id,
2841                    211_u64,
2842                ))
2843                .unwrap();
2844            output.py_response(&response, None, None).unwrap();
2845
2846            let DataEvent::ResponseEnvelope(envelope) = receiver.try_recv().unwrap() else {
2847                panic!("Expected completed response");
2848            };
2849
2850            assert!(envelope.completed());
2851            assert_eq!(envelope.failure_reason(), None);
2852
2853            let Some(DataResponse::OptionChainReferencePrice(response)) = envelope.response()
2854            else {
2855                panic!("Expected option reference response");
2856            };
2857
2858            assert_eq!(response.client_id, ClientId::from("SIM"));
2859            assert_eq!(response.series_id, series);
2860            assert_eq!(response.price, price);
2861            assert_eq!(response.correlation_id, request.request_id);
2862            assert_eq!(response.ts_init, UnixNanos::from(211));
2863            assert_eq!(response.params, None);
2864            let foreign = py
2865                .get_type::<PyOptionChainReferencePriceResponse>()
2866                .call1((
2867                    ClientId::from("OTHER"),
2868                    series,
2869                    price,
2870                    request.request_id,
2871                    223_u64,
2872                ))
2873                .unwrap();
2874            assert_eq!(
2875                output
2876                    .py_response(&foreign, None, None)
2877                    .unwrap_err()
2878                    .to_string(),
2879                "TypeError: Response client identity does not match its owner"
2880            );
2881            assert!(matches!(
2882                receiver.try_recv(),
2883                Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2884            ));
2885        });
2886    }
2887
2888    #[rstest]
2889    #[case::intermediate(Some(false), false, false)]
2890    #[case::completed(Some(true), false, false)]
2891    #[case::provenance(None, true, false)]
2892    #[case::partial_failure(None, false, true)]
2893    fn test_data_output_preserves_completion_and_partial_payload(
2894        #[case] completed: Option<bool>,
2895        #[case] provenance: bool,
2896        #[case] failed: bool,
2897    ) {
2898        use crate::python::client::responses::PyQuotesResponse;
2899        Python::initialize();
2900        let output = ClientOutput::py_new();
2901        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2902        let client_id = ClientId::from("HISTORY");
2903        let root = nautilus_core::UUID4::new();
2904        let source = provenance.then(nautilus_core::UUID4::new);
2905        let instrument_id = InstrumentId::from("AUD/USD.SIM");
2906
2907        let quote = QuoteTick::new(
2908            instrument_id,
2909            Price::from("1.00000"),
2910            Price::from("1.00010"),
2911            Quantity::from("13"),
2912            Quantity::from("17"),
2913            UnixNanos::from(19),
2914            UnixNanos::from(23),
2915        );
2916        {
2917            let mut state = output.state.lock();
2918            state.sender = Some(sender.into());
2919            state.client_id = Some(client_id);
2920        }
2921
2922        Python::attach(|py| {
2923            let response = py
2924                .get_type::<PyQuotesResponse>()
2925                .call1((client_id, instrument_id, vec![quote], root, 227_u64))
2926                .unwrap();
2927
2928            if failed {
2929                output
2930                    .py_request_failed(root, "Missing sibling".to_owned(), Some(&response))
2931                    .unwrap();
2932            } else {
2933                output.py_response(&response, completed, source).unwrap();
2934            }
2935        });
2936
2937        let event = receiver.try_recv().unwrap();
2938
2939        let response = match event {
2940            DataEvent::ResponseEnvelope(envelope) => {
2941                assert_eq!(envelope.correlation_id(), &root);
2942                assert_eq!(envelope.completed(), completed.unwrap_or(true));
2943                assert_eq!(envelope.sub_correlation_id(), source);
2944                assert_eq!(envelope.failure_reason(), None);
2945                envelope.response().unwrap().clone()
2946            }
2947            DataEvent::RequestFailed(failure) if failed => {
2948                assert_eq!(failure.request_id, root);
2949                assert_eq!(failure.reason, "Missing sibling");
2950                failure.response.unwrap()
2951            }
2952            _ => panic!("expected envelope or partial failure"),
2953        };
2954
2955        let DataResponse::Quotes(response) = response else {
2956            panic!("expected quotes");
2957        };
2958
2959        assert_eq!(response.correlation_id, root);
2960        assert_eq!(response.client_id, client_id);
2961        assert_eq!(response.instrument_ids, vec![instrument_id]);
2962        assert_eq!(response.data.as_ref(), &vec![quote]);
2963        assert_eq!(response.start, None);
2964        assert_eq!(response.end, None);
2965        assert_eq!(response.ts_init, UnixNanos::from(227));
2966        assert_eq!(response.params, None);
2967        assert_eq!(
2968            receiver.try_recv().unwrap_err(),
2969            tokio::sync::mpsc::error::TryRecvError::Empty
2970        );
2971    }
2972
2973    #[rstest]
2974    #[case::foreign_client(true)]
2975    #[case::foreign_request(false)]
2976    fn test_data_output_rejects_foreign_partial_response(#[case] foreign_client: bool) {
2977        use crate::python::client::responses::PyQuotesResponse;
2978        Python::initialize();
2979        let output = ClientOutput::py_new();
2980        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2981        let root = nautilus_core::UUID4::new();
2982        {
2983            let mut state = output.state.lock();
2984            state.sender = Some(sender.into());
2985            state.client_id = Some(ClientId::from("HISTORY"));
2986        }
2987
2988        Python::attach(|py| {
2989            let client_id = ClientId::from(if foreign_client { "OTHER" } else { "HISTORY" });
2990
2991            let correlation_id = if foreign_client {
2992                root
2993            } else {
2994                nautilus_core::UUID4::new()
2995            };
2996
2997            let response = py
2998                .get_type::<PyQuotesResponse>()
2999                .call1((
3000                    client_id,
3001                    InstrumentId::from("AUD/USD.SIM"),
3002                    Vec::<QuoteTick>::new(),
3003                    correlation_id,
3004                    229_u64,
3005                ))
3006                .unwrap();
3007            assert_eq!(
3008                output
3009                    .py_request_failed(root, "History failed".to_owned(), Some(&response))
3010                    .unwrap_err()
3011                    .to_string(),
3012                "TypeError: Partial response identity does not match the failed request"
3013            );
3014        });
3015
3016        assert_eq!(
3017            receiver.try_recv().unwrap_err(),
3018            tokio::sync::mpsc::error::TryRecvError::Empty
3019        );
3020    }
3021
3022    #[rstest]
3023    fn test_native_account_output_preserves_fields_and_owner_clock(
3024        mut native_execution_client: PythonExecutionClient,
3025        execution_output: (
3026            ClientOutput,
3027            tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
3028        ),
3029    ) {
3030        let (output, mut receiver) = execution_output;
3031        native_execution_client.client.output = output;
3032        let balances = vec![AccountBalance::new(
3033            Money::from("1000 USD"),
3034            Money::from("17 USD"),
3035            Money::from("983 USD"),
3036        )];
3037        native_execution_client
3038            .generate_account_state(balances.clone(), vec![], false, 233.into(), None)
3039            .unwrap();
3040
3041        let ExecutionEvent::Account(event) = receiver.try_recv().unwrap() else {
3042            panic!("Expected account state");
3043        };
3044
3045        assert_eq!(event.account_id, AccountId::from("SIM-001"));
3046        assert_eq!(event.account_type, AccountType::Cash);
3047        assert_eq!(event.base_currency, None);
3048        assert_eq!(event.balances, balances);
3049        assert_eq!(event.margins, vec![]);
3050        assert!(!event.is_reported);
3051        assert_eq!(event.ts_event, UnixNanos::from(233));
3052        assert_eq!(event.ts_init, UnixNanos::from(0));
3053        assert_eq!(event.info, None);
3054        Python::attach(|py| {
3055            let owned = Py::new(py, event.clone()).unwrap().into_any();
3056            native_execution_client
3057                .client
3058                .output
3059                .py_event(py, owned)
3060                .unwrap();
3061        });
3062
3063        let ExecutionEvent::Account(forwarded) = receiver.try_recv().unwrap() else {
3064            panic!("Expected forwarded account state");
3065        };
3066
3067        assert_eq!(forwarded, event);
3068    }
3069
3070    #[rstest]
3071    fn test_native_client_identity_and_single_use_lifecycle(
3072        mut native_execution_client: PythonExecutionClient,
3073    ) {
3074        let client = &mut native_execution_client;
3075        assert_eq!(client.client_id(), ClientId::from("SIM"));
3076        assert_eq!(client.account_id(), AccountId::from("SIM-001"));
3077        assert_eq!(client.venue(), Venue::from("SIM"));
3078        assert_eq!(client.oms_type(), OmsType::Netting);
3079        assert_eq!(client.position_reconciliation_tolerance(), Decimal::ZERO);
3080        assert_eq!(
3081            client.reset().unwrap_err().to_string(),
3082            "Python clients are single-use"
3083        );
3084        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
3085        nautilus_common::live::runner::replace_data_event_sender(sender);
3086        assert!(client.start().is_ok());
3087        assert!(client.stop().is_ok());
3088        assert!(client.dispose().is_ok());
3089        assert_eq!(
3090            client.client.reset().unwrap_err().to_string(),
3091            "Python clients are single-use"
3092        );
3093        assert_eq!(
3094            client.client.start().unwrap_err().to_string(),
3095            "RuntimeError: Client output cannot be rebound"
3096        );
3097        assert!(client.client.stop().is_ok());
3098        assert!(client.client.dispose().is_ok());
3099        assert_eq!(client.client.venue(), Some(Venue::from("SIM")));
3100        assert!(client.client.is_disconnected());
3101
3102        let cache = Rc::new(RefCell::new(Cache::default()));
3103
3104        client.cache = CacheView::new(cache.clone());
3105        assert_eq!(client.get_account(), None);
3106        let held = cache.borrow_mut();
3107        assert_eq!(client.get_account(), None);
3108        drop(held);
3109        let account = AccountAny::default();
3110        client.identity.account_id = account.id();
3111        cache.borrow_mut().add_account(account.clone()).unwrap();
3112        assert_eq!(client.get_account(), Some(account));
3113    }
3114
3115    #[rstest]
3116    #[case("true")]
3117    #[case("false")]
3118    #[case("wrong_type")]
3119    #[case("exception")]
3120    fn test_native_coverage_hooks_fail_closed_and_preserve_arguments(
3121        native_execution_client: PythonExecutionClient,
3122        #[case] mode: &str,
3123    ) {
3124        let client = native_execution_client;
3125        Python::attach(|py| {
3126            let runtime = client.client.runtime.bind(py);
3127
3128            match mode {
3129                "true" => runtime.setattr("result", true).unwrap(),
3130                "false" => runtime.setattr("result", false).unwrap(),
3131                "wrong_type" => runtime.setattr("result", "invalid").unwrap(),
3132                "exception" => runtime
3133                    .setattr("failure", to_pyvalue_err("Coverage unavailable").value(py))
3134                    .unwrap(),
3135                _ => unreachable!(),
3136            }
3137        });
3138
3139        let venue = Venue::from("OTHER");
3140        let instrument_id = InstrumentId::from("AUD/USD.SIM");
3141        assert_eq!(client.handles_order_venue(venue), mode == "true");
3142        assert_eq!(
3143            client.provides_bulk_position_coverage(instrument_id),
3144            mode == "true"
3145        );
3146        Python::attach(|py| {
3147            let calls = client.client.runtime.getattr(py, "calls").unwrap();
3148            assert_eq!(calls.bind(py).len().unwrap(), 2);
3149            let first = calls.bind(py).get_item(0).unwrap();
3150            assert_eq!(
3151                first.get_item(0).unwrap().extract::<String>().unwrap(),
3152                "venue"
3153            );
3154            assert_eq!(
3155                first.get_item(1).unwrap().extract::<Venue>().unwrap(),
3156                venue
3157            );
3158            let second = calls.bind(py).get_item(1).unwrap();
3159            assert_eq!(
3160                second.get_item(0).unwrap().extract::<String>().unwrap(),
3161                "positions"
3162            );
3163            assert_eq!(
3164                second
3165                    .get_item(1)
3166                    .unwrap()
3167                    .extract::<InstrumentId>()
3168                    .unwrap(),
3169                instrument_id
3170            );
3171        });
3172    }
3173
3174    #[rstest]
3175    #[case("none")]
3176    #[case("money")]
3177    #[case("exception")]
3178    fn test_native_commission_hook_preserves_inputs_and_propagates_failure(
3179        native_execution_client: PythonExecutionClient,
3180        #[case] mode: &str,
3181    ) {
3182        let client = native_execution_client;
3183        let commission = Money::from("0.17 USD");
3184        Python::attach(|py| {
3185            let runtime = client.client.runtime.bind(py);
3186
3187            if mode == "money" {
3188                runtime.setattr("result", commission).unwrap();
3189            } else if mode == "exception" {
3190                runtime
3191                    .setattr("failure", to_pyvalue_err("Fee unavailable").value(py))
3192                    .unwrap();
3193            }
3194        });
3195
3196        let instrument =
3197            InstrumentAny::CurrencyPair(nautilus_model::instruments::stubs::audusd_sim());
3198        let quantity = Quantity::from("17");
3199        let price = Price::from("0.71231");
3200        let result =
3201            client.calculate_commission(&instrument, quantity, price, LiquiditySide::Maker);
3202
3203        if mode == "exception" {
3204            assert_eq!(
3205                result.unwrap_err().to_string(),
3206                "ValueError: Fee unavailable"
3207            );
3208        } else {
3209            assert_eq!(
3210                result.unwrap(),
3211                if mode == "money" {
3212                    Some(commission)
3213                } else {
3214                    None
3215                }
3216            );
3217        }
3218
3219        Python::attach(|py| {
3220            let calls = client.client.runtime.getattr(py, "calls").unwrap();
3221            assert_eq!(calls.bind(py).len().unwrap(), 1);
3222            let call = calls.bind(py).get_item(0).unwrap();
3223            assert_eq!(
3224                call.get_item(0).unwrap().extract::<String>().unwrap(),
3225                "commission"
3226            );
3227            let args = call.get_item(1).unwrap();
3228            assert_eq!(args.len().unwrap(), 4);
3229            assert_eq!(
3230                pyobject_to_instrument_any(py, args.get_item(0).unwrap().unbind()).unwrap(),
3231                instrument
3232            );
3233            assert_eq!(
3234                args.get_item(1).unwrap().extract::<Quantity>().unwrap(),
3235                quantity
3236            );
3237            assert_eq!(args.get_item(2).unwrap().extract::<Price>().unwrap(), price);
3238            assert_eq!(
3239                args.get_item(3)
3240                    .unwrap()
3241                    .extract::<LiquiditySide>()
3242                    .unwrap(),
3243                LiquiditySide::Maker
3244            );
3245        });
3246    }
3247    macro_rules! report_sequence_test {
3248        ($name:ident, $method:ident, $command_type:ty, [$($borrow:tt)*], $command:expr, $report:expr) => {
3249            #[rstest]
3250            #[case("empty")]
3251            #[case("valid")]
3252            #[case("foreign")]
3253            #[case("wrong_type")]
3254            #[tokio::test]
3255            async fn $name(native_execution_client: PythonExecutionClient, #[case] mode: &str) {
3256                let client = native_execution_client;
3257                let mut command = $command;
3258                command.correlation_id = Some(nautilus_core::UUID4::new());
3259                command.causation_id = Some(nautilus_core::UUID4::new());
3260                command.log_receipt_level = LogLevel::Debug;
3261                let params = Params::new([("probe".to_owned(), serde_json::Value::from(17))].into());
3262                command.params = Some(params);
3263                let first = $report;
3264                let mut second = first.clone();
3265                second.ts_init = 251.into();
3266                let expected = if mode == "empty" { vec![] } else { vec![first, second] };
3267                Python::attach(|py| {
3268                    if mode != "wrong_type" {
3269                        let mut reports = expected.clone();
3270                        if mode == "foreign" {
3271                            reports[1].account_id = AccountId::from("OTHER-001");
3272                        }
3273                        let reports = reports.into_iter().map(|report| Py::new(py, report).unwrap()).collect::<Vec<_>>();
3274                        client.client.runtime.bind(py).setattr("result", reports).unwrap();
3275                    }
3276                });
3277                let result = client.$method($($borrow)* command.clone()).await;
3278                if mode == "foreign" {
3279                    assert_eq!(result.unwrap_err().to_string(), "Report account identity does not match its client");
3280                } else if mode == "wrong_type" {
3281                    assert!(result.unwrap_err().to_string().starts_with("TypeError:"));
3282                } else {
3283                    assert_eq!(result.unwrap(), expected);
3284                }
3285                Python::attach(|py| {
3286                    let calls = client.client.runtime.getattr(py, "calls").unwrap();
3287                    assert_eq!(calls.bind(py).len().unwrap(), 1);
3288                    let call = calls.bind(py).get_item(0).unwrap();
3289                    assert_eq!(call.get_item(0).unwrap().extract::<String>().unwrap(), concat!("_", stringify!($method)));
3290                    let args = call.get_item(1).unwrap();
3291                    assert_eq!(args.len().unwrap(), 1);
3292                    let argument = args.get_item(0).unwrap();
3293                    let forwarded = argument.extract::<PyRef<'_, $command_type>>().unwrap();
3294                    assert_eq!(*forwarded, command);
3295                    assert_eq!(argument.getattr("command_id").unwrap().extract::<nautilus_core::UUID4>().unwrap(), command.command_id);
3296                    assert_eq!(argument.getattr("ts_init").unwrap().extract::<u64>().unwrap(), 241);
3297                    assert_eq!(argument.getattr("correlation_id").unwrap().extract::<Option<nautilus_core::UUID4>>().unwrap(), command.correlation_id);
3298                    assert_eq!(argument.getattr("causation_id").unwrap().extract::<Option<nautilus_core::UUID4>>().unwrap(), command.causation_id);
3299                    assert_eq!(argument.getattr("instrument_id").unwrap().extract::<Option<InstrumentId>>().unwrap(), command.instrument_id);
3300                    assert_eq!(argument.getattr("start").unwrap().extract::<u64>().unwrap(), 229);
3301                    assert_eq!(argument.getattr("end").unwrap().extract::<u64>().unwrap(), 239);
3302                    assert_eq!(argument.getattr("log_receipt_level").unwrap().extract::<LogLevel>().unwrap(), command.log_receipt_level);
3303                    let params = argument.getattr("params").unwrap();
3304                    assert_eq!(params.len().unwrap(), 1);
3305                    assert_eq!(params.get_item("probe").unwrap().extract::<i64>().unwrap(), 17);
3306                });
3307            }
3308        };
3309    }
3310
3311    report_sequence_test!(
3312        test_order_report_sequences_validate_every_member,
3313        generate_order_status_reports, GenerateOrderStatusReports, [&],
3314        GenerateOrderStatusReports::new(nautilus_core::UUID4::new(), 241.into(), true,
3315            Some(InstrumentId::from("AUD/USD.SIM")), Some(229.into()), Some(239.into()), None, None),
3316        order_report()
3317    );
3318
3319    report_sequence_test!(
3320        test_fill_report_sequences_validate_every_member,
3321        generate_fill_reports,
3322        GenerateFillReports,
3323        [],
3324        GenerateFillReports::new(
3325            nautilus_core::UUID4::new(),
3326            241.into(),
3327            Some(InstrumentId::from("AUD/USD.SIM")),
3328            Some(VenueOrderId::from("VENUE-173")),
3329            Some(229.into()),
3330            Some(239.into()),
3331            None,
3332            None
3333        ),
3334        fill_report()
3335    );
3336
3337    report_sequence_test!(
3338        test_position_report_sequences_validate_every_member,
3339        generate_position_status_reports, GeneratePositionStatusReports, [&],
3340        GeneratePositionStatusReports::new(nautilus_core::UUID4::new(), 241.into(),
3341            Some(InstrumentId::from("AUD/USD.SIM")), Some(229.into()), Some(239.into()), None, None),
3342        position_report()
3343    );
3344    #[rstest]
3345    #[case("order")]
3346    #[case("fill")]
3347    #[case("position")]
3348    #[case("mass")]
3349    #[case("order_with_fills")]
3350    fn test_report_output_preserves_each_payload_family(
3351        execution_output: (
3352            ClientOutput,
3353            tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
3354        ),
3355        order_report: OrderStatusReport,
3356        fill_report: FillReport,
3357        position_report: PositionStatusReport,
3358        #[case] kind: &str,
3359    ) {
3360        let (output, mut receiver) = execution_output;
3361        {
3362            let mut state = output.state.lock();
3363            state.client_id = Some(ClientId::from("SIM"));
3364            state.venue = Some(Venue::from("SIM"));
3365        }
3366
3367        let mut mass = ExecutionMassStatus::new(
3368            ClientId::from("SIM"),
3369            AccountId::from("SIM-001"),
3370            Venue::from("SIM"),
3371            257.into(),
3372            None,
3373        );
3374        mass.add_order_reports(vec![order_report.clone()]);
3375        mass.add_fill_reports(vec![fill_report.clone()]);
3376        mass.add_position_reports(vec![position_report.clone()]);
3377        Python::attach(|py| {
3378            let report = match kind {
3379                "order" | "order_with_fills" => {
3380                    Py::new(py, order_report.clone()).unwrap().into_any()
3381                }
3382                "fill" => Py::new(py, fill_report.clone()).unwrap().into_any(),
3383                "position" => Py::new(py, position_report.clone()).unwrap().into_any(),
3384                "mass" => Py::new(py, mass.clone()).unwrap().into_any(),
3385                _ => unreachable!(),
3386            };
3387
3388            output
3389                .py_report(
3390                    report.bind(py),
3391                    if kind == "order_with_fills" {
3392                        Some(vec![fill_report.clone()])
3393                    } else {
3394                        None
3395                    },
3396                )
3397                .unwrap();
3398        });
3399
3400        let ExecutionEvent::Report(report) = receiver.try_recv().unwrap() else {
3401            panic!("Expected execution report");
3402        };
3403
3404        match (kind, report) {
3405            ("order", ExecutionReport::Order(report)) => assert_eq!(*report, order_report),
3406            ("fill", ExecutionReport::Fill(report)) => assert_eq!(*report, fill_report),
3407            ("position", ExecutionReport::Position(report)) => assert_eq!(*report, position_report),
3408            ("mass", ExecutionReport::MassStatus(report)) => assert_eq!(*report, mass),
3409            ("order_with_fills", ExecutionReport::OrderWithFills(report, fills)) => {
3410                assert_eq!(*report, order_report);
3411                assert_eq!(fills, vec![fill_report]);
3412            }
3413            _ => panic!("Wrong execution report variant"),
3414        }
3415    }
3416
3417    #[fixture]
3418    fn order_report() -> OrderStatusReport {
3419        OrderStatusReport::new(
3420            AccountId::from("SIM-001"),
3421            InstrumentId::from("AUD/USD.SIM"),
3422            Some(ClientOrderId::from("ORDER-167")),
3423            VenueOrderId::from("VENUE-173"),
3424            Some(OrderSide::Buy),
3425            OrderType::Limit,
3426            TimeInForce::Gtc,
3427            OrderStatus::Accepted,
3428            Quantity::from("17"),
3429            Quantity::from("3"),
3430            149.into(),
3431            151.into(),
3432            157.into(),
3433            None,
3434        )
3435    }
3436
3437    #[fixture]
3438    fn fill_report() -> FillReport {
3439        FillReport::new(
3440            AccountId::from("SIM-001"),
3441            InstrumentId::from("AUD/USD.SIM"),
3442            VenueOrderId::from("VENUE-173"),
3443            TradeId::from("TRADE-179"),
3444            OrderSide::Buy,
3445            Quantity::from("3"),
3446            Price::from("0.71231"),
3447            Money::from("0.17 USD"),
3448            LiquiditySide::Maker,
3449            Some(ClientOrderId::from("ORDER-167")),
3450            Some(PositionId::from("POSITION-181")),
3451            191.into(),
3452            193.into(),
3453            None,
3454        )
3455    }
3456
3457    #[fixture]
3458    fn position_report() -> PositionStatusReport {
3459        PositionStatusReport::new(
3460            AccountId::from("SIM-001"),
3461            InstrumentId::from("AUD/USD.SIM"),
3462            PositionSide::Short,
3463            Quantity::from("17"),
3464            191.into(),
3465            193.into(),
3466            None,
3467            Some(PositionId::from("POSITION-181")),
3468            Some(Decimal::new(71231, 5)),
3469        )
3470    }
3471    #[rstest]
3472    #[case("none")]
3473    #[case("valid")]
3474    #[case("client")]
3475    #[case("account")]
3476    #[case("venue")]
3477    #[case("order")]
3478    #[case("fill")]
3479    #[case("position")]
3480    #[case("wrong_type")]
3481    #[tokio::test]
3482    async fn test_mass_report_checks_outer_and_nested_identity(
3483        native_execution_client: PythonExecutionClient,
3484        mut order_report: OrderStatusReport,
3485        mut fill_report: FillReport,
3486        mut position_report: PositionStatusReport,
3487        #[case] kind: &str,
3488    ) {
3489        let client = native_execution_client;
3490        let account = AccountId::from("OTHER-001");
3491
3492        if kind == "order" {
3493            order_report.account_id = account;
3494        }
3495
3496        if kind == "fill" {
3497            fill_report.account_id = account;
3498        }
3499
3500        if kind == "position" {
3501            position_report.account_id = account;
3502        }
3503
3504        let mut mass = ExecutionMassStatus::new(
3505            ClientId::from(if kind == "client" { "OTHER" } else { "SIM" }),
3506            if kind == "account" {
3507                account
3508            } else {
3509                AccountId::from("SIM-001")
3510            },
3511            Venue::from(if kind == "venue" { "OTHER" } else { "SIM" }),
3512            263.into(),
3513            None,
3514        );
3515
3516        mass.add_order_reports(vec![order_report]);
3517        mass.add_fill_reports(vec![fill_report]);
3518        mass.add_position_reports(vec![position_report]);
3519        Python::attach(|py| {
3520            if kind == "wrong_type" {
3521                client
3522                    .client
3523                    .runtime
3524                    .bind(py)
3525                    .setattr("result", 17)
3526                    .unwrap();
3527            } else if kind != "none" {
3528                client
3529                    .client
3530                    .runtime
3531                    .bind(py)
3532                    .setattr("result", Py::new(py, mass.clone()).unwrap())
3533                    .unwrap();
3534            }
3535        });
3536
3537        let result = client.generate_mass_status(Some(23)).await;
3538
3539        match kind {
3540            "none" => assert_eq!(result.unwrap(), None),
3541            "valid" => assert_eq!(result.unwrap(), Some(mass)),
3542            "wrong_type" => assert!(result.unwrap_err().to_string().starts_with("TypeError:")),
3543            _ => assert_eq!(
3544                result.unwrap_err().to_string(),
3545                "Mass status identity does not match its client"
3546            ),
3547        }
3548
3549        Python::attach(|py| {
3550            let calls = client.client.runtime.getattr(py, "calls").unwrap();
3551            assert_eq!(calls.bind(py).len().unwrap(), 1);
3552            let call = calls.bind(py).get_item(0).unwrap();
3553            assert_eq!(
3554                call.get_item(0).unwrap().extract::<String>().unwrap(),
3555                "_generate_mass_status"
3556            );
3557            assert_eq!(
3558                call.get_item(1).unwrap().extract::<(u64,)>().unwrap(),
3559                (23,)
3560            );
3561        });
3562    }
3563    #[rstest]
3564    fn test_cache_bytes_and_order_lists_are_owned_snapshots() {
3565        Python::initialize();
3566
3567        let cache = Rc::new(RefCell::new(Cache::default()));
3568        let id = CACHE_ID.fetch_add(1, Ordering::Relaxed);
3569        CLIENT_CACHES.with_borrow_mut(|caches| caches.insert(id, CacheView::new(cache.clone())));
3570
3571        let view = PyClientCache { id };
3572
3573        let list = OrderList::new(
3574            OrderListId::from("OL-SNAPSHOT"),
3575            InstrumentId::from("AUD/USD.SIM"),
3576            StrategyId::from("S-SNAPSHOT"),
3577            vec![
3578                ClientOrderId::from("O-FIRST"),
3579                ClientOrderId::from("O-SECOND"),
3580            ],
3581            UnixNanos::from(173),
3582        );
3583        cache
3584            .borrow_mut()
3585            .add("probe", vec![0, 17, 255].into())
3586            .unwrap();
3587        cache.borrow_mut().add_order_list(list.clone()).unwrap();
3588        let mut bytes = view.py_get("probe").unwrap().unwrap();
3589        let snapshot = view.py_order_list(list.id).unwrap().unwrap();
3590        bytes[1] = 29;
3591        cache
3592            .borrow_mut()
3593            .add("probe", vec![31, 0, 127].into())
3594            .unwrap();
3595        assert_eq!(bytes, vec![0, 29, 255]);
3596        assert_eq!(view.py_get("probe").unwrap(), Some(vec![31, 0, 127]));
3597        assert_eq!(snapshot, list);
3598        assert_eq!(view.py_order_list(list.id).unwrap(), Some(list));
3599        assert_eq!(view.py_get("missing").unwrap(), None);
3600        assert_eq!(
3601            view.py_order_list(OrderListId::from("OL-MISSING")).unwrap(),
3602            None
3603        );
3604        CLIENT_CACHES.with_borrow_mut(|caches| caches.remove(&id));
3605    }
3606    #[rstest]
3607    fn test_factory_metadata() {
3608        Python::initialize();
3609        Python::attach(|py| {
3610            let data = PythonDataFactory {
3611                factory: py.None(),
3612                clients: PythonClients::default(),
3613            };
3614
3615            let execution = PythonExecutionFactory {
3616                factory: py.None(),
3617                clients: PythonClients::default(),
3618            };
3619
3620            assert_eq!(data.name(), "Python");
3621            assert_eq!(data.config_type(), "DataClientConfig");
3622            assert_eq!(execution.name(), "Python");
3623            assert_eq!(execution.config_type(), "ExecutionClientConfig");
3624        });
3625    }
3626
3627    #[rstest]
3628    #[case(None)]
3629    #[case(Some(nautilus_core::UUID4::new()))]
3630    fn test_subscription_metadata_preserves_timestamp_and_correlation(
3631        #[case] correlation_id: Option<nautilus_core::UUID4>,
3632    ) {
3633        Python::initialize();
3634        Python::attach(|py| {
3635            let command_id = nautilus_core::UUID4::new();
3636
3637            let command = SubscribeQuotes::new(
3638                InstrumentId::from("AUD/USD.SIM"),
3639                Some(ClientId::from("SIM")),
3640                Some(Venue::from("SIM")),
3641                command_id,
3642                283.into(),
3643                correlation_id,
3644                None,
3645            );
3646            let command_py = Py::new(py, command).unwrap();
3647            assert_eq!(
3648                command_py
3649                    .getattr(py, "command_id")
3650                    .unwrap()
3651                    .extract::<nautilus_core::UUID4>(py)
3652                    .unwrap(),
3653                command_id
3654            );
3655            assert_eq!(
3656                command_py
3657                    .getattr(py, "ts_init")
3658                    .unwrap()
3659                    .extract::<u64>(py)
3660                    .unwrap(),
3661                283
3662            );
3663            assert_eq!(
3664                command_py
3665                    .getattr(py, "correlation_id")
3666                    .unwrap()
3667                    .extract::<Option<nautilus_core::UUID4>>(py)
3668                    .unwrap(),
3669                correlation_id
3670            );
3671        });
3672    }
3673}