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