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}