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