Skip to main content

deribit_base/model/
order.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/7/25
5******************************************************************************/
6use pretty_simple_display::{DebugPretty, DisplaySimple};
7use serde::{Deserialize, Serialize};
8
9/// Time in force enumeration
10#[derive(DebugPretty, DisplaySimple, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum TimeInForce {
12    /// Order remains active until explicitly cancelled
13    #[serde(rename = "good_til_cancelled")]
14    GoodTilCancelled,
15    /// Order expires at the end of the trading day
16    #[serde(rename = "good_til_day")]
17    GoodTilDay,
18    /// Order must be filled immediately and completely or cancelled
19    #[serde(rename = "fill_or_kill")]
20    FillOrKill,
21    /// Order must be filled immediately, partial fills allowed, remaining cancelled
22    #[serde(rename = "immediate_or_cancel")]
23    ImmediateOrCancel,
24}
25
26impl TimeInForce {
27    /// Returns the string representation of the time in force value
28    pub fn as_str(&self) -> &'static str {
29        match self {
30            TimeInForce::GoodTilCancelled => "good_til_cancelled",
31            TimeInForce::GoodTilDay => "good_til_day",
32            TimeInForce::FillOrKill => "fill_or_kill",
33            TimeInForce::ImmediateOrCancel => "immediate_or_cancel",
34        }
35    }
36}
37
38/// Order side enumeration
39#[derive(DebugPretty, DisplaySimple, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum OrderSide {
41    /// Buy order
42    #[serde(rename = "buy")]
43    Buy,
44    /// Sell order
45    #[serde(rename = "sell")]
46    Sell,
47}
48
49impl OrderSide {
50    /// Returns the string representation of the order side
51    pub fn as_str(&self) -> &'static str {
52        match self {
53            OrderSide::Buy => "buy",
54            OrderSide::Sell => "sell",
55        }
56    }
57}
58
59/// Order type enum
60#[derive(DebugPretty, DisplaySimple, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61pub enum OrderType {
62    /// Limit order - executes at specified price or better
63    #[serde(rename = "limit")]
64    Limit,
65    /// Market order - executes immediately at best available price
66    #[serde(rename = "market")]
67    Market,
68    /// Stop limit order - becomes limit order when stop price is reached
69    #[serde(rename = "stop_limit")]
70    StopLimit,
71    /// Stop market order - becomes market order when stop price is reached
72    #[serde(rename = "stop_market")]
73    StopMarket,
74    /// Take limit order - limit order to take profit
75    #[serde(rename = "take_limit")]
76    TakeLimit,
77    /// Take market order - market order to take profit
78    #[serde(rename = "take_market")]
79    TakeMarket,
80    /// Market limit order - market order with limit price protection
81    #[serde(rename = "market_limit")]
82    MarketLimit,
83    /// Trailing stop order - stop order that trails the market price
84    #[serde(rename = "trailing_stop")]
85    TrailingStop,
86}
87
88impl OrderType {
89    /// Returns the string representation of the order type
90    pub fn as_str(&self) -> &'static str {
91        match self {
92            OrderType::Limit => "limit",
93            OrderType::Market => "market",
94            OrderType::StopLimit => "stop_limit",
95            OrderType::StopMarket => "stop_market",
96            OrderType::TakeLimit => "take_limit",
97            OrderType::TakeMarket => "take_market",
98            OrderType::MarketLimit => "market_limit",
99            OrderType::TrailingStop => "trailing_stop",
100        }
101    }
102}
103
104/// New order request structure
105#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
106pub struct NewOrderRequest {
107    /// Trading symbol/instrument name
108    pub symbol: String,
109    /// Order side (buy or sell)
110    pub side: OrderSide,
111    /// Type of order
112    pub order_type: OrderType,
113    /// Order quantity
114    pub quantity: f64,
115    /// Order price (required for limit orders)
116    pub price: Option<f64>,
117    /// Time in force specification
118    pub time_in_force: TimeInForce,
119    /// Client-specified order identifier
120    pub client_order_id: Option<String>,
121}
122
123/// Order status enumeration
124#[derive(DebugPretty, DisplaySimple, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub enum OrderStatus {
126    /// Order has been accepted by the system
127    New,
128    /// Order has been partially filled
129    PartiallyFilled,
130    /// Order has been completely filled
131    Filled,
132    /// Order is done for the day
133    DoneForDay,
134    /// Order has been cancelled
135    Canceled,
136    /// Order has been replaced
137    Replaced,
138    /// Order cancellation is pending
139    PendingCancel,
140    /// Order has been stopped
141    Stopped,
142    /// Order has been rejected
143    Rejected,
144    /// Order has been suspended
145    Suspended,
146    /// Order is pending acceptance
147    PendingNew,
148    /// Order has been calculated
149    Calculated,
150    /// Order has expired
151    Expired,
152    /// Order has been accepted for bidding
153    AcceptedForBidding,
154    /// Order replacement is pending
155    PendingReplace,
156}
157
158/// Order information
159#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
160pub struct OrderInfo {
161    /// Order amount
162    pub amount: f64,
163    /// Whether order was placed via API
164    pub api: bool,
165    /// Average execution price
166    pub average_price: f64,
167    /// Order creation timestamp
168    pub creation_timestamp: u64,
169    /// Order direction (buy/sell)
170    pub direction: String,
171    /// Amount that has been filled
172    pub filled_amount: f64,
173    /// Instrument name
174    pub instrument_name: String,
175    /// Whether this is a liquidation order
176    pub is_liquidation: bool,
177    /// Order label
178    pub label: String,
179    /// Last update timestamp
180    pub last_update_timestamp: u64,
181    /// Maximum amount to show in order book (optional)
182    pub max_show: Option<f64>,
183    /// Unique order identifier
184    pub order_id: String,
185    /// Current order state
186    pub order_state: String,
187    /// Type of order
188    pub order_type: String,
189    /// Original order type before any modifications
190    pub original_order_type: Option<String>,
191    /// Whether this is a post-only order
192    pub post_only: bool,
193    /// Order price
194    pub price: f64,
195    /// Current profit/loss on the order
196    pub profit_loss: Option<f64>,
197    /// Whether this order only reduces position
198    pub reduce_only: bool,
199    /// Whether this order has been replaced
200    pub replaced: bool,
201    /// Whether this order reduces risk
202    pub risk_reducing: bool,
203    /// Time in force specification
204    pub time_in_force: String,
205    /// Whether the order has been triggered
206    pub triggered: Option<bool>,
207    /// Trigger condition for the order
208    pub trigger: Option<String>,
209    /// USD value of the order
210    pub usd: Option<f64>,
211    /// Whether order was placed via web interface
212    pub web: bool,
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_time_in_force_as_str() {
221        assert_eq!(TimeInForce::GoodTilCancelled.as_str(), "good_til_cancelled");
222        assert_eq!(TimeInForce::GoodTilDay.as_str(), "good_til_day");
223        assert_eq!(TimeInForce::FillOrKill.as_str(), "fill_or_kill");
224        assert_eq!(
225            TimeInForce::ImmediateOrCancel.as_str(),
226            "immediate_or_cancel"
227        );
228    }
229
230    #[test]
231    fn test_time_in_force_serialization() {
232        let tif = TimeInForce::GoodTilCancelled;
233        let json = serde_json::to_string(&tif).unwrap();
234        assert_eq!(json, "\"good_til_cancelled\"");
235
236        let deserialized: TimeInForce = serde_json::from_str(&json).unwrap();
237        assert_eq!(deserialized, TimeInForce::GoodTilCancelled);
238    }
239
240    #[test]
241    fn test_order_side_serialization() {
242        let buy_side = OrderSide::Buy;
243        let sell_side = OrderSide::Sell;
244
245        let buy_json = serde_json::to_string(&buy_side).unwrap();
246        let sell_json = serde_json::to_string(&sell_side).unwrap();
247
248        assert_eq!(buy_json, "\"buy\"");
249        assert_eq!(sell_json, "\"sell\"");
250
251        let buy_deserialized: OrderSide = serde_json::from_str(&buy_json).unwrap();
252        let sell_deserialized: OrderSide = serde_json::from_str(&sell_json).unwrap();
253
254        assert_eq!(buy_deserialized, OrderSide::Buy);
255        assert_eq!(sell_deserialized, OrderSide::Sell);
256    }
257
258    #[test]
259    fn test_order_type_as_str() {
260        assert_eq!(OrderType::Limit.as_str(), "limit");
261        assert_eq!(OrderType::Market.as_str(), "market");
262        assert_eq!(OrderType::StopLimit.as_str(), "stop_limit");
263        assert_eq!(OrderType::StopMarket.as_str(), "stop_market");
264        assert_eq!(OrderType::TakeLimit.as_str(), "take_limit");
265        assert_eq!(OrderType::TakeMarket.as_str(), "take_market");
266        assert_eq!(OrderType::MarketLimit.as_str(), "market_limit");
267        assert_eq!(OrderType::TrailingStop.as_str(), "trailing_stop");
268    }
269
270    #[test]
271    fn test_order_type_serialization() {
272        let order_type = OrderType::Limit;
273        let json = serde_json::to_string(&order_type).unwrap();
274        assert_eq!(json, "\"limit\"");
275
276        let deserialized: OrderType = serde_json::from_str(&json).unwrap();
277        assert_eq!(deserialized, OrderType::Limit);
278    }
279
280    #[test]
281    fn test_new_order_request_creation() {
282        let order = NewOrderRequest {
283            symbol: "BTC-PERPETUAL".to_string(),
284            side: OrderSide::Buy,
285            order_type: OrderType::Limit,
286            quantity: 1.0,
287            price: Some(50000.0),
288            time_in_force: TimeInForce::GoodTilCancelled,
289            client_order_id: Some("CLIENT_ORDER_123".to_string()),
290        };
291
292        assert_eq!(order.symbol, "BTC-PERPETUAL");
293        assert_eq!(order.side, OrderSide::Buy);
294        assert_eq!(order.order_type, OrderType::Limit);
295        assert_eq!(order.quantity, 1.0);
296        assert_eq!(order.price, Some(50000.0));
297        assert_eq!(order.time_in_force, TimeInForce::GoodTilCancelled);
298        assert_eq!(order.client_order_id, Some("CLIENT_ORDER_123".to_string()));
299    }
300
301    #[test]
302    fn test_new_order_request_market_order() {
303        let market_order = NewOrderRequest {
304            symbol: "ETH-PERPETUAL".to_string(),
305            side: OrderSide::Sell,
306            order_type: OrderType::Market,
307            quantity: 2.0,
308            price: None, // Market orders don't have price
309            time_in_force: TimeInForce::ImmediateOrCancel,
310            client_order_id: None,
311        };
312
313        assert_eq!(market_order.order_type, OrderType::Market);
314        assert_eq!(market_order.price, None);
315        assert_eq!(market_order.client_order_id, None);
316    }
317
318    #[test]
319    fn test_order_status_variants() {
320        let statuses = vec![
321            OrderStatus::New,
322            OrderStatus::PartiallyFilled,
323            OrderStatus::Filled,
324            OrderStatus::DoneForDay,
325            OrderStatus::Canceled,
326            OrderStatus::Replaced,
327            OrderStatus::PendingCancel,
328            OrderStatus::Stopped,
329            OrderStatus::Rejected,
330            OrderStatus::Suspended,
331            OrderStatus::PendingNew,
332            OrderStatus::Calculated,
333            OrderStatus::Expired,
334            OrderStatus::AcceptedForBidding,
335            OrderStatus::PendingReplace,
336        ];
337
338        // Test that all variants can be created and compared
339        for status in statuses {
340            let cloned = status;
341            assert_eq!(status, cloned);
342        }
343    }
344
345    #[test]
346    fn test_order_info_creation() {
347        let order_info = OrderInfo {
348            amount: 1.0,
349            api: true,
350            average_price: 50000.0,
351            creation_timestamp: 1640995200000,
352            direction: "buy".to_string(),
353            filled_amount: 0.5,
354            instrument_name: "BTC-PERPETUAL".to_string(),
355            is_liquidation: false,
356            label: "test_order".to_string(),
357            last_update_timestamp: 1640995300000,
358            max_show: Some(0.8),
359            order_id: "ORDER_123".to_string(),
360            order_state: "open".to_string(),
361            order_type: "limit".to_string(),
362            original_order_type: None,
363            post_only: false,
364            price: 50000.0,
365            profit_loss: Some(100.0),
366            reduce_only: false,
367            replaced: false,
368            risk_reducing: false,
369            time_in_force: "good_til_cancelled".to_string(),
370            triggered: Some(false),
371            trigger: None,
372            usd: Some(50000.0),
373            web: false,
374        };
375
376        assert_eq!(order_info.amount, 1.0);
377        assert_eq!(order_info.instrument_name, "BTC-PERPETUAL");
378        assert_eq!(order_info.filled_amount, 0.5);
379        assert_eq!(order_info.max_show, Some(0.8));
380        assert_eq!(order_info.profit_loss, Some(100.0));
381        assert!(order_info.api);
382        assert!(!order_info.is_liquidation);
383    }
384
385    #[test]
386    fn test_order_info_optional_fields() {
387        let minimal_order_info = OrderInfo {
388            amount: 1.0,
389            api: true,
390            average_price: 0.0,
391            creation_timestamp: 1640995200000,
392            direction: "buy".to_string(),
393            filled_amount: 0.0,
394            instrument_name: "BTC-PERPETUAL".to_string(),
395            is_liquidation: false,
396            label: "".to_string(),
397            last_update_timestamp: 1640995200000,
398            max_show: None,
399            order_id: "ORDER_123".to_string(),
400            order_state: "new".to_string(),
401            order_type: "limit".to_string(),
402            original_order_type: None,
403            post_only: false,
404            price: 50000.0,
405            profit_loss: None,
406            reduce_only: false,
407            replaced: false,
408            risk_reducing: false,
409            time_in_force: "good_til_cancelled".to_string(),
410            triggered: None,
411            trigger: None,
412            usd: None,
413            web: false,
414        };
415
416        assert_eq!(minimal_order_info.max_show, None);
417        assert_eq!(minimal_order_info.profit_loss, None);
418        assert_eq!(minimal_order_info.triggered, None);
419        assert_eq!(minimal_order_info.trigger, None);
420        assert_eq!(minimal_order_info.usd, None);
421    }
422
423    #[test]
424    fn test_serialization_roundtrip() {
425        let order = NewOrderRequest {
426            symbol: "BTC-PERPETUAL".to_string(),
427            side: OrderSide::Buy,
428            order_type: OrderType::Limit,
429            quantity: 1.0,
430            price: Some(50000.0),
431            time_in_force: TimeInForce::GoodTilCancelled,
432            client_order_id: Some("CLIENT_ORDER_123".to_string()),
433        };
434
435        let json = serde_json::to_string(&order).unwrap();
436        let deserialized: NewOrderRequest = serde_json::from_str(&json).unwrap();
437
438        assert_eq!(order.symbol, deserialized.symbol);
439        assert_eq!(order.side, deserialized.side);
440        assert_eq!(order.order_type, deserialized.order_type);
441        assert_eq!(order.quantity, deserialized.quantity);
442        assert_eq!(order.price, deserialized.price);
443        assert_eq!(order.time_in_force, deserialized.time_in_force);
444        assert_eq!(order.client_order_id, deserialized.client_order_id);
445    }
446
447    #[test]
448    fn test_debug_and_display_implementations() {
449        let order = NewOrderRequest {
450            symbol: "BTC-PERPETUAL".to_string(),
451            side: OrderSide::Buy,
452            order_type: OrderType::Limit,
453            quantity: 1.0,
454            price: Some(50000.0),
455            time_in_force: TimeInForce::GoodTilCancelled,
456            client_order_id: Some("CLIENT_ORDER_123".to_string()),
457        };
458
459        let debug_str = format!("{:?}", order);
460        let display_str = format!("{}", order);
461
462        assert!(debug_str.contains("BTC-PERPETUAL"));
463        assert!(display_str.contains("BTC-PERPETUAL"));
464    }
465
466    #[test]
467    fn test_enum_equality_and_cloning() {
468        let tif1 = TimeInForce::GoodTilCancelled;
469        let tif2 = tif1;
470        assert_eq!(tif1, tif2);
471
472        let side1 = OrderSide::Buy;
473        let side2 = side1;
474        assert_eq!(side1, side2);
475
476        let type1 = OrderType::Limit;
477        let type2 = type1;
478        assert_eq!(type1, type2);
479
480        let status1 = OrderStatus::New;
481        let status2 = status1;
482        assert_eq!(status1, status2);
483    }
484
485    #[test]
486    fn test_order_type_variants_coverage() {
487        // Test all order type variants
488        let types = vec![
489            OrderType::Limit,
490            OrderType::Market,
491            OrderType::StopLimit,
492            OrderType::StopMarket,
493            OrderType::TakeLimit,
494            OrderType::TakeMarket,
495            OrderType::MarketLimit,
496            OrderType::TrailingStop,
497        ];
498
499        for order_type in types {
500            // Test as_str method
501            let str_repr = order_type.as_str();
502            assert!(!str_repr.is_empty());
503
504            // Test serialization
505            let json = serde_json::to_string(&order_type).unwrap();
506            let deserialized: OrderType = serde_json::from_str(&json).unwrap();
507            assert_eq!(order_type, deserialized);
508        }
509    }
510
511    #[test]
512    fn test_time_in_force_variants_coverage() {
513        let tifs = vec![
514            TimeInForce::GoodTilCancelled,
515            TimeInForce::GoodTilDay,
516            TimeInForce::FillOrKill,
517            TimeInForce::ImmediateOrCancel,
518        ];
519
520        for tif in tifs {
521            // Test as_str method
522            let str_repr = tif.as_str();
523            assert!(!str_repr.is_empty());
524
525            // Test serialization
526            let json = serde_json::to_string(&tif).unwrap();
527            let deserialized: TimeInForce = serde_json::from_str(&json).unwrap();
528            assert_eq!(tif, deserialized);
529        }
530    }
531}