1use nautilus_core::python::to_pyvalue_err;
19use nautilus_model::{
20 enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
21 identifiers::{ClientOrderId, InstrumentId, OrderListId},
22 python::instruments::pyobject_to_instrument_any,
23 types::{Price, Quantity},
24};
25use pyo3::{conversion::IntoPyObjectExt, prelude::*, types::PyDict};
26
27use crate::{
28 broadcast::submitter::{SubmitBroadcaster, SubmitBroadcasterConfig},
29 common::enums::{BitmexEnvironment, BitmexPegPriceType},
30};
31
32#[pymethods]
33#[pyo3_stub_gen::derive::gen_stub_pymethods]
34impl SubmitBroadcaster {
35 #[new]
41 #[pyo3(signature = (
42 pool_size,
43 api_key=None,
44 api_secret=None,
45 base_url=None,
46 environment=BitmexEnvironment::Mainnet,
47 timeout_secs=60,
48 max_retries=3,
49 retry_delay_ms=1_000,
50 retry_delay_max_ms=5_000,
51 recv_window_ms=10_000,
52 max_requests_per_second=10,
53 max_requests_per_minute=120,
54 health_check_interval_secs=30,
55 health_check_timeout_secs=5,
56 expected_reject_patterns=None,
57 proxy_urls=None,
58 ))]
59 #[expect(clippy::too_many_arguments)]
60 fn py_new(
61 pool_size: usize,
62 api_key: Option<String>,
63 api_secret: Option<String>,
64 base_url: Option<String>,
65 environment: BitmexEnvironment,
66 timeout_secs: u64,
67 max_retries: u32,
68 retry_delay_ms: u64,
69 retry_delay_max_ms: u64,
70 recv_window_ms: u64,
71 max_requests_per_second: u32,
72 max_requests_per_minute: u32,
73 health_check_interval_secs: u64,
74 health_check_timeout_secs: u64,
75 expected_reject_patterns: Option<Vec<String>>,
76 proxy_urls: Option<Vec<Option<String>>>,
77 ) -> PyResult<Self> {
78 let config = SubmitBroadcasterConfig {
79 pool_size,
80 api_key,
81 api_secret,
82 base_url,
83 environment,
84 timeout_secs,
85 max_retries,
86 retry_delay_ms,
87 retry_delay_max_ms,
88 recv_window_ms,
89 max_requests_per_second,
90 max_requests_per_minute,
91 health_check_interval_secs,
92 health_check_timeout_secs,
93 expected_reject_patterns: expected_reject_patterns
94 .unwrap_or_else(|| SubmitBroadcasterConfig::default().expected_reject_patterns),
95 proxy_urls: proxy_urls.unwrap_or_default(),
96 };
97
98 Self::new(config).map_err(to_pyvalue_err)
99 }
100
101 #[pyo3(name = "start")]
107 fn py_start<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
108 let broadcaster = self.clone_for_async();
109 pyo3_async_runtimes::tokio::future_into_py(py, async move {
110 broadcaster.start().await.map_err(to_pyvalue_err)
111 })
112 }
113
114 #[pyo3(name = "stop")]
116 fn py_stop<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
117 let broadcaster = self.clone_for_async();
118 pyo3_async_runtimes::tokio::future_into_py(py, async move {
119 broadcaster.stop().await;
120 Ok(())
121 })
122 }
123
124 #[pyo3(name = "broadcast_submit")]
135 #[pyo3(signature = (
136 instrument_id,
137 client_order_id,
138 order_side,
139 order_type,
140 quantity,
141 time_in_force,
142 price=None,
143 trigger_price=None,
144 trigger_type=None,
145 trailing_offset=None,
146 trailing_offset_type=None,
147 display_qty=None,
148 post_only=false,
149 reduce_only=false,
150 order_list_id=None,
151 contingency_type=None,
152 submit_tries=None,
153 peg_price_type=None,
154 peg_offset_value=None
155 ))]
156 #[expect(clippy::too_many_arguments)]
157 fn py_broadcast_submit<'py>(
158 &self,
159 py: Python<'py>,
160 instrument_id: InstrumentId,
161 client_order_id: ClientOrderId,
162 order_side: OrderSide,
163 order_type: OrderType,
164 quantity: Quantity,
165 time_in_force: TimeInForce,
166 price: Option<Price>,
167 trigger_price: Option<Price>,
168 trigger_type: Option<TriggerType>,
169 trailing_offset: Option<f64>,
170 trailing_offset_type: Option<TrailingOffsetType>,
171 display_qty: Option<Quantity>,
172 post_only: bool,
173 reduce_only: bool,
174 order_list_id: Option<OrderListId>,
175 contingency_type: Option<ContingencyType>,
176 submit_tries: Option<usize>,
177 peg_price_type: Option<String>,
178 peg_offset_value: Option<f64>,
179 ) -> PyResult<Bound<'py, PyAny>> {
180 let broadcaster = self.clone_for_async();
181
182 let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
183 .map(|s| {
184 s.parse::<BitmexPegPriceType>()
185 .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
186 })
187 .transpose()?;
188
189 pyo3_async_runtimes::tokio::future_into_py(py, async move {
190 let report = broadcaster
191 .broadcast_submit(
192 instrument_id,
193 client_order_id,
194 order_side,
195 order_type,
196 quantity,
197 time_in_force,
198 price,
199 trigger_price,
200 trigger_type,
201 trailing_offset,
202 trailing_offset_type,
203 display_qty,
204 post_only,
205 reduce_only,
206 order_list_id,
207 contingency_type,
208 submit_tries,
209 peg_price_type,
210 peg_offset_value,
211 )
212 .await
213 .map_err(to_pyvalue_err)?;
214
215 Python::attach(|py| report.into_py_any(py))
216 })
217 }
218
219 #[pyo3(name = "get_metrics")]
221 fn py_get_metrics(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
222 let metrics = self.get_metrics();
223 let dict = PyDict::new(py);
224 dict.set_item("total_submits", metrics.total_submits)?;
225 dict.set_item("successful_submits", metrics.successful_submits)?;
226 dict.set_item("failed_submits", metrics.failed_submits)?;
227 dict.set_item("expected_rejects", metrics.expected_rejects)?;
228 dict.set_item("healthy_clients", metrics.healthy_clients)?;
229 dict.set_item("total_clients", metrics.total_clients)?;
230 Ok(dict.into())
231 }
232
233 #[pyo3(name = "get_client_stats")]
235 fn py_get_client_stats(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
236 let stats = self.get_client_stats();
237 let list = pyo3::types::PyList::empty(py);
238 for stat in stats {
239 let dict = PyDict::new(py);
240 dict.set_item("client_id", stat.client_id.clone())?;
241 dict.set_item("healthy", stat.healthy)?;
242 dict.set_item("submit_count", stat.submit_count)?;
243 dict.set_item("error_count", stat.error_count)?;
244 list.append(dict)?;
245 }
246 Ok(list.into())
247 }
248
249 #[pyo3(name = "cache_instrument")]
251 fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
252 let inst_any = pyobject_to_instrument_any(py, instrument)?;
253 self.cache_instrument(&inst_any);
254 Ok(())
255 }
256}