1#![allow(clippy::missing_errors_doc)]
19
20use std::{str::FromStr, sync::Arc};
21
22use chrono::Utc;
23use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
24use nautilus_model::{
25 enums::{OrderSide, TimeInForce},
26 identifiers::InstrumentId,
27 types::{Price, Quantity},
28};
29use pyo3::prelude::*;
30
31use super::grpc::PyDydxGrpcClient;
32use crate::{
33 execution::{block_time::BlockTimeMonitor, submitter::OrderSubmitter},
34 grpc::{DEFAULT_RUST_CLIENT_METADATA, types::ChainId},
35 http::client::DydxHttpClient,
36};
37
38#[pyclass(name = "DydxOrderSubmitter")]
56#[derive(Debug)]
57pub struct PyDydxOrderSubmitter {
58 pub(crate) inner: Arc<OrderSubmitter>,
59 block_time_monitor: Arc<BlockTimeMonitor>,
61}
62
63#[pymethods]
64impl PyDydxOrderSubmitter {
65 #[new]
80 #[pyo3(signature = (
81 grpc_client,
82 http_client,
83 private_key,
84 wallet_address,
85 subaccount_number=0,
86 chain_id=None
87 ))]
88 pub fn py_new(
89 grpc_client: PyDydxGrpcClient,
90 http_client: DydxHttpClient,
91 private_key: &str,
92 wallet_address: String,
93 subaccount_number: u32,
94 chain_id: Option<&str>,
95 ) -> PyResult<Self> {
96 let chain_id = if let Some(chain_str) = chain_id {
97 ChainId::from_str(chain_str).map_err(to_pyvalue_err)?
98 } else {
99 ChainId::Mainnet1
100 };
101
102 let block_time_monitor = Arc::new(BlockTimeMonitor::new());
104
105 let submitter = OrderSubmitter::new(
106 grpc_client.inner.as_ref().clone(),
107 http_client,
108 private_key,
109 wallet_address,
110 subaccount_number,
111 chain_id,
112 Arc::clone(&block_time_monitor),
113 )
114 .map_err(to_pyvalue_err)?;
115
116 Ok(Self {
117 inner: Arc::new(submitter),
118 block_time_monitor,
119 })
120 }
121
122 #[pyo3(name = "record_block")]
131 fn py_record_block(&self, height: u64, timestamp: Option<&str>) -> PyResult<()> {
132 let time = if let Some(ts) = timestamp {
133 chrono::DateTime::parse_from_rfc3339(ts)
134 .map(|dt| dt.with_timezone(&Utc))
135 .map_err(|e| to_pyvalue_err(format!("Invalid timestamp: {e}")))?
136 } else {
137 Utc::now()
138 };
139 self.block_time_monitor.record_block(height, time);
140 Ok(())
141 }
142
143 #[pyo3(name = "set_block_height")]
148 fn py_set_block_height(&self, height: u64) {
149 self.block_time_monitor.record_block(height, Utc::now());
150 }
151
152 #[pyo3(name = "get_block_height")]
154 fn py_get_block_height(&self) -> u64 {
155 self.block_time_monitor.current_block_height()
156 }
157
158 #[pyo3(name = "estimated_seconds_per_block")]
162 fn py_estimated_seconds_per_block(&self) -> Option<f64> {
163 self.block_time_monitor.estimated_seconds_per_block()
164 }
165
166 #[pyo3(name = "is_block_time_ready")]
168 fn py_is_block_time_ready(&self) -> bool {
169 self.block_time_monitor.is_ready()
170 }
171
172 #[pyo3(name = "wallet_address")]
174 fn py_wallet_address(&self) -> String {
175 self.inner.wallet_address().to_string()
176 }
177
178 #[pyo3(name = "resolve_authenticators")]
191 fn py_resolve_authenticators<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
192 let submitter = self.inner.clone();
193 pyo3_async_runtimes::tokio::future_into_py(py, async move {
194 submitter
195 .tx_manager()
196 .resolve_authenticators()
197 .await
198 .map_err(to_pyruntime_err)
199 })
200 }
201
202 #[pyo3(name = "submit_market_order")]
206 #[pyo3(signature = (instrument_id, client_order_id, side, quantity, client_metadata=None))]
207 fn py_submit_market_order<'py>(
208 &self,
209 py: Python<'py>,
210 instrument_id: &str,
211 client_order_id: u32,
212 side: i64,
213 quantity: &str,
214 client_metadata: Option<u32>,
215 ) -> PyResult<Bound<'py, PyAny>> {
216 let submitter = self.inner.clone();
217 let instrument_id = InstrumentId::from(instrument_id);
218 let side = OrderSide::from_repr(side as usize)
219 .ok_or_else(|| to_pyvalue_err("Invalid OrderSide"))?;
220 let quantity = Quantity::from(quantity);
221 let client_metadata = client_metadata.unwrap_or(DEFAULT_RUST_CLIENT_METADATA);
222
223 pyo3_async_runtimes::tokio::future_into_py(py, async move {
224 let tx_hash = submitter
225 .submit_market_order(
226 instrument_id,
227 client_order_id,
228 client_metadata,
229 side,
230 quantity,
231 )
232 .await
233 .map_err(to_pyruntime_err)?;
234 Ok(tx_hash)
235 })
236 }
237
238 #[pyo3(name = "submit_limit_order")]
242 #[pyo3(signature = (instrument_id, client_order_id, side, price, quantity, time_in_force, post_only, reduce_only, expire_time=None, client_metadata=None))]
243 #[allow(clippy::too_many_arguments)]
244 fn py_submit_limit_order<'py>(
245 &self,
246 py: Python<'py>,
247 instrument_id: &str,
248 client_order_id: u32,
249 side: i64,
250 price: &str,
251 quantity: &str,
252 time_in_force: i64,
253 post_only: bool,
254 reduce_only: bool,
255 expire_time: Option<i64>,
256 client_metadata: Option<u32>,
257 ) -> PyResult<Bound<'py, PyAny>> {
258 let submitter = self.inner.clone();
259 let instrument_id = InstrumentId::from(instrument_id);
260 let side = OrderSide::from_repr(side as usize)
261 .ok_or_else(|| to_pyvalue_err("Invalid OrderSide"))?;
262 let price = Price::from(price);
263 let quantity = Quantity::from(quantity);
264 let time_in_force = TimeInForce::from_repr(time_in_force as usize)
265 .ok_or_else(|| to_pyvalue_err("Invalid TimeInForce"))?;
266 let client_metadata = client_metadata.unwrap_or(DEFAULT_RUST_CLIENT_METADATA);
267
268 pyo3_async_runtimes::tokio::future_into_py(py, async move {
269 let tx_hash = submitter
270 .submit_limit_order(
271 instrument_id,
272 client_order_id,
273 client_metadata,
274 side,
275 price,
276 quantity,
277 time_in_force,
278 post_only,
279 reduce_only,
280 expire_time,
281 )
282 .await
283 .map_err(to_pyruntime_err)?;
284 Ok(tx_hash)
285 })
286 }
287
288 #[pyo3(name = "submit_stop_market_order")]
290 #[pyo3(signature = (instrument_id, client_order_id, side, trigger_price, quantity, reduce_only, expire_time=None, client_metadata=None))]
291 #[allow(clippy::too_many_arguments)]
292 fn py_submit_stop_market_order<'py>(
293 &self,
294 py: Python<'py>,
295 instrument_id: &str,
296 client_order_id: u32,
297 side: i64,
298 trigger_price: &str,
299 quantity: &str,
300 reduce_only: bool,
301 expire_time: Option<i64>,
302 client_metadata: Option<u32>,
303 ) -> PyResult<Bound<'py, PyAny>> {
304 let submitter = self.inner.clone();
305 let instrument_id = InstrumentId::from(instrument_id);
306 let side = OrderSide::from_repr(side as usize)
307 .ok_or_else(|| to_pyvalue_err("Invalid OrderSide"))?;
308 let trigger_price = Price::from(trigger_price);
309 let quantity = Quantity::from(quantity);
310 let client_metadata = client_metadata.unwrap_or(DEFAULT_RUST_CLIENT_METADATA);
311
312 pyo3_async_runtimes::tokio::future_into_py(py, async move {
313 let tx_hash = submitter
314 .submit_stop_market_order(
315 instrument_id,
316 client_order_id,
317 client_metadata,
318 side,
319 trigger_price,
320 quantity,
321 reduce_only,
322 expire_time,
323 )
324 .await
325 .map_err(to_pyruntime_err)?;
326 Ok(tx_hash)
327 })
328 }
329
330 #[pyo3(name = "submit_stop_limit_order")]
332 #[pyo3(signature = (instrument_id, client_order_id, side, trigger_price, limit_price, quantity, time_in_force, post_only, reduce_only, expire_time=None, client_metadata=None))]
333 #[allow(clippy::too_many_arguments)]
334 fn py_submit_stop_limit_order<'py>(
335 &self,
336 py: Python<'py>,
337 instrument_id: &str,
338 client_order_id: u32,
339 side: i64,
340 trigger_price: &str,
341 limit_price: &str,
342 quantity: &str,
343 time_in_force: i64,
344 post_only: bool,
345 reduce_only: bool,
346 expire_time: Option<i64>,
347 client_metadata: Option<u32>,
348 ) -> PyResult<Bound<'py, PyAny>> {
349 let submitter = self.inner.clone();
350 let instrument_id = InstrumentId::from(instrument_id);
351 let side = OrderSide::from_repr(side as usize)
352 .ok_or_else(|| to_pyvalue_err("Invalid OrderSide"))?;
353 let trigger_price = Price::from(trigger_price);
354 let limit_price = Price::from(limit_price);
355 let quantity = Quantity::from(quantity);
356 let time_in_force = TimeInForce::from_repr(time_in_force as usize)
357 .ok_or_else(|| to_pyvalue_err("Invalid TimeInForce"))?;
358 let client_metadata = client_metadata.unwrap_or(DEFAULT_RUST_CLIENT_METADATA);
359
360 pyo3_async_runtimes::tokio::future_into_py(py, async move {
361 let tx_hash = submitter
362 .submit_stop_limit_order(
363 instrument_id,
364 client_order_id,
365 client_metadata,
366 side,
367 trigger_price,
368 limit_price,
369 quantity,
370 time_in_force,
371 post_only,
372 reduce_only,
373 expire_time,
374 )
375 .await
376 .map_err(to_pyruntime_err)?;
377 Ok(tx_hash)
378 })
379 }
380
381 #[pyo3(name = "submit_take_profit_market_order")]
383 #[pyo3(signature = (instrument_id, client_order_id, side, trigger_price, quantity, reduce_only, expire_time=None, client_metadata=None))]
384 #[allow(clippy::too_many_arguments)]
385 fn py_submit_take_profit_market_order<'py>(
386 &self,
387 py: Python<'py>,
388 instrument_id: &str,
389 client_order_id: u32,
390 side: i64,
391 trigger_price: &str,
392 quantity: &str,
393 reduce_only: bool,
394 expire_time: Option<i64>,
395 client_metadata: Option<u32>,
396 ) -> PyResult<Bound<'py, PyAny>> {
397 let submitter = self.inner.clone();
398 let instrument_id = InstrumentId::from(instrument_id);
399 let side = OrderSide::from_repr(side as usize)
400 .ok_or_else(|| to_pyvalue_err("Invalid OrderSide"))?;
401 let trigger_price = Price::from(trigger_price);
402 let quantity = Quantity::from(quantity);
403 let client_metadata = client_metadata.unwrap_or(DEFAULT_RUST_CLIENT_METADATA);
404
405 pyo3_async_runtimes::tokio::future_into_py(py, async move {
406 let tx_hash = submitter
407 .submit_take_profit_market_order(
408 instrument_id,
409 client_order_id,
410 client_metadata,
411 side,
412 trigger_price,
413 quantity,
414 reduce_only,
415 expire_time,
416 )
417 .await
418 .map_err(to_pyruntime_err)?;
419 Ok(tx_hash)
420 })
421 }
422
423 #[pyo3(name = "submit_take_profit_limit_order")]
425 #[pyo3(signature = (instrument_id, client_order_id, side, trigger_price, limit_price, quantity, time_in_force, post_only, reduce_only, expire_time=None, client_metadata=None))]
426 #[allow(clippy::too_many_arguments)]
427 fn py_submit_take_profit_limit_order<'py>(
428 &self,
429 py: Python<'py>,
430 instrument_id: &str,
431 client_order_id: u32,
432 side: i64,
433 trigger_price: &str,
434 limit_price: &str,
435 quantity: &str,
436 time_in_force: i64,
437 post_only: bool,
438 reduce_only: bool,
439 expire_time: Option<i64>,
440 client_metadata: Option<u32>,
441 ) -> PyResult<Bound<'py, PyAny>> {
442 let submitter = self.inner.clone();
443 let instrument_id = InstrumentId::from(instrument_id);
444 let side = OrderSide::from_repr(side as usize)
445 .ok_or_else(|| to_pyvalue_err("Invalid OrderSide"))?;
446 let trigger_price = Price::from(trigger_price);
447 let limit_price = Price::from(limit_price);
448 let quantity = Quantity::from(quantity);
449 let time_in_force = TimeInForce::from_repr(time_in_force as usize)
450 .ok_or_else(|| to_pyvalue_err("Invalid TimeInForce"))?;
451 let client_metadata = client_metadata.unwrap_or(DEFAULT_RUST_CLIENT_METADATA);
452
453 pyo3_async_runtimes::tokio::future_into_py(py, async move {
454 let tx_hash = submitter
455 .submit_take_profit_limit_order(
456 instrument_id,
457 client_order_id,
458 client_metadata,
459 side,
460 trigger_price,
461 limit_price,
462 quantity,
463 time_in_force,
464 post_only,
465 reduce_only,
466 expire_time,
467 )
468 .await
469 .map_err(to_pyruntime_err)?;
470 Ok(tx_hash)
471 })
472 }
473
474 #[pyo3(name = "cancel_order")]
478 #[pyo3(signature = (instrument_id, client_order_id, time_in_force=None, expire_time_ns=None))]
479 fn py_cancel_order<'py>(
480 &self,
481 py: Python<'py>,
482 instrument_id: &str,
483 client_order_id: u32,
484 time_in_force: Option<i64>,
485 expire_time_ns: Option<u64>,
486 ) -> PyResult<Bound<'py, PyAny>> {
487 let submitter = self.inner.clone();
488 let instrument_id = InstrumentId::from(instrument_id);
489 let time_in_force = time_in_force
490 .and_then(|tif| TimeInForce::from_repr(tif as usize))
491 .unwrap_or(TimeInForce::Gtc);
492 let expire_time_ns = expire_time_ns.map(nautilus_core::UnixNanos::from);
493
494 pyo3_async_runtimes::tokio::future_into_py(py, async move {
495 let tx_hash = submitter
496 .cancel_order(
497 instrument_id,
498 client_order_id,
499 time_in_force,
500 expire_time_ns,
501 )
502 .await
503 .map_err(to_pyruntime_err)?;
504 Ok(tx_hash)
505 })
506 }
507
508 #[pyo3(name = "cancel_orders_batch")]
513 fn py_cancel_orders_batch<'py>(
514 &self,
515 py: Python<'py>,
516 orders: Vec<(String, u32, Option<i64>, Option<u64>)>,
517 ) -> PyResult<Bound<'py, PyAny>> {
518 let submitter = self.inner.clone();
519 let orders: Vec<(
520 InstrumentId,
521 u32,
522 TimeInForce,
523 Option<nautilus_core::UnixNanos>,
524 )> = orders
525 .into_iter()
526 .map(|(id, client_id, tif, expire_ns)| {
527 let tif = tif
528 .and_then(|t| TimeInForce::from_repr(t as usize))
529 .unwrap_or(TimeInForce::Gtc);
530 let expire_ns = expire_ns.map(nautilus_core::UnixNanos::from);
531 (InstrumentId::from(id), client_id, tif, expire_ns)
532 })
533 .collect();
534
535 pyo3_async_runtimes::tokio::future_into_py(py, async move {
536 let tx_hash = submitter
537 .cancel_orders_batch(&orders)
538 .await
539 .map_err(to_pyruntime_err)?;
540 Ok(tx_hash)
541 })
542 }
543
544 fn __repr__(&self) -> String {
545 format!(
546 "DydxOrderSubmitter(address={}, block_height={})",
547 self.inner.wallet_address(),
548 self.block_time_monitor.current_block_height()
549 )
550 }
551}