nautilus_dydx/execution/types.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 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
16//! Shared types for dYdX v4 execution module.
17//!
18//! This module centralizes type definitions used across order submission,
19//! transaction management, and WebSocket handling components.
20
21use chrono::{DateTime, Duration, Utc};
22use nautilus_core::UnixNanos;
23use nautilus_model::{
24 enums::{OrderSide, TimeInForce},
25 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
26 types::{Price, Quantity},
27};
28
29use crate::error::DydxError;
30
31/// Default expiration for GTC conditional orders (90 days).
32///
33/// The protocol limit (`StatefulOrderTimeWindow`) is 95 days. This default
34/// leaves a 5-day buffer so the order timestamp never exceeds the window
35/// when the local clock is slightly ahead of the last block time.
36pub const GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS: i64 = 90;
37
38/// Order flag for short-term orders (expire by block height).
39pub const ORDER_FLAG_SHORT_TERM: u32 = 0;
40
41/// Order flag for conditional orders (stop-loss, take-profit).
42pub const ORDER_FLAG_CONDITIONAL: u32 = 32;
43
44/// Order flag for long-term/stateful orders (expire by timestamp).
45pub const ORDER_FLAG_LONG_TERM: u32 = 64;
46
47/// Order lifetime type determined by time_in_force and expire_time.
48///
49/// dYdX v4 has different execution paths for orders based on their expected lifetime:
50/// - **ShortTerm**: Lower latency/fees, expire by block height (max 20 blocks ~30s)
51/// - **LongTerm**: Stored on-chain, expire by timestamp, explicit cancel events
52/// - **Conditional**: Triggered by price conditions, always stored on-chain
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum OrderLifetime {
55 /// Short-term orders expire by block height (max 20 blocks).
56 /// Lower latency and fees, but expire silently without cancel events.
57 /// Used for IOC, FOK, or orders expiring within 60 seconds.
58 ShortTerm,
59 /// Long-term orders expire by timestamp.
60 /// Stored on-chain with explicit cancel events when they expire or are cancelled.
61 /// Used for GTC, GTD orders with expiry > 60 seconds.
62 LongTerm,
63 /// Conditional orders triggered by price conditions.
64 /// Always stored on-chain (stateful), used for stop-loss and take-profit orders.
65 Conditional,
66}
67
68impl OrderLifetime {
69 /// Determines order lifetime based on time_in_force, expire_time, and max short-term duration.
70 ///
71 /// The `max_short_term_secs` is computed dynamically from `BlockTimeMonitor`:
72 /// `max_short_term_secs = SHORT_TERM_ORDER_MAXIMUM_LIFETIME (20 blocks) × seconds_per_block`
73 ///
74 /// Returns `ShortTerm` when:
75 /// - TimeInForce is IOC or FOK (immediate execution orders)
76 /// - expire_time is set and within `max_short_term_secs` from now
77 ///
78 /// Returns `LongTerm` for GTC/GTD orders with expiry beyond short-term window.
79 /// Returns `Conditional` for stop/take-profit orders (when `is_conditional` is true).
80 #[must_use]
81 pub fn from_time_in_force(
82 time_in_force: TimeInForce,
83 expire_time: Option<i64>,
84 is_conditional: bool,
85 max_short_term_secs: f64,
86 ) -> Self {
87 if is_conditional {
88 return Self::Conditional;
89 }
90
91 // IOC and FOK are always short-term (immediate execution)
92 if matches!(time_in_force, TimeInForce::Ioc | TimeInForce::Fok) {
93 return Self::ShortTerm;
94 }
95
96 // Check if expire_time is within the short-term window
97 if let Some(expire_ts) = expire_time {
98 let now = Utc::now().timestamp();
99 let time_until_expiry = expire_ts - now;
100 if time_until_expiry > 0 && (time_until_expiry as f64) <= max_short_term_secs {
101 return Self::ShortTerm;
102 }
103 }
104
105 Self::LongTerm
106 }
107
108 /// Returns the dYdX order_flags value for this lifetime.
109 ///
110 /// These flags are used in both `MsgPlaceOrder` and `MsgCancelOrder` to identify
111 /// the order type on-chain.
112 #[must_use]
113 pub const fn order_flags(&self) -> u32 {
114 match self {
115 Self::ShortTerm => ORDER_FLAG_SHORT_TERM,
116 Self::LongTerm => ORDER_FLAG_LONG_TERM,
117 Self::Conditional => ORDER_FLAG_CONDITIONAL,
118 }
119 }
120
121 /// Returns true if this is a short-term order.
122 #[must_use]
123 pub const fn is_short_term(&self) -> bool {
124 matches!(self, Self::ShortTerm)
125 }
126
127 /// Returns true if this is a conditional order (stop/take-profit).
128 #[must_use]
129 pub const fn is_conditional(&self) -> bool {
130 matches!(self, Self::Conditional)
131 }
132}
133
134/// Conditional order types supported by dYdX.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum ConditionalOrderType {
137 /// Triggers at trigger price, executes as market order.
138 StopMarket,
139 /// Triggers at trigger price, places limit order at limit price.
140 StopLimit,
141 /// Triggers at trigger price for profit taking, executes as market order.
142 TakeProfitMarket,
143 /// Triggers at trigger price for profit taking, places limit order at limit price.
144 TakeProfitLimit,
145}
146
147/// Parameters for a limit order in batch submission.
148#[derive(Debug, Clone)]
149pub struct LimitOrderParams {
150 /// Instrument to trade.
151 pub instrument_id: InstrumentId,
152 /// Client-assigned order ID (u32 for dYdX protocol).
153 pub client_order_id: u32,
154 /// Client metadata for bidirectional ClientOrderId encoding.
155 /// Used to store identity bits (trader/strategy/count) for deterministic decoding.
156 pub client_metadata: u32,
157 /// Order side (Buy or Sell).
158 pub side: OrderSide,
159 /// Limit price.
160 pub price: Price,
161 /// Order quantity.
162 pub quantity: Quantity,
163 /// Time in force.
164 pub time_in_force: TimeInForce,
165 /// Whether this is a post-only order.
166 pub post_only: bool,
167 /// Whether this is a reduce-only order.
168 pub reduce_only: bool,
169 /// Optional expiration timestamp (nanoseconds since epoch).
170 /// The builder will convert this to seconds and apply default_short_term_expiry if configured.
171 pub expire_time_ns: Option<UnixNanos>,
172}
173
174/// Contains the raw bytes and metadata for retry handling.
175#[derive(Debug, Clone)]
176pub struct PreparedTransaction {
177 /// Serialized transaction bytes.
178 pub tx_bytes: Vec<u8>,
179 /// Sequence number used for this transaction.
180 pub sequence: u64,
181 /// Human-readable operation name for logging.
182 pub operation: String,
183}
184
185/// Order context passed from submission to WebSocket confirmation handler.
186///
187/// This context is registered before transaction submission and used by the
188/// WebSocket handler to correlate incoming order updates with the original
189/// submission request, similar to Deribit's `order_contexts` pattern.
190#[derive(Debug, Clone)]
191pub struct OrderContext {
192 /// Nautilus client order ID.
193 pub client_order_id: ClientOrderId,
194 /// Trader ID from the order.
195 pub trader_id: TraderId,
196 /// Strategy ID that submitted the order.
197 pub strategy_id: StrategyId,
198 /// Instrument being traded.
199 pub instrument_id: InstrumentId,
200 /// Timestamp when the order was submitted.
201 pub submitted_at: UnixNanos,
202 /// dYdX order flags (0=short-term, 32=conditional, 64=long-term).
203 /// Stored at submission time to ensure cancellation uses correct flags.
204 pub order_flags: u32,
205}
206
207/// Calculates the expiration time for conditional orders based on TimeInForce.
208///
209/// - `GTD` with explicit `expire_time`: uses the provided timestamp.
210/// - `GTC` or no `expire_time`: defaults to 90 days from now.
211/// - `IOC`/`FOK`: uses 1 hour (these are unusual for conditional orders).
212///
213/// # Errors
214///
215/// Returns `DydxError::Parse` if the provided `expire_time` timestamp is invalid.
216pub fn calculate_conditional_order_expiration(
217 time_in_force: TimeInForce,
218 expire_time: Option<i64>,
219) -> Result<DateTime<Utc>, DydxError> {
220 if let Some(expire_ts) = expire_time {
221 DateTime::from_timestamp(expire_ts, 0)
222 .ok_or_else(|| DydxError::Parse(format!("Invalid expire timestamp: {expire_ts}")))
223 } else {
224 let expiration = match time_in_force {
225 TimeInForce::Gtc => Utc::now() + Duration::days(GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS),
226 TimeInForce::Ioc | TimeInForce::Fok => {
227 // IOC/FOK don't typically apply to conditional orders, use short expiration
228 Utc::now() + Duration::hours(1)
229 }
230 // GTD without expire_time, or any other TIF - use long default
231 _ => Utc::now() + Duration::days(GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS),
232 };
233 Ok(expiration)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use rstest::rstest;
240
241 use super::*;
242
243 // Default max short-term seconds for testing (20 blocks × 3 sec/block = 60 sec)
244 const TEST_MAX_SHORT_TERM_SECS: f64 = 60.0;
245
246 #[rstest]
247 fn test_order_lifetime_ioc_is_short_term() {
248 let lifetime = OrderLifetime::from_time_in_force(
249 TimeInForce::Ioc,
250 None,
251 false,
252 TEST_MAX_SHORT_TERM_SECS,
253 );
254 assert_eq!(lifetime, OrderLifetime::ShortTerm);
255 assert!(lifetime.is_short_term());
256 }
257
258 #[rstest]
259 fn test_order_lifetime_fok_is_short_term() {
260 let lifetime = OrderLifetime::from_time_in_force(
261 TimeInForce::Fok,
262 None,
263 false,
264 TEST_MAX_SHORT_TERM_SECS,
265 );
266 assert_eq!(lifetime, OrderLifetime::ShortTerm);
267 }
268
269 #[rstest]
270 fn test_order_lifetime_gtc_is_long_term() {
271 let lifetime = OrderLifetime::from_time_in_force(
272 TimeInForce::Gtc,
273 None,
274 false,
275 TEST_MAX_SHORT_TERM_SECS,
276 );
277 assert_eq!(lifetime, OrderLifetime::LongTerm);
278 assert!(!lifetime.is_short_term());
279 }
280
281 #[rstest]
282 fn test_order_lifetime_conditional_flag() {
283 let lifetime = OrderLifetime::from_time_in_force(
284 TimeInForce::Gtc,
285 None,
286 true,
287 TEST_MAX_SHORT_TERM_SECS,
288 );
289 assert_eq!(lifetime, OrderLifetime::Conditional);
290 assert!(lifetime.is_conditional());
291 }
292
293 #[rstest]
294 fn test_order_lifetime_short_expire_time() {
295 // Expire time 30 seconds from now should be short-term (within 60s max)
296 let expire_time = Some(Utc::now().timestamp() + 30);
297 let lifetime = OrderLifetime::from_time_in_force(
298 TimeInForce::Gtc,
299 expire_time,
300 false,
301 TEST_MAX_SHORT_TERM_SECS,
302 );
303 assert_eq!(lifetime, OrderLifetime::ShortTerm);
304 }
305
306 #[rstest]
307 fn test_order_lifetime_long_expire_time() {
308 // Expire time 5 minutes from now should be long-term (beyond 60s max)
309 let expire_time = Some(Utc::now().timestamp() + 300);
310 let lifetime = OrderLifetime::from_time_in_force(
311 TimeInForce::Gtd,
312 expire_time,
313 false,
314 TEST_MAX_SHORT_TERM_SECS,
315 );
316 assert_eq!(lifetime, OrderLifetime::LongTerm);
317 }
318
319 #[rstest]
320 fn test_order_lifetime_expire_at_boundary() {
321 // Expire time exactly at max_short_term_secs should be short-term
322 let expire_time = Some(Utc::now().timestamp() + 60);
323 let lifetime = OrderLifetime::from_time_in_force(
324 TimeInForce::Gtd,
325 expire_time,
326 false,
327 TEST_MAX_SHORT_TERM_SECS,
328 );
329 assert_eq!(lifetime, OrderLifetime::ShortTerm);
330 }
331
332 #[rstest]
333 fn test_order_lifetime_expire_just_beyond_boundary() {
334 // Expire time 1 second beyond max should be long-term
335 let expire_time = Some(Utc::now().timestamp() + 61);
336 let lifetime = OrderLifetime::from_time_in_force(
337 TimeInForce::Gtd,
338 expire_time,
339 false,
340 TEST_MAX_SHORT_TERM_SECS,
341 );
342 assert_eq!(lifetime, OrderLifetime::LongTerm);
343 }
344
345 #[rstest]
346 fn test_order_flags() {
347 assert_eq!(
348 OrderLifetime::ShortTerm.order_flags(),
349 ORDER_FLAG_SHORT_TERM
350 );
351 assert_eq!(OrderLifetime::LongTerm.order_flags(), ORDER_FLAG_LONG_TERM);
352 assert_eq!(
353 OrderLifetime::Conditional.order_flags(),
354 ORDER_FLAG_CONDITIONAL
355 );
356 }
357}