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