nautilus_databento/python/
live.rs1use std::path::PathBuf;
19
20use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
21use nautilus_model::{
22 identifiers::InstrumentId,
23 python::{data::data_to_pyobject, instruments::instrument_any_to_pyobject},
24};
25use pyo3::{IntoPyObjectExt, prelude::*};
26
27use super::types::DatabentoSubscriptionAck;
28pub use crate::live::DatabentoLiveClient;
29use crate::live::{DatabentoMessage, is_command_send_error};
30
31impl DatabentoLiveClient {
32 async fn process_messages(
33 mut msg_rx: tokio::sync::mpsc::UnboundedReceiver<DatabentoMessage>,
34 callback: Py<PyAny>,
35 callback_pyo3: Py<PyAny>,
36 ) -> PyResult<()> {
37 log::debug!("Processing messages...");
38 while let Some(msg) = msg_rx.recv().await {
40 log::trace!("Received message: {msg:?}");
41
42 match msg {
43 DatabentoMessage::Data(data) => Python::attach(|py| -> PyResult<()> {
44 let py_obj = data_to_pyobject(py, data)?;
45 call_python(py, &callback, py_obj);
46 Ok(())
47 })?,
48 DatabentoMessage::Instrument(data) => {
49 Python::attach(|py| match instrument_any_to_pyobject(py, *data) {
50 Ok(py_obj) => call_python(py, &callback, py_obj),
51 Err(e) => log::error!("Failed creating instrument: {e}"),
52 });
53 }
54 DatabentoMessage::Status(data) => {
55 Python::attach(|py| -> PyResult<()> {
56 call_python(py, &callback_pyo3, data.into_py_any(py)?);
57 Ok(())
58 })?;
59 }
60 DatabentoMessage::Imbalance(data) => {
61 Python::attach(|py| -> PyResult<()> {
62 call_python(py, &callback_pyo3, data.into_py_any(py)?);
63 Ok(())
64 })?;
65 }
66 DatabentoMessage::Statistics(data) => {
67 Python::attach(|py| -> PyResult<()> {
68 call_python(py, &callback_pyo3, data.into_py_any(py)?);
69 Ok(())
70 })?;
71 }
72 DatabentoMessage::SubscriptionAck(ack) => {
73 Python::attach(|py| -> PyResult<()> {
74 let py_obj = DatabentoSubscriptionAck::from(ack).into_py_any(py)?;
75 call_python(py, &callback_pyo3, py_obj);
76 Ok(())
77 })?;
78 }
79 DatabentoMessage::Close => {
80 break;
82 }
83 DatabentoMessage::Error(e) => {
84 return Err(to_pyruntime_err(e));
86 }
87 }
88 }
89
90 msg_rx.close();
91 log::debug!("Closed message receiver");
92
93 Ok(())
94 }
95}
96
97fn call_python(py: Python, callback: &Py<PyAny>, py_obj: Py<PyAny>) {
98 if let Err(e) = callback.call1(py, (py_obj,)) {
99 if !e.to_string().contains("CancelledError") {
101 log::error!("Error calling Python: {e}");
102 }
103 }
104}
105
106#[pymethods]
107#[pyo3_stub_gen::derive::gen_stub_pymethods]
108impl DatabentoLiveClient {
109 #[new]
115 #[pyo3(signature = (key, dataset, publishers_filepath, use_exchange_as_venue, bars_timestamp_on_close=None, reconnect_timeout_mins=None))]
116 pub fn py_new(
117 key: String,
118 dataset: String,
119 publishers_filepath: PathBuf,
120 use_exchange_as_venue: bool,
121 bars_timestamp_on_close: Option<bool>,
122 reconnect_timeout_mins: Option<i64>,
123 ) -> PyResult<Self> {
124 Self::new(
125 key,
126 dataset,
127 publishers_filepath,
128 use_exchange_as_venue,
129 bars_timestamp_on_close,
130 reconnect_timeout_mins,
131 )
132 .map_err(to_pyvalue_err)
133 }
134
135 #[getter]
136 fn dataset(&self) -> &str {
137 self.dataset.as_str()
138 }
139
140 #[pyo3(name = "is_running")]
141 const fn py_is_running(&self) -> bool {
142 self.is_running()
143 }
144
145 #[pyo3(name = "is_closed")]
146 const fn py_is_closed(&self) -> bool {
147 self.is_closed()
148 }
149
150 #[pyo3(name = "subscribe")]
157 #[pyo3(signature = (schema, instrument_ids, start=None, snapshot=None, price_precisions=None, stype_in=None))]
158 fn py_subscribe(
159 &mut self,
160 schema: String,
161 instrument_ids: Vec<InstrumentId>,
162 start: Option<u64>,
163 snapshot: Option<bool>,
164 price_precisions: Option<Vec<Option<u8>>>,
165 stype_in: Option<String>,
166 ) -> PyResult<()> {
167 if let Err(e) = self.subscribe(
168 schema,
169 instrument_ids,
170 start,
171 snapshot,
172 price_precisions,
173 stype_in,
174 ) {
175 return if is_command_send_error(&e) {
176 Err(to_pyruntime_err(e))
177 } else {
178 Err(to_pyvalue_err(e))
179 };
180 }
181
182 Ok(())
183 }
184
185 #[pyo3(name = "start")]
191 fn py_start<'py>(
192 &mut self,
193 py: Python<'py>,
194 callback: Py<PyAny>,
195 callback_pyo3: Py<PyAny>,
196 ) -> PyResult<Bound<'py, PyAny>> {
197 let (mut feed_handler, msg_rx) = self.start().map_err(to_pyruntime_err)?;
198
199 pyo3_async_runtimes::tokio::future_into_py(py, async move {
200 let (proc_handle, feed_handle) = tokio::join!(
201 Self::process_messages(msg_rx, callback, callback_pyo3),
202 feed_handler.run(),
203 );
204
205 if let Err(e) = proc_handle {
206 log::error!("Message processor error: {e}");
207 return Err(e);
208 }
209
210 if let Err(e) = feed_handle {
211 log::error!("Feed handler error: {e}");
212 return Err(to_pyruntime_err(e));
213 }
214
215 log::debug!("Live client completed");
216 Ok(())
217 })
218 }
219
220 #[pyo3(name = "close")]
227 fn py_close(&mut self) -> PyResult<()> {
228 self.close().map_err(to_pyruntime_err)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use std::path::PathBuf;
235
236 use pyo3::exceptions::{PyRuntimeError, PyValueError};
237 use rstest::rstest;
238
239 use super::*;
240
241 fn create_test_client() -> DatabentoLiveClient {
242 DatabentoLiveClient::new(
243 "test-api-key".to_string(),
244 "GLBX.MDP3".to_string(),
245 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json"),
246 true,
247 None,
248 None,
249 )
250 .unwrap()
251 }
252
253 #[rstest]
254 fn test_py_subscribe_maps_invalid_input_to_value_error() {
255 Python::initialize();
256 let mut client = create_test_client();
257
258 let err = client
259 .py_subscribe(
260 "definition".to_string(),
261 vec![InstrumentId::from("ES.FUT.GLBX")],
262 None,
263 None,
264 None,
265 Some("not-a-stype".to_string()),
266 )
267 .unwrap_err();
268
269 Python::attach(|py| {
270 assert!(err.is_instance_of::<PyValueError>(py));
271 });
272 }
273
274 #[rstest]
275 fn test_py_subscribe_maps_command_send_error_to_runtime_error() {
276 Python::initialize();
277 let mut client = create_test_client();
278 let (feed_handler, msg_rx) = client.start().unwrap();
279 drop(feed_handler);
280 drop(msg_rx);
281
282 let err = client
283 .py_subscribe(
284 "definition".to_string(),
285 vec![InstrumentId::from("ES.FUT.GLBX")],
286 None,
287 None,
288 None,
289 Some("parent".to_string()),
290 )
291 .unwrap_err();
292
293 Python::attach(|py| {
294 assert!(err.is_instance_of::<PyRuntimeError>(py));
295 });
296 }
297}