1use std::sync::Arc;
23
24use nautilus_common::messages::data::{
25 BarsResponse, BookDeltasResponse, BookDepthResponse, BookResponse, CustomDataResponse,
26 DataResponse, FundingRatesResponse, InstrumentResponse, InstrumentsResponse,
27 OptionChainReferencePriceResponse, QuotesResponse, TradesResponse,
28};
29use nautilus_core::{
30 UUID4,
31 python::{
32 params::{params_to_pydict, pydict_to_params},
33 to_pytype_err,
34 },
35};
36use nautilus_model::{
37 data::{
38 Bar, BarType, CustomData, DataType, FundingRateUpdate, OrderBookDelta, OrderBookDepth,
39 QuoteTick, TradeTick,
40 },
41 identifiers::{ClientId, InstrumentId, OptionSeriesId, Venue},
42 orderbook::OrderBook,
43 python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
44 types::Price,
45};
46use pyo3::{prelude::*, types::PyDict};
47
48macro_rules! response {
49 ($wrapper:ident, $response:ident, $name:tt, $key:ident: $key_type:ty, $data:ty) => {
50 #[doc = concat!("An owned ", $name, " response.")]
51 #[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
52 #[pyclass(name = $name, module = "nautilus_trader.live", frozen, from_py_object)]
53 #[derive(Debug, Clone)]
54 pub struct $wrapper {
55 pub(crate) response: $response,
56 }
57
58 #[pyo3_stub_gen::derive::gen_stub_pymethods]
59 #[pymethods]
60 impl $wrapper {
61 #[new]
62 #[pyo3(signature = (client_id, $key, data, correlation_id, ts_init, start=None, end=None, params=None))]
63 #[expect(clippy::too_many_arguments, reason = "response fields cross the Python boundary together")]
64 fn py_new(py: Python<'_>, client_id: ClientId, $key: $key_type, data: $data,
65 correlation_id: UUID4, ts_init: u64, start: Option<u64>, end: Option<u64>,
66 params: Option<Py<PyDict>>) -> PyResult<Self> {
67 Ok(Self { response: $response {
68 client_id, $key, data, correlation_id, ts_init: ts_init.into(),
69 start: start.map(Into::into), end: end.map(Into::into),
70 params: params.as_ref().map(|params| pydict_to_params(py, params)).transpose()?.flatten(),
71 }})
72 }
73
74 #[getter]
75 fn client_id(&self) -> ClientId { self.response.client_id }
76
77 #[getter]
78 fn $key(&self) -> $key_type { self.response.$key }
79
80 #[getter]
81 fn data(&self) -> $data { self.response.data.clone() }
82
83 #[getter]
84 fn correlation_id(&self) -> UUID4 { self.response.correlation_id }
85
86 #[getter]
87 fn ts_init(&self) -> u64 { self.response.ts_init.as_u64() }
88
89 #[getter]
90 fn start(&self) -> Option<u64> { self.response.start.map(|value| value.as_u64()) }
91
92 #[getter]
93 fn end(&self) -> Option<u64> { self.response.end.map(|value| value.as_u64()) }
94
95 #[getter]
96 fn params(&self, py: Python<'_>) -> PyResult<Option<Py<PyDict>>> {
97 self.response.params.as_ref().map(|params| params_to_pydict(py, params)).transpose()
98 }
99 }
100 };
101}
102
103response!(PyQuotesResponse, QuotesResponse, "QuotesResponse", instrument_id: InstrumentId, Vec<QuoteTick>);
104response!(PyTradesResponse, TradesResponse, "TradesResponse", instrument_id: InstrumentId, Vec<TradeTick>);
105response!(PyFundingRatesResponse, FundingRatesResponse, "FundingRatesResponse", instrument_id: InstrumentId, Vec<FundingRateUpdate>);
106response!(PyBarsResponse, BarsResponse, "BarsResponse", bar_type: BarType, Vec<Bar>);
107response!(PyBookDeltasResponse, BookDeltasResponse, "BookDeltasResponse", instrument_id: InstrumentId, Vec<OrderBookDelta>);
108response!(PyBookDepthResponse, BookDepthResponse, "BookDepthResponse", instrument_id: InstrumentId, Vec<OrderBookDepth>);
109response!(PyBookResponse, BookResponse, "BookResponse", instrument_id: InstrumentId, OrderBook);
110
111#[pyclass(
113 name = "InstrumentResponse",
114 module = "nautilus_trader.live",
115 frozen,
116 from_py_object
117)]
118#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
119#[derive(Debug, Clone)]
120pub struct PyInstrumentResponse {
121 response: InstrumentResponse,
122}
123
124#[pyo3_stub_gen::derive::gen_stub_pymethods]
125#[pymethods]
126impl PyInstrumentResponse {
127 #[new]
128 #[pyo3(signature = (client_id, instrument_id, data, correlation_id, ts_init, start=None, end=None, params=None))]
129 #[expect(
130 clippy::too_many_arguments,
131 clippy::needless_pass_by_value,
132 reason = "response fields cross the Python boundary together"
133 )]
134 fn py_new(
135 py: Python<'_>,
136 client_id: ClientId,
137 instrument_id: InstrumentId,
138 data: Py<PyAny>,
139 correlation_id: UUID4,
140 ts_init: u64,
141 start: Option<u64>,
142 end: Option<u64>,
143 params: Option<Py<PyDict>>,
144 ) -> PyResult<Self> {
145 Ok(Self {
146 response: InstrumentResponse {
147 client_id,
148 instrument_id,
149 data: pyobject_to_instrument_any(py, data)?,
150 correlation_id,
151 ts_init: ts_init.into(),
152 start: start.map(Into::into),
153 end: end.map(Into::into),
154 params: params
155 .as_ref()
156 .map(|params| pydict_to_params(py, params))
157 .transpose()?
158 .flatten(),
159 },
160 })
161 }
162
163 #[getter]
164 fn client_id(&self) -> ClientId {
165 self.response.client_id
166 }
167
168 #[getter]
169 fn instrument_id(&self) -> InstrumentId {
170 self.response.instrument_id
171 }
172
173 #[getter]
174 fn data(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
175 instrument_any_to_pyobject(py, self.response.data.clone())
176 }
177
178 #[getter]
179 fn correlation_id(&self) -> UUID4 {
180 self.response.correlation_id
181 }
182
183 #[getter]
184 fn ts_init(&self) -> u64 {
185 self.response.ts_init.as_u64()
186 }
187
188 #[getter]
189 fn start(&self) -> Option<u64> {
190 self.response.start.map(|value| value.as_u64())
191 }
192
193 #[getter]
194 fn end(&self) -> Option<u64> {
195 self.response.end.map(|value| value.as_u64())
196 }
197
198 #[getter]
199 fn params(&self, py: Python<'_>) -> PyResult<Option<Py<PyDict>>> {
200 self.response
201 .params
202 .as_ref()
203 .map(|params| params_to_pydict(py, params))
204 .transpose()
205 }
206}
207
208#[pyclass(
210 name = "InstrumentsResponse",
211 module = "nautilus_trader.live",
212 frozen,
213 from_py_object
214)]
215#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
216#[derive(Debug, Clone)]
217pub struct PyInstrumentsResponse {
218 response: InstrumentsResponse,
219}
220
221#[pyo3_stub_gen::derive::gen_stub_pymethods]
222#[pymethods]
223impl PyInstrumentsResponse {
224 #[new]
225 #[pyo3(signature = (client_id, venue, data, correlation_id, ts_init, start=None, end=None, params=None))]
226 #[expect(
227 clippy::too_many_arguments,
228 clippy::needless_pass_by_value,
229 reason = "response fields cross the Python boundary together"
230 )]
231 fn py_new(
232 py: Python<'_>,
233 client_id: ClientId,
234 venue: Venue,
235 data: Vec<Py<PyAny>>,
236 correlation_id: UUID4,
237 ts_init: u64,
238 start: Option<u64>,
239 end: Option<u64>,
240 params: Option<Py<PyDict>>,
241 ) -> PyResult<Self> {
242 Ok(Self {
243 response: InstrumentsResponse {
244 client_id,
245 venue,
246 data: data
247 .into_iter()
248 .map(|data| pyobject_to_instrument_any(py, data))
249 .collect::<PyResult<Vec<_>>>()?,
250 correlation_id,
251 ts_init: ts_init.into(),
252 start: start.map(Into::into),
253 end: end.map(Into::into),
254 params: params
255 .as_ref()
256 .map(|params| pydict_to_params(py, params))
257 .transpose()?
258 .flatten(),
259 },
260 })
261 }
262
263 #[getter]
264 fn client_id(&self) -> ClientId {
265 self.response.client_id
266 }
267
268 #[getter]
269 fn venue(&self) -> Venue {
270 self.response.venue
271 }
272
273 #[getter]
274 fn data(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
275 self.response
276 .data
277 .iter()
278 .cloned()
279 .map(|data| instrument_any_to_pyobject(py, data))
280 .collect()
281 }
282
283 #[getter]
284 fn correlation_id(&self) -> UUID4 {
285 self.response.correlation_id
286 }
287
288 #[getter]
289 fn ts_init(&self) -> u64 {
290 self.response.ts_init.as_u64()
291 }
292
293 #[getter]
294 fn start(&self) -> Option<u64> {
295 self.response.start.map(|value| value.as_u64())
296 }
297
298 #[getter]
299 fn end(&self) -> Option<u64> {
300 self.response.end.map(|value| value.as_u64())
301 }
302
303 #[getter]
304 fn params(&self, py: Python<'_>) -> PyResult<Option<Py<PyDict>>> {
305 self.response
306 .params
307 .as_ref()
308 .map(|params| params_to_pydict(py, params))
309 .transpose()
310 }
311}
312
313#[pyclass(
315 name = "CustomDataResponse",
316 module = "nautilus_trader.live",
317 frozen,
318 from_py_object
319)]
320#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
321#[derive(Debug, Clone)]
322pub struct PyCustomDataResponse {
323 response: CustomDataResponse,
324}
325
326#[pyo3_stub_gen::derive::gen_stub_pymethods]
327#[pymethods]
328impl PyCustomDataResponse {
329 #[new]
330 #[pyo3(signature = (client_id, data_type, venue, data, correlation_id, ts_init, start=None, end=None, params=None))]
331 #[expect(
332 clippy::too_many_arguments,
333 clippy::needless_pass_by_value,
334 reason = "response fields cross the Python boundary together"
335 )]
336 fn py_new(
337 py: Python<'_>,
338 client_id: ClientId,
339 data_type: DataType,
340 venue: Option<Venue>,
341 data: Vec<CustomData>,
342 correlation_id: UUID4,
343 ts_init: u64,
344 start: Option<u64>,
345 end: Option<u64>,
346 params: Option<Py<PyDict>>,
347 ) -> PyResult<Self> {
348 Ok(Self {
349 response: CustomDataResponse {
350 client_id,
351 data_type,
352 venue,
353 data: Arc::new(data),
354 correlation_id,
355 ts_init: ts_init.into(),
356 start: start.map(Into::into),
357 end: end.map(Into::into),
358 params: params
359 .as_ref()
360 .map(|params| pydict_to_params(py, params))
361 .transpose()?
362 .flatten(),
363 },
364 })
365 }
366
367 #[getter]
368 fn client_id(&self) -> ClientId {
369 self.response.client_id
370 }
371
372 #[getter]
373 fn data_type(&self) -> DataType {
374 self.response.data_type.clone()
375 }
376
377 #[getter]
378 fn venue(&self) -> Option<Venue> {
379 self.response.venue
380 }
381
382 #[getter]
383 fn data(&self, _py: Python<'_>) -> PyResult<Vec<CustomData>> {
384 self.response
385 .data
386 .downcast_ref::<Vec<CustomData>>()
387 .cloned()
388 .ok_or_else(|| to_pytype_err("Expected custom data response payload"))
389 }
390
391 #[getter]
392 fn correlation_id(&self) -> UUID4 {
393 self.response.correlation_id
394 }
395
396 #[getter]
397 fn ts_init(&self) -> u64 {
398 self.response.ts_init.as_u64()
399 }
400
401 #[getter]
402 fn start(&self) -> Option<u64> {
403 self.response.start.map(|value| value.as_u64())
404 }
405
406 #[getter]
407 fn end(&self) -> Option<u64> {
408 self.response.end.map(|value| value.as_u64())
409 }
410
411 #[getter]
412 fn params(&self, py: Python<'_>) -> PyResult<Option<Py<PyDict>>> {
413 self.response
414 .params
415 .as_ref()
416 .map(|params| params_to_pydict(py, params))
417 .transpose()
418 }
419}
420
421#[pyclass(
423 name = "OptionChainReferencePriceResponse",
424 module = "nautilus_trader.live",
425 frozen,
426 from_py_object
427)]
428#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
429#[derive(Debug, Clone)]
430pub struct PyOptionChainReferencePriceResponse {
431 response: OptionChainReferencePriceResponse,
432}
433
434#[pyo3_stub_gen::derive::gen_stub_pymethods]
435#[pymethods]
436impl PyOptionChainReferencePriceResponse {
437 #[new]
438 #[pyo3(signature = (client_id, series_id, price, correlation_id, ts_init, params=None))]
439 #[expect(
440 clippy::needless_pass_by_value,
441 reason = "response fields cross the Python boundary together"
442 )]
443 fn py_new(
444 py: Python<'_>,
445 client_id: ClientId,
446 series_id: OptionSeriesId,
447 price: Option<Price>,
448 correlation_id: UUID4,
449 ts_init: u64,
450 params: Option<Py<PyDict>>,
451 ) -> PyResult<Self> {
452 Ok(Self {
453 response: OptionChainReferencePriceResponse {
454 client_id,
455 series_id,
456 price,
457 correlation_id,
458 ts_init: ts_init.into(),
459
460 params: params
461 .as_ref()
462 .map(|params| pydict_to_params(py, params))
463 .transpose()?
464 .flatten(),
465 },
466 })
467 }
468
469 #[getter]
470 fn client_id(&self) -> ClientId {
471 self.response.client_id
472 }
473
474 #[getter]
475 fn series_id(&self) -> OptionSeriesId {
476 self.response.series_id
477 }
478
479 #[getter]
480 fn price(&self) -> Option<Price> {
481 self.response.price
482 }
483
484 #[getter]
485 fn correlation_id(&self) -> UUID4 {
486 self.response.correlation_id
487 }
488
489 #[getter]
490 fn ts_init(&self) -> u64 {
491 self.response.ts_init.as_u64()
492 }
493
494 #[getter]
495 fn params(&self, py: Python<'_>) -> PyResult<Option<Py<PyDict>>> {
496 self.response
497 .params
498 .as_ref()
499 .map(|params| params_to_pydict(py, params))
500 .transpose()
501 }
502}
503
504pub(crate) fn extract_response(response: &Bound<'_, PyAny>) -> PyResult<(ClientId, DataResponse)> {
505 if response.is_instance_of::<PyQuotesResponse>() {
506 let response = response.extract::<PyQuotesResponse>()?.response;
507 return Ok((response.client_id, DataResponse::Quotes(response)));
508 }
509
510 if response.is_instance_of::<PyTradesResponse>() {
511 let response = response.extract::<PyTradesResponse>()?.response;
512 return Ok((response.client_id, DataResponse::Trades(response)));
513 }
514
515 if response.is_instance_of::<PyFundingRatesResponse>() {
516 let response = response.extract::<PyFundingRatesResponse>()?.response;
517 return Ok((response.client_id, DataResponse::FundingRates(response)));
518 }
519
520 if response.is_instance_of::<PyBarsResponse>() {
521 let response = response.extract::<PyBarsResponse>()?.response;
522 return Ok((response.client_id, DataResponse::Bars(response)));
523 }
524
525 if response.is_instance_of::<PyBookDeltasResponse>() {
526 let response = response.extract::<PyBookDeltasResponse>()?.response;
527 return Ok((response.client_id, DataResponse::BookDeltas(response)));
528 }
529
530 if response.is_instance_of::<PyBookDepthResponse>() {
531 let response = response.extract::<PyBookDepthResponse>()?.response;
532 return Ok((response.client_id, DataResponse::BookDepth(response)));
533 }
534
535 if response.is_instance_of::<PyBookResponse>() {
536 let response = response.extract::<PyBookResponse>()?.response;
537 return Ok((response.client_id, DataResponse::Book(response)));
538 }
539
540 if response.is_instance_of::<PyInstrumentResponse>() {
541 let response = response.extract::<PyInstrumentResponse>()?.response;
542 return Ok((
543 response.client_id,
544 DataResponse::Instrument(Box::new(response)),
545 ));
546 }
547
548 if response.is_instance_of::<PyInstrumentsResponse>() {
549 let response = response.extract::<PyInstrumentsResponse>()?.response;
550 return Ok((response.client_id, DataResponse::Instruments(response)));
551 }
552
553 if response.is_instance_of::<PyCustomDataResponse>() {
554 let response = response.extract::<PyCustomDataResponse>()?.response;
555 return Ok((response.client_id, DataResponse::Data(response)));
556 }
557
558 if response.is_instance_of::<PyOptionChainReferencePriceResponse>() {
559 let response = response
560 .extract::<PyOptionChainReferencePriceResponse>()?
561 .response;
562 return Ok((
563 response.client_id,
564 DataResponse::OptionChainReferencePrice(response),
565 ));
566 }
567
568 Err(to_pytype_err(
569 "Expected a Nautilus data response from the installed wheel",
570 ))
571}
572
573pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
574 m.add_class::<PyInstrumentResponse>()?;
575 m.add_class::<PyInstrumentsResponse>()?;
576 m.add_class::<PyCustomDataResponse>()?;
577 m.add_class::<PyOptionChainReferencePriceResponse>()?;
578
579 m.add_class::<PyQuotesResponse>()?;
580 m.add_class::<PyTradesResponse>()?;
581 m.add_class::<PyFundingRatesResponse>()?;
582 m.add_class::<PyBarsResponse>()?;
583 m.add_class::<PyBookDeltasResponse>()?;
584 m.add_class::<PyBookDepthResponse>()?;
585 m.add_class::<PyBookResponse>()?;
586 Ok(())
587}