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