1use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
19use nautilus_model::{
20 enums::OrderSide,
21 identifiers::{ClientOrderId, InstrumentId, VenueOrderId},
22 python::instruments::pyobject_to_instrument_any,
23};
24use pyo3::{conversion::IntoPyObjectExt, prelude::*, types::PyDict};
25
26use crate::{
27 broadcast::canceller::{CancelBroadcaster, CancelBroadcasterConfig},
28 common::enums::BitmexEnvironment,
29};
30
31#[pymethods]
32#[pyo3_stub_gen::derive::gen_stub_pymethods]
33impl CancelBroadcaster {
34 #[new]
42 #[pyo3(signature = (
43 pool_size,
44 api_key=None,
45 api_secret=None,
46 base_url=None,
47 environment=BitmexEnvironment::Mainnet,
48 timeout_secs=60,
49 max_retries=3,
50 retry_delay_ms=1_000,
51 retry_delay_max_ms=5_000,
52 recv_window_ms=10_000,
53 max_requests_per_second=10,
54 max_requests_per_minute=120,
55 health_check_interval_secs=30,
56 health_check_timeout_secs=5,
57 expected_reject_patterns=None,
58 idempotent_success_patterns=None,
59 proxy_urls=None
60 ))]
61 #[expect(clippy::too_many_arguments)]
62 fn py_new(
63 pool_size: usize,
64 api_key: Option<String>,
65 api_secret: Option<String>,
66 base_url: Option<String>,
67 environment: BitmexEnvironment,
68 timeout_secs: u64,
69 max_retries: u32,
70 retry_delay_ms: u64,
71 retry_delay_max_ms: u64,
72 recv_window_ms: u64,
73 max_requests_per_second: u32,
74 max_requests_per_minute: u32,
75 health_check_interval_secs: u64,
76 health_check_timeout_secs: u64,
77 expected_reject_patterns: Option<Vec<String>>,
78 idempotent_success_patterns: Option<Vec<String>>,
79 proxy_urls: Option<Vec<Option<String>>>,
80 ) -> PyResult<Self> {
81 let config = CancelBroadcasterConfig {
82 pool_size,
83 api_key: api_key.map(SecretString::from),
84 api_secret: api_secret.map(SecretString::from),
85 base_url,
86 environment,
87 timeout_secs,
88 max_retries,
89 retry_delay_ms,
90 retry_delay_max_ms,
91 recv_window_ms,
92 max_requests_per_second,
93 max_requests_per_minute,
94 health_check_interval_secs,
95 health_check_timeout_secs,
96 expected_reject_patterns: expected_reject_patterns
97 .unwrap_or_else(|| CancelBroadcasterConfig::default().expected_reject_patterns),
98 idempotent_success_patterns: idempotent_success_patterns
99 .unwrap_or_else(|| CancelBroadcasterConfig::default().idempotent_success_patterns),
100 proxy_urls: proxy_urls
101 .unwrap_or_default()
102 .into_iter()
103 .map(|value| value.map(SecretString::from))
104 .collect(),
105 };
106
107 Self::new(config).map_err(to_pyvalue_err)
108 }
109
110 #[pyo3(name = "cache_instrument")]
112 fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
113 let inst_any = pyobject_to_instrument_any(py, instrument)?;
114 self.cache_instrument(&inst_any);
115 Ok(())
116 }
117
118 #[pyo3(name = "start")]
124 fn py_start<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
125 let broadcaster = self.clone_for_async();
126 pyo3_async_runtimes::tokio::future_into_py(py, async move {
127 broadcaster.start().await.map_err(to_pyvalue_err)
128 })
129 }
130
131 #[pyo3(name = "stop")]
133 fn py_stop<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
134 let broadcaster = self.clone_for_async();
135 pyo3_async_runtimes::tokio::future_into_py(py, async move {
136 broadcaster.stop().await;
137 Ok(())
138 })
139 }
140
141 #[pyo3(name = "broadcast_cancel")]
153 fn py_broadcast_cancel<'py>(
154 &self,
155 py: Python<'py>,
156 instrument_id: InstrumentId,
157 client_order_id: Option<ClientOrderId>,
158 venue_order_id: Option<VenueOrderId>,
159 ) -> PyResult<Bound<'py, PyAny>> {
160 let broadcaster = self.clone_for_async();
161 pyo3_async_runtimes::tokio::future_into_py(py, async move {
162 let report = broadcaster
163 .broadcast_cancel(instrument_id, client_order_id, venue_order_id)
164 .await
165 .map_err(to_pyvalue_err)?;
166
167 Python::attach(|py| match report {
168 Some(r) => r.into_py_any(py),
169 None => Ok(py.None()),
170 })
171 })
172 }
173
174 #[pyo3(name = "broadcast_batch_cancel")]
180 fn py_broadcast_batch_cancel<'py>(
181 &self,
182 py: Python<'py>,
183 instrument_id: InstrumentId,
184 client_order_ids: Option<Vec<ClientOrderId>>,
185 venue_order_ids: Option<Vec<VenueOrderId>>,
186 ) -> PyResult<Bound<'py, PyAny>> {
187 let broadcaster = self.clone_for_async();
188 pyo3_async_runtimes::tokio::future_into_py(py, async move {
189 let reports = broadcaster
190 .broadcast_batch_cancel(instrument_id, client_order_ids, venue_order_ids)
191 .await
192 .map_err(to_pyvalue_err)?;
193
194 Python::attach(|py| {
195 let py_reports: PyResult<Vec<_>> = reports
196 .into_iter()
197 .map(|report| report.into_py_any(py))
198 .collect();
199 let pylist = pyo3::types::PyList::new(py, py_reports?)?
200 .into_any()
201 .unbind();
202 Ok(pylist)
203 })
204 })
205 }
206
207 #[pyo3(name = "broadcast_cancel_all")]
213 fn py_broadcast_cancel_all<'py>(
214 &self,
215 py: Python<'py>,
216 instrument_id: InstrumentId,
217 order_side: Option<OrderSide>,
218 ) -> PyResult<Bound<'py, PyAny>> {
219 let broadcaster = self.clone_for_async();
220 pyo3_async_runtimes::tokio::future_into_py(py, async move {
221 let reports = broadcaster
222 .broadcast_cancel_all(instrument_id, order_side)
223 .await
224 .map_err(to_pyvalue_err)?;
225
226 Python::attach(|py| {
227 let py_reports: PyResult<Vec<_>> = reports
228 .into_iter()
229 .map(|report| report.into_py_any(py))
230 .collect();
231 let pylist = pyo3::types::PyList::new(py, py_reports?)?
232 .into_any()
233 .unbind();
234 Ok(pylist)
235 })
236 })
237 }
238
239 #[pyo3(name = "get_metrics")]
241 fn py_get_metrics(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
242 let metrics = self.get_metrics();
243 let dict = PyDict::new(py);
244 dict.set_item("total_cancels", metrics.total_cancels)?;
245 dict.set_item("successful_cancels", metrics.successful_cancels)?;
246 dict.set_item("failed_cancels", metrics.failed_cancels)?;
247 dict.set_item("expected_rejects", metrics.expected_rejects)?;
248 dict.set_item("idempotent_successes", metrics.idempotent_successes)?;
249 dict.set_item("healthy_clients", metrics.healthy_clients)?;
250 dict.set_item("total_clients", metrics.total_clients)?;
251 Ok(dict.into())
252 }
253
254 #[pyo3(name = "get_client_stats")]
256 fn py_get_client_stats(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
257 let stats = self.get_client_stats();
258 let list = pyo3::types::PyList::empty(py);
259 for stat in stats {
260 let dict = PyDict::new(py);
261 dict.set_item("client_id", stat.client_id.clone())?;
262 dict.set_item("healthy", stat.healthy)?;
263 dict.set_item("cancel_count", stat.cancel_count)?;
264 dict.set_item("error_count", stat.error_count)?;
265 list.append(dict)?;
266 }
267 Ok(list.into())
268 }
269}