1use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
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]
43 #[pyo3(signature = (
44 pool_size,
45 api_key=None,
46 api_secret=None,
47 base_url=None,
48 environment=BitmexEnvironment::Mainnet,
49 timeout_secs=60,
50 max_retries=3,
51 retry_delay_ms=1_000,
52 retry_delay_max_ms=5_000,
53 recv_window_ms=10_000,
54 max_requests_per_second=10,
55 max_requests_per_minute=120,
56 health_check_interval_secs=30,
57 health_check_timeout_secs=5,
58 expected_reject_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 proxy_urls: Option<Vec<Option<String>>>,
79 ) -> PyResult<Self> {
80 let config = SubmitBroadcasterConfig {
81 pool_size,
82 api_key: api_key.map(SecretString::from),
83 api_secret: api_secret.map(SecretString::from),
84 base_url,
85 environment,
86 timeout_secs,
87 max_retries,
88 retry_delay_ms,
89 retry_delay_max_ms,
90 recv_window_ms,
91 max_requests_per_second,
92 max_requests_per_minute,
93 health_check_interval_secs,
94 health_check_timeout_secs,
95 expected_reject_patterns: expected_reject_patterns
96 .unwrap_or_else(|| SubmitBroadcasterConfig::default().expected_reject_patterns),
97 proxy_urls: proxy_urls
98 .unwrap_or_default()
99 .into_iter()
100 .map(|value| value.map(SecretString::from))
101 .collect(),
102 };
103
104 Self::new(config).map_err(to_pyvalue_err)
105 }
106
107 #[pyo3(name = "start")]
113 fn py_start<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
114 let broadcaster = self.clone_for_async();
115 pyo3_async_runtimes::tokio::future_into_py(py, async move {
116 broadcaster.start().await.map_err(to_pyvalue_err)
117 })
118 }
119
120 #[pyo3(name = "stop")]
122 fn py_stop<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
123 let broadcaster = self.clone_for_async();
124 pyo3_async_runtimes::tokio::future_into_py(py, async move {
125 broadcaster.stop().await;
126 Ok(())
127 })
128 }
129
130 #[pyo3(name = "broadcast_submit")]
141 #[pyo3(signature = (
142 instrument_id,
143 client_order_id,
144 order_side,
145 order_type,
146 quantity,
147 time_in_force,
148 price=None,
149 trigger_price=None,
150 trigger_type=None,
151 trailing_offset=None,
152 trailing_offset_type=None,
153 display_qty=None,
154 post_only=false,
155 reduce_only=false,
156 order_list_id=None,
157 contingency_type=None,
158 submit_tries=None,
159 peg_price_type=None,
160 peg_offset_value=None
161 ))]
162 #[expect(clippy::too_many_arguments)]
163 fn py_broadcast_submit<'py>(
164 &self,
165 py: Python<'py>,
166 instrument_id: InstrumentId,
167 client_order_id: ClientOrderId,
168 order_side: OrderSide,
169 order_type: OrderType,
170 quantity: Quantity,
171 time_in_force: TimeInForce,
172 price: Option<Price>,
173 trigger_price: Option<Price>,
174 trigger_type: Option<TriggerType>,
175 trailing_offset: Option<f64>,
176 trailing_offset_type: Option<TrailingOffsetType>,
177 display_qty: Option<Quantity>,
178 post_only: bool,
179 reduce_only: bool,
180 order_list_id: Option<OrderListId>,
181 contingency_type: Option<ContingencyType>,
182 submit_tries: Option<usize>,
183 peg_price_type: Option<String>,
184 peg_offset_value: Option<f64>,
185 ) -> PyResult<Bound<'py, PyAny>> {
186 let broadcaster = self.clone_for_async();
187
188 let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
189 .map(|s| {
190 s.parse::<BitmexPegPriceType>()
191 .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
192 })
193 .transpose()?;
194
195 pyo3_async_runtimes::tokio::future_into_py(py, async move {
196 let report = broadcaster
197 .broadcast_submit(
198 instrument_id,
199 client_order_id,
200 order_side,
201 order_type,
202 quantity,
203 time_in_force,
204 price,
205 trigger_price,
206 trigger_type,
207 trailing_offset,
208 trailing_offset_type,
209 display_qty,
210 post_only,
211 reduce_only,
212 order_list_id,
213 contingency_type,
214 submit_tries,
215 peg_price_type,
216 peg_offset_value,
217 )
218 .await
219 .map_err(to_pyvalue_err)?;
220
221 Python::attach(|py| report.into_py_any(py))
222 })
223 }
224
225 #[pyo3(name = "get_metrics")]
227 fn py_get_metrics(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
228 let metrics = self.get_metrics();
229 let dict = PyDict::new(py);
230 dict.set_item("total_submits", metrics.total_submits)?;
231 dict.set_item("successful_submits", metrics.successful_submits)?;
232 dict.set_item("failed_submits", metrics.failed_submits)?;
233 dict.set_item("expected_rejects", metrics.expected_rejects)?;
234 dict.set_item("healthy_clients", metrics.healthy_clients)?;
235 dict.set_item("total_clients", metrics.total_clients)?;
236 Ok(dict.into())
237 }
238
239 #[pyo3(name = "get_client_stats")]
241 fn py_get_client_stats(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
242 let stats = self.get_client_stats();
243 let list = pyo3::types::PyList::empty(py);
244 for stat in stats {
245 let dict = PyDict::new(py);
246 dict.set_item("client_id", stat.client_id.clone())?;
247 dict.set_item("healthy", stat.healthy)?;
248 dict.set_item("submit_count", stat.submit_count)?;
249 dict.set_item("error_count", stat.error_count)?;
250 list.append(dict)?;
251 }
252 Ok(list.into())
253 }
254
255 #[pyo3(name = "cache_instrument")]
257 fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
258 let inst_any = pyobject_to_instrument_any(py, instrument)?;
259 self.cache_instrument(&inst_any);
260 Ok(())
261 }
262}