Skip to main content

deribit_fix/model/
request.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 6/3/26
5******************************************************************************/
6
7//! Order request model types for Deribit API compatibility
8//!
9//! This module provides order-related types that were previously imported from
10//! deribit-base. These types represent order requests and their parameters
11//! in API-style format (not FIX protocol format).
12
13use serde::{Deserialize, Serialize};
14
15/// Time in force enumeration (API style)
16///
17/// Specifies how long an order remains active before it is executed or expires.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum TimeInForce {
20    /// Order remains active until explicitly cancelled
21    #[serde(rename = "good_til_cancelled")]
22    GoodTilCancelled,
23    /// Order expires at the end of the trading day
24    #[serde(rename = "good_til_day")]
25    GoodTilDay,
26    /// Order must be filled immediately and completely or cancelled
27    #[serde(rename = "fill_or_kill")]
28    FillOrKill,
29    /// Order must be filled immediately, partial fills allowed, remaining cancelled
30    #[serde(rename = "immediate_or_cancel")]
31    ImmediateOrCancel,
32}
33
34impl TimeInForce {
35    /// Returns the string representation of the time in force value
36    #[must_use]
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            TimeInForce::GoodTilCancelled => "good_til_cancelled",
40            TimeInForce::GoodTilDay => "good_til_day",
41            TimeInForce::FillOrKill => "fill_or_kill",
42            TimeInForce::ImmediateOrCancel => "immediate_or_cancel",
43        }
44    }
45}
46
47/// Order side enumeration (API style)
48///
49/// Indicates whether an order is a buy or sell order.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum OrderSide {
52    /// Buy order
53    Buy,
54    /// Sell order
55    Sell,
56}
57
58/// Order type enumeration (API style)
59///
60/// Specifies the type of order execution.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62pub enum OrderType {
63    /// Limit order - executes at specified price or better
64    #[serde(rename = "limit")]
65    Limit,
66    /// Market order - executes immediately at best available price
67    #[serde(rename = "market")]
68    Market,
69    /// Stop limit order - becomes limit order when stop price is reached
70    #[serde(rename = "stop_limit")]
71    StopLimit,
72    /// Stop market order - becomes market order when stop price is reached
73    #[serde(rename = "stop_market")]
74    StopMarket,
75    /// Take limit order - limit order to take profit
76    #[serde(rename = "take_limit")]
77    TakeLimit,
78    /// Take market order - market order to take profit
79    #[serde(rename = "take_market")]
80    TakeMarket,
81    /// Market limit order - market order with limit price protection
82    #[serde(rename = "market_limit")]
83    MarketLimit,
84    /// Trailing stop order - stop order that trails the market price
85    #[serde(rename = "trailing_stop")]
86    TrailingStop,
87}
88
89impl OrderType {
90    /// Returns the string representation of the order type
91    #[must_use]
92    pub fn as_str(&self) -> &'static str {
93        match self {
94            OrderType::Limit => "limit",
95            OrderType::Market => "market",
96            OrderType::StopLimit => "stop_limit",
97            OrderType::StopMarket => "stop_market",
98            OrderType::TakeLimit => "take_limit",
99            OrderType::TakeMarket => "take_market",
100            OrderType::MarketLimit => "market_limit",
101            OrderType::TrailingStop => "trailing_stop",
102        }
103    }
104}
105
106/// Trigger type for stop orders
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108pub enum TriggerType {
109    /// Index price trigger
110    #[serde(rename = "index_price")]
111    IndexPrice,
112    /// Mark price trigger
113    #[serde(rename = "mark_price")]
114    MarkPrice,
115    /// Last price trigger
116    #[serde(rename = "last_price")]
117    LastPrice,
118}
119
120/// Advanced order type
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122pub enum AdvancedOrderType {
123    /// USD order
124    #[serde(rename = "usd")]
125    Usd,
126    /// Implv order (implied volatility)
127    #[serde(rename = "implv")]
128    Implv,
129}
130
131/// Generic request for creating new orders (API style)
132///
133/// This structure represents an order request in the API format used by
134/// deribit-base. It contains all parameters needed to place a new order.
135#[derive(Clone, Serialize, Deserialize)]
136pub struct NewOrderRequest {
137    /// Instrument name (e.g., "BTC-PERPETUAL")
138    pub instrument_name: String,
139    /// Order amount
140    pub amount: f64,
141    /// Order type
142    #[serde(rename = "type")]
143    pub order_type: OrderType,
144    /// Order side (buy/sell)
145    pub side: OrderSide,
146    /// Order price (required for limit orders)
147    pub price: Option<f64>,
148    /// Time in force
149    pub time_in_force: TimeInForce,
150    /// Post-only flag
151    pub post_only: Option<bool>,
152    /// Reduce-only flag
153    pub reduce_only: Option<bool>,
154    /// Order label
155    pub label: Option<String>,
156    /// Stop price for stop orders
157    pub stop_price: Option<f64>,
158    /// Trigger type for stop orders
159    pub trigger: Option<TriggerType>,
160    /// Advanced order type
161    pub advanced: Option<AdvancedOrderType>,
162    /// Maximum show amount (iceberg orders)
163    pub max_show: Option<f64>,
164    /// Reject post-only flag
165    pub reject_post_only: Option<bool>,
166    /// Valid until timestamp
167    pub valid_until: Option<i64>,
168    /// Client order ID for tracking
169    pub client_order_id: Option<String>,
170}
171
172impl_json_display!(NewOrderRequest);
173impl_json_debug_pretty!(NewOrderRequest);
174
175impl NewOrderRequest {
176    /// Create a new market buy order
177    #[must_use]
178    pub fn market_buy(instrument_name: String, amount: f64) -> Self {
179        Self {
180            instrument_name,
181            amount,
182            order_type: OrderType::Market,
183            side: OrderSide::Buy,
184            price: None,
185            time_in_force: TimeInForce::ImmediateOrCancel,
186            post_only: None,
187            reduce_only: None,
188            label: None,
189            stop_price: None,
190            trigger: None,
191            advanced: None,
192            max_show: None,
193            reject_post_only: None,
194            valid_until: None,
195            client_order_id: None,
196        }
197    }
198
199    /// Create a new market sell order
200    #[must_use]
201    pub fn market_sell(instrument_name: String, amount: f64) -> Self {
202        Self {
203            instrument_name,
204            amount,
205            order_type: OrderType::Market,
206            side: OrderSide::Sell,
207            price: None,
208            time_in_force: TimeInForce::ImmediateOrCancel,
209            post_only: None,
210            reduce_only: None,
211            label: None,
212            stop_price: None,
213            trigger: None,
214            advanced: None,
215            max_show: None,
216            reject_post_only: None,
217            valid_until: None,
218            client_order_id: None,
219        }
220    }
221
222    /// Create a new limit buy order
223    #[must_use]
224    pub fn limit_buy(instrument_name: String, amount: f64, price: f64) -> Self {
225        Self {
226            instrument_name,
227            amount,
228            order_type: OrderType::Limit,
229            side: OrderSide::Buy,
230            price: Some(price),
231            time_in_force: TimeInForce::GoodTilCancelled,
232            post_only: None,
233            reduce_only: None,
234            label: None,
235            stop_price: None,
236            trigger: None,
237            advanced: None,
238            max_show: None,
239            reject_post_only: None,
240            valid_until: None,
241            client_order_id: None,
242        }
243    }
244
245    /// Create a new limit sell order
246    #[must_use]
247    pub fn limit_sell(instrument_name: String, amount: f64, price: f64) -> Self {
248        Self {
249            instrument_name,
250            amount,
251            order_type: OrderType::Limit,
252            side: OrderSide::Sell,
253            price: Some(price),
254            time_in_force: TimeInForce::GoodTilCancelled,
255            post_only: None,
256            reduce_only: None,
257            label: None,
258            stop_price: None,
259            trigger: None,
260            advanced: None,
261            max_show: None,
262            reject_post_only: None,
263            valid_until: None,
264            client_order_id: None,
265        }
266    }
267
268    /// Set the order as post-only
269    #[must_use]
270    pub fn with_post_only(mut self, post_only: bool) -> Self {
271        self.post_only = Some(post_only);
272        self
273    }
274
275    /// Set the order as reduce-only
276    #[must_use]
277    pub fn with_reduce_only(mut self, reduce_only: bool) -> Self {
278        self.reduce_only = Some(reduce_only);
279        self
280    }
281
282    /// Set order label
283    #[must_use]
284    pub fn with_label(mut self, label: String) -> Self {
285        self.label = Some(label);
286        self
287    }
288
289    /// Set time in force
290    #[must_use]
291    pub fn with_time_in_force(mut self, tif: TimeInForce) -> Self {
292        self.time_in_force = tif;
293        self
294    }
295
296    /// Set client order ID
297    #[must_use]
298    pub fn with_client_order_id(mut self, client_order_id: String) -> Self {
299        self.client_order_id = Some(client_order_id);
300        self
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_time_in_force_as_str() {
310        assert_eq!(TimeInForce::GoodTilCancelled.as_str(), "good_til_cancelled");
311        assert_eq!(TimeInForce::GoodTilDay.as_str(), "good_til_day");
312        assert_eq!(TimeInForce::FillOrKill.as_str(), "fill_or_kill");
313        assert_eq!(
314            TimeInForce::ImmediateOrCancel.as_str(),
315            "immediate_or_cancel"
316        );
317    }
318
319    #[test]
320    fn test_order_type_as_str() {
321        assert_eq!(OrderType::Limit.as_str(), "limit");
322        assert_eq!(OrderType::Market.as_str(), "market");
323        assert_eq!(OrderType::StopLimit.as_str(), "stop_limit");
324        assert_eq!(OrderType::StopMarket.as_str(), "stop_market");
325    }
326
327    #[test]
328    fn test_new_order_request_market_buy() {
329        let order = NewOrderRequest::market_buy("BTC-PERPETUAL".to_string(), 1.0);
330        assert_eq!(order.instrument_name, "BTC-PERPETUAL");
331        assert_eq!(order.amount, 1.0);
332        assert_eq!(order.order_type, OrderType::Market);
333        assert_eq!(order.side, OrderSide::Buy);
334        assert_eq!(order.price, None);
335        assert_eq!(order.time_in_force, TimeInForce::ImmediateOrCancel);
336    }
337
338    #[test]
339    fn test_new_order_request_limit_sell() {
340        let order = NewOrderRequest::limit_sell("ETH-PERPETUAL".to_string(), 2.0, 3500.0);
341        assert_eq!(order.instrument_name, "ETH-PERPETUAL");
342        assert_eq!(order.amount, 2.0);
343        assert_eq!(order.order_type, OrderType::Limit);
344        assert_eq!(order.side, OrderSide::Sell);
345        assert_eq!(order.price, Some(3500.0));
346        assert_eq!(order.time_in_force, TimeInForce::GoodTilCancelled);
347    }
348
349    #[test]
350    fn test_new_order_request_builder_pattern() {
351        let order = NewOrderRequest::limit_buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0)
352            .with_post_only(true)
353            .with_reduce_only(false)
354            .with_label("test_order".to_string())
355            .with_client_order_id("CLIENT_123".to_string());
356
357        assert_eq!(order.post_only, Some(true));
358        assert_eq!(order.reduce_only, Some(false));
359        assert_eq!(order.label, Some("test_order".to_string()));
360        assert_eq!(order.client_order_id, Some("CLIENT_123".to_string()));
361    }
362
363    #[test]
364    fn test_serialization_roundtrip() {
365        let order = NewOrderRequest::limit_buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0)
366            .with_post_only(true)
367            .with_label("test_order".to_string());
368
369        let json = serde_json::to_string(&order).unwrap();
370        let deserialized: NewOrderRequest = serde_json::from_str(&json).unwrap();
371
372        assert_eq!(order.instrument_name, deserialized.instrument_name);
373        assert_eq!(order.amount, deserialized.amount);
374        assert_eq!(order.order_type, deserialized.order_type);
375        assert_eq!(order.side, deserialized.side);
376        assert_eq!(order.price, deserialized.price);
377        assert_eq!(order.post_only, deserialized.post_only);
378        assert_eq!(order.label, deserialized.label);
379    }
380}