1use std::fmt::Debug;
17
18use nautilus_core::{
19 UUID4,
20 python::{params::params_to_pydict, to_pyruntime_err, to_pytype_err, to_pyvalue_err},
21};
22use nautilus_model::{
23 data::CustomData,
24 identifiers::ClientId,
25 python::{data::PyNautilusDataType, instruments::instrument_any_to_pyobject},
26};
27use pyo3::{
28 IntoPyObjectExt,
29 prelude::*,
30 sync::PyOnceLock,
31 types::{PyDict, PyList},
32};
33use ustr::Ustr;
34
35use crate::{
36 actor::{DataActor, DataActorNative, ResponseScope, registry::try_get_actor_unchecked},
37 messages::data::{DataResponse, DataResponseEnvelope},
38 msgbus::TypedHandler,
39 python::logging::format_exception,
40};
41
42#[pyclass(name = "DataResponse", module = "nautilus_trader.common", frozen)]
44#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
45pub struct PyDataResponse {
46 envelope: DataResponseEnvelope,
47 data: PyOnceLock<Py<PyAny>>,
48}
49
50impl Debug for PyDataResponse {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct(stringify!(PyDataResponse))
53 .field("envelope", &self.envelope)
54 .finish_non_exhaustive()
55 }
56}
57
58#[pymethods]
59#[pyo3_stub_gen::derive::gen_stub_pymethods]
60impl PyDataResponse {
61 #[getter]
62 fn correlation_id(&self) -> String {
63 self.envelope.correlation_id().to_string()
64 }
65
66 #[getter]
67 fn completed(&self) -> bool {
68 self.envelope.completed()
69 }
70
71 #[getter]
72 fn sub_correlation_id(&self) -> Option<String> {
73 self.envelope.sub_correlation_id().map(|id| id.to_string())
74 }
75
76 #[getter]
77 fn failure_reason(&self) -> Option<&str> {
78 self.envelope.failure_reason()
79 }
80
81 #[getter]
82 fn kind(&self) -> Option<PyNautilusDataType> {
83 self.envelope
84 .response()
85 .map(|r| PyNautilusDataType::new(r.data_kind()))
86 }
87
88 #[getter]
89 fn client_id(&self) -> Option<ClientId> {
90 self.envelope.response().map(|r| *r.client_id())
91 }
92
93 #[getter]
94 fn params(&self, py: Python<'_>) -> PyResult<Option<Py<PyDict>>> {
95 self.envelope
96 .response()
97 .and_then(DataResponse::params)
98 .map(|params| params_to_pydict(py, params))
99 .transpose()
100 }
101
102 #[getter]
103 fn start(&self) -> Option<u64> {
104 self.envelope
105 .response()
106 .and_then(DataResponse::start)
107 .map(|v| v.as_u64())
108 }
109
110 #[getter]
111 fn end(&self) -> Option<u64> {
112 self.envelope
113 .response()
114 .and_then(DataResponse::end)
115 .map(|v| v.as_u64())
116 }
117
118 #[getter]
119 fn ts_init(&self) -> Option<u64> {
120 self.envelope.response().map(|r| r.ts_init().as_u64())
121 }
122
123 #[getter]
124 fn data(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
125 self.data
126 .get_or_try_init(py, || response_data(py, self.envelope.response()))
127 .map(|value| value.clone_ref(py))
128 }
129}
130
131#[doc(hidden)]
133pub fn response_handler<T>(
134 actor_id: Ustr,
135 instance: fn(&T) -> PyResult<Option<Py<PyAny>>>,
136 callback: Option<TypedHandler<DataResponseEnvelope>>,
137) -> TypedHandler<DataResponseEnvelope>
138where
139 T: DataActor + DataActorNative + Debug + 'static,
140{
141 TypedHandler::from(move |envelope: &DataResponseEnvelope| {
142 let Some((mut scope, instance)) = prepare_response::<T>(actor_id, instance, envelope)
143 else {
144 return;
145 };
146
147 Python::attach(|py| {
148 let wrapper = PyDataResponse {
149 envelope: envelope.clone(),
150 data: PyOnceLock::new(),
151 };
152
153 if let Some(instance) = &instance
154 && let Err(e) = dispatch_payload(py, instance, &wrapper)
155 {
156 log::error!(
157 "Python historical data hook failed: {}",
158 format_exception(&e)
159 );
160 }
161
162 let current = try_get_actor_unchecked::<T>(&actor_id)
163 .is_some_and(|actor| actor.core().complete_request(envelope, &scope));
164
165 if current {
166 dispatch_completion(py, instance.as_ref(), wrapper);
167 dispatch_callback::<T>(actor_id, &scope, envelope, callback.as_ref());
168 }
169
170 scope.leave();
171 drain_groups::<T>(py, actor_id, instance.as_ref(), &scope);
172 });
173 })
174}
175
176#[doc(hidden)]
182pub fn request_handler<T>(
183 py: Python<'_>,
184 actor_id: Ustr,
185 instance: fn(&T) -> PyResult<Option<Py<PyAny>>>,
186 callback: Option<Py<PyAny>>,
187) -> PyResult<TypedHandler<DataResponseEnvelope>>
188where
189 T: DataActor + DataActorNative + Debug + 'static,
190{
191 let callback = completion_handler(py, callback)?;
192 Ok(response_handler(actor_id, instance, callback))
193}
194
195fn drain_groups<T>(
196 py: Python<'_>,
197 actor_id: Ustr,
198 instance: Option<&Py<PyAny>>,
199 origin: &ResponseScope,
200) where
201 T: DataActor + DataActorNative + Debug + 'static,
202{
203 loop {
204 let prepared = try_get_actor_unchecked::<T>(&actor_id).and_then(|actor| {
205 if !actor.core().is_response_current(origin) {
206 return None;
207 }
208
209 let (envelope, callback) = actor.core().next_group_response()?;
210 let scope = actor.core().begin_response(envelope.correlation_id())?;
211 actor
212 .core()
213 .complete_request(&envelope, &scope)
214 .then_some((envelope, scope, callback))
215 });
216
217 let Some((envelope, scope, callback)) = prepared else {
218 return;
219 };
220
221 let wrapper = PyDataResponse {
222 envelope: envelope.clone(),
223 data: PyOnceLock::new(),
224 };
225
226 dispatch_completion(py, instance, wrapper);
227 dispatch_callback::<T>(actor_id, &scope, &envelope, callback.as_ref());
228 }
229}
230
231#[doc(hidden)]
237pub fn extract_request_id(value: &Bound<'_, PyAny>) -> PyResult<UUID4> {
238 if let Ok(id) = value.extract::<UUID4>() {
239 return Ok(id);
240 }
241
242 value.extract::<String>()?.parse().map_err(to_pyvalue_err)
243}
244
245#[doc(hidden)]
252pub fn request_join<T>(
253 py: Python<'_>,
254 actor_id: Ustr,
255 instance: fn(&T) -> PyResult<Option<Py<PyAny>>>,
256 request_ids: Vec<UUID4>,
257 request_id: Option<UUID4>,
258 callback: Option<Py<PyAny>>,
259) -> PyResult<String>
260where
261 T: DataActor + DataActorNative + Debug + 'static,
262{
263 let callback = completion_handler(py, callback)?;
264
265 let (id, mut scope, instance) = {
266 let actor = try_get_actor_unchecked::<T>(&actor_id)
267 .ok_or_else(|| to_pyruntime_err("Actor is not registered"))?;
268 let instance = instance(&actor)?;
269 let id = actor
270 .core()
271 .register_request_group(request_ids, request_id, callback)
272 .map_err(to_pyvalue_err)?;
273 let scope = actor
274 .core()
275 .begin_response(&id)
276 .expect("newly registered group is pending");
277 (id, scope, instance)
278 };
279
280 scope.leave();
281 drain_groups::<T>(py, actor_id, instance.as_ref(), &scope);
282 Ok(id.to_string())
283}
284
285fn completion_handler(
286 py: Python<'_>,
287 callback: Option<Py<PyAny>>,
288) -> PyResult<Option<TypedHandler<DataResponseEnvelope>>> {
289 let Some(callback) = callback else {
290 return Ok(None);
291 };
292
293 if !callback.bind(py).is_callable() {
294 return Err(to_pytype_err("callback must be callable"));
295 }
296
297 Ok(Some(TypedHandler::from(
298 move |envelope: &DataResponseEnvelope| {
299 Python::attach(|py| {
300 if let Err(e) = callback.call1(py, (envelope.correlation_id().to_string(),)) {
301 log::error!(
302 "Python historical request callback failed: {}",
303 format_exception(&e)
304 );
305 }
306 });
307 },
308 )))
309}
310
311fn dispatch_callback<T>(
312 actor_id: Ustr,
313 scope: &ResponseScope,
314 envelope: &DataResponseEnvelope,
315 callback: Option<&TypedHandler<DataResponseEnvelope>>,
316) where
317 T: DataActor + DataActorNative + Debug + 'static,
318{
319 let Some(callback) = callback else {
320 return;
321 };
322
323 if envelope.completed()
324 && try_get_actor_unchecked::<T>(&actor_id)
325 .is_some_and(|actor| actor.core().is_response_current(scope))
326 {
327 callback.0.handle(envelope);
328 }
329}
330
331fn dispatch_completion(py: Python<'_>, instance: Option<&Py<PyAny>>, wrapper: PyDataResponse) {
332 let Some(instance) = instance else {
333 return;
334 };
335
336 let result = Py::new(py, wrapper)
337 .and_then(|response| instance.call_method1(py, "on_data_response", (response,)));
338
339 if let Err(e) = result {
340 log::error!("Python on_data_response failed: {}", format_exception(&e));
341 }
342}
343
344fn prepare_response<T>(
345 actor_id: Ustr,
346 instance: fn(&T) -> PyResult<Option<Py<PyAny>>>,
347 envelope: &DataResponseEnvelope,
348) -> Option<(ResponseScope, Option<Py<PyAny>>)>
349where
350 T: DataActor + DataActorNative + Debug + 'static,
351{
352 let Some(actor) = try_get_actor_unchecked::<T>(&actor_id) else {
353 log::error!("Actor {actor_id} not found for response handling");
354 return None;
355 };
356
357 let scope = actor.core().begin_response(envelope.correlation_id())?;
358 let result = match envelope.response() {
359 Some(DataResponse::Quotes(r)) => actor.core().handle_indicators_for_quotes(&r.data),
360 Some(DataResponse::Trades(r)) => actor.core().handle_indicators_for_trades(&r.data),
361 Some(DataResponse::Bars(r)) => actor.core().handle_indicators_for_bars(&r.data),
362 _ => Ok(()),
363 };
364
365 if let Err(e) = result {
366 log::error!("Historical indicator update failed: {e}");
367 }
368
369 match instance(&actor) {
370 Ok(instance) => Some((scope, instance)),
371 Err(e) => {
372 log::error!(
373 "Python response instance lookup failed: {}",
374 format_exception(&e)
375 );
376 Some((scope, None))
377 }
378 }
379}
380
381fn dispatch_payload(
382 py: Python<'_>,
383 instance: &Py<PyAny>,
384 wrapper: &PyDataResponse,
385) -> PyResult<()> {
386 let Some(response) = wrapper.envelope.response() else {
387 return Ok(());
388 };
389
390 match response {
391 DataResponse::Instruments(r) => {
392 for instrument in &r.data {
393 let value = instrument_any_to_pyobject(py, instrument.clone())?;
394 instance.call_method1(py, "on_instrument", (value,))?;
395 }
396
397 return Ok(());
398 }
399 DataResponse::OptionChainReferencePrice(_) => return Ok(()),
400 _ => {}
401 }
402
403 let hook = match response {
404 DataResponse::Data(_) => "on_historical_data",
405 DataResponse::Instrument(_) => "on_instrument",
406 DataResponse::Book(_) => "on_book",
407 DataResponse::BookDeltas(_) => "on_historical_book_deltas",
408 DataResponse::BookDepth(_) => "on_historical_book_depth",
409 DataResponse::Quotes(_) => "on_historical_quotes",
410 DataResponse::Trades(_) => "on_historical_trades",
411 DataResponse::FundingRates(_) => "on_historical_funding_rates",
412 DataResponse::Bars(_) => "on_historical_bars",
413 DataResponse::Instruments(_) | DataResponse::OptionChainReferencePrice(_) => unreachable!(),
414 };
415
416 let data = wrapper.data(py)?;
417 instance.call_method1(py, hook, (data,))?;
418 Ok(())
419}
420
421fn response_data(py: Python<'_>, response: Option<&DataResponse>) -> PyResult<Py<PyAny>> {
422 let Some(response) = response else {
423 return Ok(py.None());
424 };
425
426 match response {
427 DataResponse::Data(r) => {
428 if let Some(data) = r.data.downcast_ref::<CustomData>() {
429 data.clone().into_py_any(py)
430 } else if let Some(data) = r.data.downcast_ref::<Vec<CustomData>>() {
431 data.clone().into_py_any(py)
432 } else {
433 Err(to_pytype_err("Unsupported historical custom data payload"))
434 }
435 }
436 DataResponse::Instrument(r) => instrument_any_to_pyobject(py, r.data.clone()),
437 DataResponse::Instruments(r) => {
438 let values = r
439 .data
440 .iter()
441 .map(|value| instrument_any_to_pyobject(py, value.clone()))
442 .collect::<PyResult<Vec<_>>>()?;
443 Ok(PyList::new(py, values)?.into_any().unbind())
444 }
445 DataResponse::Book(r) => r.data.clone().into_py_any(py),
446 DataResponse::BookDeltas(r) => r.data.to_vec().into_py_any(py),
447 DataResponse::BookDepth(r) => r.data.to_vec().into_py_any(py),
448 DataResponse::Quotes(r) => r.data.to_vec().into_py_any(py),
449 DataResponse::Trades(r) => r.data.to_vec().into_py_any(py),
450 DataResponse::FundingRates(r) => r.data.to_vec().into_py_any(py),
451 DataResponse::Bars(r) => r.data.to_vec().into_py_any(py),
452 DataResponse::OptionChainReferencePrice(r) => r.price.into_py_any(py),
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use nautilus_core::{UUID4, UnixNanos};
459 use nautilus_model::{
460 data::{BatchView, DataType, NautilusDataType, QuoteTick},
461 identifiers::ClientId,
462 };
463 use rstest::rstest;
464
465 use super::*;
466 use crate::messages::data::{CustomDataResponse, QuotesResponse};
467
468 #[rstest]
469 fn test_response_metadata_is_lazy_and_terminal_can_omit_payload() {
470 Python::initialize();
471 Python::attach(|py| {
472 let root = UUID4::new();
473
474 let payload = CustomDataResponse::new(
475 root,
476 ClientId::new("HISTORY"),
477 None,
478 DataType::new("Opaque", None, None),
479 42_u8,
480 Some(UnixNanos::from(11)),
481 Some(UnixNanos::from(23)),
482 UnixNanos::from(37),
483 None,
484 );
485
486 let wrapper = PyDataResponse {
487 envelope: DataResponseEnvelope::new(DataResponse::Data(payload), false, None),
488 data: PyOnceLock::new(),
489 };
490
491 assert_eq!(wrapper.correlation_id(), root.to_string());
492 assert_eq!(
493 wrapper.kind().unwrap().inner(),
494 NautilusDataType::Custom {
495 type_name: "Opaque".to_owned()
496 }
497 );
498 assert_eq!(wrapper.client_id(), Some(ClientId::new("HISTORY")));
499 assert_eq!(
500 (wrapper.start(), wrapper.end(), wrapper.ts_init()),
501 (Some(11), Some(23), Some(37))
502 );
503 assert!(!wrapper.completed());
504 assert!(wrapper.data.get(py).is_none());
505 assert!(wrapper.data(py).is_err());
506 assert!(wrapper.data.get(py).is_none());
507
508 let terminal = PyDataResponse {
509 envelope: DataResponseEnvelope::terminal(root, Some("source failed".to_owned())),
510 data: PyOnceLock::new(),
511 };
512
513 assert!(terminal.data(py).unwrap().is_none(py));
514 assert!(terminal.completed());
515 assert_eq!(terminal.failure_reason(), Some("source failed"));
516 assert!(terminal.kind().is_none());
517 assert!(terminal.client_id().is_none());
518 });
519 }
520
521 #[rstest]
522 fn test_response_data_is_an_ordinary_cached_python_list() {
523 Python::initialize();
524 Python::attach(|py| {
525 let payload = CustomDataResponse::new(
526 UUID4::new(),
527 ClientId::new("HISTORY"),
528 None,
529 DataType::new("Custom", None, None),
530 Vec::<CustomData>::new(),
531 None,
532 None,
533 UnixNanos::from(37),
534 None,
535 );
536
537 let wrapper = PyDataResponse {
538 envelope: DataResponseEnvelope::new(DataResponse::Data(payload), true, None),
539 data: PyOnceLock::new(),
540 };
541
542 let first = wrapper.data(py).unwrap();
543 let second = wrapper.data(py).unwrap();
544 assert!(first.is(&second));
545 assert_eq!(first.bind(py).cast::<PyList>().unwrap().len(), 0);
546 });
547 }
548
549 #[rstest]
550 #[case::visible(1..2)]
551 #[case::empty(1..1)]
552 fn test_response_data_list_contains_only_visible_rows(#[case] range: std::ops::Range<usize>) {
553 Python::initialize();
554 Python::attach(|py| {
555 let rows = [11, 23, 37].map(|ts| QuoteTick {
556 ts_init: UnixNanos::from(ts),
557 ..Default::default()
558 });
559
560 let expected = rows[range.clone()].to_vec();
561
562 let payload = QuotesResponse::new(
563 UUID4::new(),
564 ClientId::new("HISTORY"),
565 rows[1].instrument_id,
566 BatchView::new(std::sync::Arc::new(rows.to_vec()), range),
567 None,
568 None,
569 UnixNanos::from(41),
570 None,
571 );
572
573 let wrapper = PyDataResponse {
574 envelope: DataResponseEnvelope::new(DataResponse::Quotes(payload), true, None),
575 data: PyOnceLock::new(),
576 };
577
578 let first = wrapper.data(py).unwrap();
579 let second = wrapper.data(py).unwrap();
580 let list = first.bind(py).cast::<PyList>().unwrap();
581
582 assert!(first.is(&second));
583 assert_eq!(list.extract::<Vec<QuoteTick>>().unwrap(), expected);
584 });
585 }
586}