nautilus_dydx/execution/submitter.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use nautilus_model::{
17 enums::{OrderSide, TimeInForce},
18 instruments::Instrument,
19 types::{Price, Quantity},
20};
21
22use crate::{
23 error::DydxError,
24 grpc::{DydxGrpcClient, OrderMarketParams, Wallet},
25 // TODO: Enable when proto is generated
26 // proto::dydxprotocol::clob::order::{
27 // Side as ProtoOrderSide, TimeInForce as ProtoTimeInForce,
28 // },
29};
30
31// Temporary placeholder types until proto is generated
32#[derive(Debug, Clone, Copy)]
33pub enum ProtoOrderSide {
34 Buy,
35 Sell,
36}
37
38#[derive(Debug, Clone, Copy)]
39pub enum ProtoTimeInForce {
40 Unspecified,
41 Ioc,
42 FillOrKill,
43}
44
45#[derive(Debug)]
46pub struct OrderSubmitter {
47 #[allow(dead_code)]
48 grpc_client: DydxGrpcClient,
49 #[allow(dead_code)]
50 wallet_address: String,
51 #[allow(dead_code)]
52 subaccount_number: u32,
53}
54
55impl OrderSubmitter {
56 pub fn new(
57 grpc_client: DydxGrpcClient,
58 wallet_address: String,
59 subaccount_number: u32,
60 ) -> Self {
61 Self {
62 grpc_client,
63 wallet_address,
64 subaccount_number,
65 }
66 }
67
68 /// Submits a market order to dYdX via gRPC.
69 ///
70 /// Market orders execute immediately at the best available price.
71 ///
72 /// # Errors
73 ///
74 /// Returns `DydxError` if gRPC submission fails.
75 pub async fn submit_market_order(
76 &self,
77 _wallet: &Wallet,
78 client_order_id: u32,
79 side: OrderSide,
80 quantity: Quantity,
81 _block_height: u32,
82 ) -> Result<(), DydxError> {
83 // TODO: Implement when proto is generated
84 tracing::info!(
85 "[STUB] Submitting market order: client_id={}, side={:?}, quantity={}",
86 client_order_id,
87 side,
88 quantity
89 );
90 Ok(())
91 }
92
93 /// Submits a limit order to dYdX via gRPC.
94 ///
95 /// Limit orders execute only at the specified price or better.
96 ///
97 /// # Errors
98 ///
99 /// Returns `DydxError` if gRPC submission fails.
100 #[allow(clippy::too_many_arguments)]
101 pub async fn submit_limit_order(
102 &self,
103 _wallet: &Wallet,
104 client_order_id: u32,
105 side: OrderSide,
106 price: Price,
107 quantity: Quantity,
108 time_in_force: TimeInForce,
109 post_only: bool,
110 reduce_only: bool,
111 _block_height: u32,
112 _expire_time: Option<i64>,
113 ) -> Result<(), DydxError> {
114 // TODO: Implement when proto is generated
115 tracing::info!(
116 "[STUB] Submitting limit order: client_id={}, side={:?}, price={}, quantity={}, tif={:?}, post_only={}, reduce_only={}",
117 client_order_id,
118 side,
119 price,
120 quantity,
121 time_in_force,
122 post_only,
123 reduce_only
124 );
125 Ok(())
126 }
127
128 /// Cancels an order on dYdX via gRPC.
129 ///
130 /// # Errors
131 ///
132 /// Returns `DydxError` if gRPC cancellation fails.
133 pub async fn cancel_order(
134 &self,
135 _wallet: &Wallet,
136 client_order_id: u32,
137 _block_height: u32,
138 ) -> Result<(), DydxError> {
139 // TODO: Implement when proto is generated
140 tracing::info!("[STUB] Cancelling order: client_id={}", client_order_id);
141 Ok(())
142 }
143
144 /// Cancels multiple orders via individual gRPC transactions.
145 ///
146 /// dYdX v4 requires separate blockchain transactions for each cancellation.
147 ///
148 /// # Errors
149 ///
150 /// Returns `DydxError` if any gRPC cancellation fails.
151 pub async fn cancel_orders_batch(
152 &self,
153 _wallet: &Wallet,
154 client_order_ids: &[u32],
155 _block_height: u32,
156 ) -> Result<(), DydxError> {
157 // TODO: Implement when proto is generated
158 // Note: Each order requires a separate gRPC transaction
159 tracing::info!(
160 "[STUB] Batch cancelling {} orders: ids={:?}",
161 client_order_ids.len(),
162 client_order_ids
163 );
164 Ok(())
165 }
166
167 /// Submits a stop market order to dYdX via gRPC.
168 ///
169 /// # Errors
170 ///
171 /// Returns `DydxError::NotImplemented` until conditional order support is added.
172 #[allow(clippy::too_many_arguments)]
173 pub async fn submit_stop_market_order(
174 &self,
175 _wallet: &Wallet,
176 _client_order_id: u32,
177 _side: OrderSide,
178 _trigger_price: Price,
179 _quantity: Quantity,
180 _reduce_only: bool,
181 _block_height: u32,
182 _expire_time: Option<i64>,
183 ) -> Result<(), DydxError> {
184 Err(DydxError::NotImplemented(
185 "Stop market orders not yet implemented - awaiting proto generation".to_string(),
186 ))
187 }
188
189 /// Submits a stop limit order to dYdX via gRPC.
190 ///
191 /// # Errors
192 ///
193 /// Returns `DydxError::NotImplemented` until conditional order support is added.
194 #[allow(clippy::too_many_arguments)]
195 pub async fn submit_stop_limit_order(
196 &self,
197 _wallet: &Wallet,
198 _client_order_id: u32,
199 _side: OrderSide,
200 _trigger_price: Price,
201 _limit_price: Price,
202 _quantity: Quantity,
203 _time_in_force: TimeInForce,
204 _post_only: bool,
205 _reduce_only: bool,
206 _block_height: u32,
207 _expire_time: Option<i64>,
208 ) -> Result<(), DydxError> {
209 Err(DydxError::NotImplemented(
210 "Stop limit orders not yet implemented - awaiting proto generation".to_string(),
211 ))
212 }
213
214 /// Submits a take profit market order to dYdX via gRPC.
215 ///
216 /// # Errors
217 ///
218 /// Returns `DydxError::NotImplemented` until conditional order support is added.
219 #[allow(clippy::too_many_arguments)]
220 pub async fn submit_take_profit_market_order(
221 &self,
222 _wallet: &Wallet,
223 _client_order_id: u32,
224 _side: OrderSide,
225 _trigger_price: Price,
226 _quantity: Quantity,
227 _reduce_only: bool,
228 _block_height: u32,
229 _expire_time: Option<i64>,
230 ) -> Result<(), DydxError> {
231 Err(DydxError::NotImplemented(
232 "Take profit market orders not yet implemented - awaiting proto generation".to_string(),
233 ))
234 }
235
236 /// Submits a take profit limit order to dYdX via gRPC.
237 ///
238 /// # Errors
239 ///
240 /// Returns `DydxError::NotImplemented` until conditional order support is added.
241 #[allow(clippy::too_many_arguments)]
242 pub async fn submit_take_profit_limit_order(
243 &self,
244 _wallet: &Wallet,
245 _client_order_id: u32,
246 _side: OrderSide,
247 _trigger_price: Price,
248 _limit_price: Price,
249 _quantity: Quantity,
250 _time_in_force: TimeInForce,
251 _post_only: bool,
252 _reduce_only: bool,
253 _block_height: u32,
254 _expire_time: Option<i64>,
255 ) -> Result<(), DydxError> {
256 Err(DydxError::NotImplemented(
257 "Take profit limit orders not yet implemented - awaiting proto generation".to_string(),
258 ))
259 }
260
261 /// Submits a trailing stop order to dYdX via gRPC.
262 ///
263 /// # Errors
264 ///
265 /// Returns `DydxError::NotImplemented` - trailing stops not yet supported by dYdX v4 protocol.
266 #[allow(clippy::too_many_arguments)]
267 pub async fn submit_trailing_stop_order(
268 &self,
269 _wallet: &Wallet,
270 _client_order_id: u32,
271 _side: OrderSide,
272 _trailing_offset: Price,
273 _quantity: Quantity,
274 _reduce_only: bool,
275 _block_height: u32,
276 _expire_time: Option<i64>,
277 ) -> Result<(), DydxError> {
278 Err(DydxError::NotImplemented(
279 "Trailing stop orders not yet supported by dYdX v4 protocol".to_string(),
280 ))
281 }
282
283 #[allow(dead_code)]
284 fn extract_market_params(
285 &self,
286 instrument: &dyn Instrument,
287 ) -> Result<OrderMarketParams, DydxError> {
288 // NOTE:
289 // dYdX-specific quantization parameters (atomic_resolution, quantum_conversion_exponent,
290 // step_base_quantums, subticks_per_tick and clob_pair_id) ultimately come from the
291 // PerpetualMarket metadata exposed by the Indexer API.
292 //
293 // The full wiring from HTTP market metadata → instrument → gRPC order builder is not yet
294 // implemented in Rust. Until proto files are generated and that plumbing is in place, we
295 // derive a best-effort set of parameters from the instrument itself so that:
296 // - Values are at least instrument-specific (not hard-coded placeholders).
297 // - Future work can replace this logic with exact dYdX metadata without changing callers.
298 //
299 // Mapping strategy (stub until proto):
300 // - atomic_resolution: negative of the instrument size precision.
301 // - quantum_conversion_exponent: negative of the instrument price precision.
302 // - step_base_quantums / subticks_per_tick: minimal non-zero values (1) so that
303 // quantization code has valid, non-zero divisors without assuming dYdX-specific scales.
304 //
305 // clob_pair_id and oracle_price still require venue metadata and remain stubbed.
306 let size_precision = instrument.size_precision() as i32;
307 let price_precision = instrument.price_precision() as i32;
308
309 let atomic_resolution = -size_precision;
310 let quantum_conversion_exponent = -price_precision;
311
312 Ok(OrderMarketParams {
313 atomic_resolution,
314 clob_pair_id: 0, // Will be set from instrument metadata
315 oracle_price: None,
316 quantum_conversion_exponent,
317 step_base_quantums: 1,
318 subticks_per_tick: 1,
319 })
320 }
321
322 /// Handles the exchange response from order submission.
323 #[allow(dead_code)]
324 fn handle_exchange_response(&self, _response: &[u8]) -> Result<String, DydxError> {
325 // TODO: Parse proto response when available
326 Ok("stubbed_tx_hash".to_string())
327 }
328
329 /// Parses exchange order ID from response.
330 #[allow(dead_code)]
331 fn parse_venue_order_id(&self, _response: &[u8]) -> Result<String, DydxError> {
332 // TODO: Extract venue order ID from proto response
333 Ok("stubbed_venue_id".to_string())
334 }
335
336 /// Stores ClientOrderId to VenueOrderId mapping.
337 #[allow(dead_code)]
338 fn store_order_id_mapping(&self, _client_id: u32, _venue_id: &str) -> Result<(), DydxError> {
339 // TODO: Store in cache/database
340 tracing::debug!("[STUB] Would store order ID mapping");
341 Ok(())
342 }
343
344 /// Retrieves VenueOrderId from ClientOrderId.
345 #[allow(dead_code)]
346 fn get_venue_order_id(&self, _client_id: u32) -> Result<Option<String>, DydxError> {
347 // TODO: Retrieve from cache/database
348 Ok(None)
349 }
350
351 /// Generates OrderAccepted event from exchange response.
352 #[allow(dead_code)]
353 fn generate_order_accepted(&self, _client_id: u32, _venue_id: &str) -> Result<(), DydxError> {
354 // TODO: Generate and send OrderAccepted event
355 tracing::debug!("[STUB] Would generate OrderAccepted event");
356 Ok(())
357 }
358
359 /// Generates OrderRejected event from exchange error.
360 #[allow(dead_code)]
361 fn generate_order_rejected(&self, _client_id: u32, _reason: &str) -> Result<(), DydxError> {
362 // TODO: Generate and send OrderRejected event
363 tracing::debug!("[STUB] Would generate OrderRejected event");
364 Ok(())
365 }
366}