Skip to main content

nautilus_live/python/client/
responses.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Implements typed data responses that Python adapters construct to answer core requests.
17//!
18//! Provides PyO3 constructors and read-only payload access for historical data, instruments,
19//! and option-chain reference prices. Converts supported response objects into Rust data
20//! responses with their client identifiers for delivery through the live runner.
21
22use 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/// An owned `InstrumentResponse` response.
112#[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/// An owned `InstrumentsResponse` response.
209#[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/// An owned `CustomDataResponse` response.
314#[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/// An owned `OptionChainReferencePriceResponse` response.
422#[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}