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