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