Skip to main content

deribit_websocket/model/
trading.rs

1//! Trading model definitions for Deribit WebSocket API
2//!
3//! This module provides types for buy, sell, cancel, and edit order operations.
4
5use pretty_simple_display::{DebugPretty, DisplaySimple};
6use serde::{Deserialize, Serialize};
7
8/// Order type enumeration
9#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, DebugPretty, DisplaySimple)]
10#[serde(rename_all = "snake_case")]
11pub enum OrderType {
12    /// Limit order - executes at specified price or better
13    Limit,
14    /// Market order - executes immediately at best available price
15    Market,
16    /// Stop limit order - becomes limit order when stop price is reached
17    StopLimit,
18    /// Stop market order - becomes market order when stop price is reached
19    StopMarket,
20    /// Take limit order - limit order to take profit
21    TakeLimit,
22    /// Take market order - market order to take profit
23    TakeMarket,
24    /// Market limit order - market order with limit price protection
25    MarketLimit,
26    /// Trailing stop order - stop order that trails the market price
27    TrailingStop,
28}
29
30impl OrderType {
31    /// Returns the string representation of the order type
32    #[must_use]
33    pub fn as_str(&self) -> &'static str {
34        match self {
35            OrderType::Limit => "limit",
36            OrderType::Market => "market",
37            OrderType::StopLimit => "stop_limit",
38            OrderType::StopMarket => "stop_market",
39            OrderType::TakeLimit => "take_limit",
40            OrderType::TakeMarket => "take_market",
41            OrderType::MarketLimit => "market_limit",
42            OrderType::TrailingStop => "trailing_stop",
43        }
44    }
45}
46
47/// Time in force specification for orders
48#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, DebugPretty, DisplaySimple)]
49#[serde(rename_all = "snake_case")]
50pub enum TimeInForce {
51    /// Good till cancelled - order remains active until filled or cancelled
52    #[serde(rename = "good_til_cancelled")]
53    GoodTilCancelled,
54    /// Good till day - order expires at end of trading day
55    #[serde(rename = "good_til_day")]
56    GoodTilDay,
57    /// Fill or kill - order must be filled immediately and completely or cancelled
58    #[serde(rename = "fill_or_kill")]
59    FillOrKill,
60    /// Immediate or cancel - fill what can be filled immediately, cancel the rest
61    #[serde(rename = "immediate_or_cancel")]
62    ImmediateOrCancel,
63}
64
65impl TimeInForce {
66    /// Returns the string representation of the time in force
67    #[must_use]
68    pub fn as_str(&self) -> &'static str {
69        match self {
70            TimeInForce::GoodTilCancelled => "good_til_cancelled",
71            TimeInForce::GoodTilDay => "good_til_day",
72            TimeInForce::FillOrKill => "fill_or_kill",
73            TimeInForce::ImmediateOrCancel => "immediate_or_cancel",
74        }
75    }
76}
77
78/// Trigger type for conditional orders
79#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, DebugPretty, DisplaySimple)]
80#[serde(rename_all = "snake_case")]
81pub enum Trigger {
82    /// Trigger based on index price
83    IndexPrice,
84    /// Trigger based on mark price
85    MarkPrice,
86    /// Trigger based on last traded price
87    LastPrice,
88}
89
90/// Order request parameters for buy/sell operations
91#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
92pub struct OrderRequest {
93    /// Instrument name (e.g., "BTC-PERPETUAL")
94    pub instrument_name: String,
95    /// Order amount (positive number)
96    pub amount: f64,
97    /// Order type (limit, market, etc.)
98    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
99    pub order_type: Option<OrderType>,
100    /// User-defined label for the order (max 64 chars)
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub label: Option<String>,
103    /// Limit price for the order
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub price: Option<f64>,
106    /// Time in force specification
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub time_in_force: Option<TimeInForce>,
109    /// Maximum amount to show in order book (for iceberg orders)
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub max_show: Option<f64>,
112    /// Whether the order should only be posted (not taken)
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub post_only: Option<bool>,
115    /// Whether this order only reduces position
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub reduce_only: Option<bool>,
118    /// Trigger price for conditional orders
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub trigger_price: Option<f64>,
121    /// Trigger type for conditional orders
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub trigger: Option<Trigger>,
124    /// Advanced order type (usd or implv)
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub advanced: Option<String>,
127    /// Market maker protection flag
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub mmp: Option<bool>,
130    /// Order validity timestamp (Unix timestamp in milliseconds)
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub valid_until: Option<u64>,
133}
134
135impl OrderRequest {
136    /// Create a new limit order request
137    #[must_use]
138    pub fn limit(instrument_name: String, amount: f64, price: f64) -> Self {
139        Self {
140            instrument_name,
141            amount,
142            order_type: Some(OrderType::Limit),
143            label: None,
144            price: Some(price),
145            time_in_force: None,
146            max_show: None,
147            post_only: None,
148            reduce_only: None,
149            trigger_price: None,
150            trigger: None,
151            advanced: None,
152            mmp: None,
153            valid_until: None,
154        }
155    }
156
157    /// Create a new market order request
158    #[must_use]
159    pub fn market(instrument_name: String, amount: f64) -> Self {
160        Self {
161            instrument_name,
162            amount,
163            order_type: Some(OrderType::Market),
164            label: None,
165            price: None,
166            time_in_force: None,
167            max_show: None,
168            post_only: None,
169            reduce_only: None,
170            trigger_price: None,
171            trigger: None,
172            advanced: None,
173            mmp: None,
174            valid_until: None,
175        }
176    }
177
178    /// Set the order label
179    #[must_use]
180    pub fn with_label(mut self, label: String) -> Self {
181        self.label = Some(label);
182        self
183    }
184
185    /// Set time in force
186    #[must_use]
187    pub fn with_time_in_force(mut self, tif: TimeInForce) -> Self {
188        self.time_in_force = Some(tif);
189        self
190    }
191
192    /// Set post-only flag
193    #[must_use]
194    pub fn with_post_only(mut self, post_only: bool) -> Self {
195        self.post_only = Some(post_only);
196        self
197    }
198
199    /// Set reduce-only flag
200    #[must_use]
201    pub fn with_reduce_only(mut self, reduce_only: bool) -> Self {
202        self.reduce_only = Some(reduce_only);
203        self
204    }
205
206    /// Set max show amount for iceberg orders
207    #[must_use]
208    pub fn with_max_show(mut self, max_show: f64) -> Self {
209        self.max_show = Some(max_show);
210        self
211    }
212
213    /// Set trigger price for conditional orders
214    #[must_use]
215    pub fn with_trigger(mut self, trigger_price: f64, trigger: Trigger) -> Self {
216        self.trigger_price = Some(trigger_price);
217        self.trigger = Some(trigger);
218        self
219    }
220
221    /// Set MMP flag
222    #[must_use]
223    pub fn with_mmp(mut self, mmp: bool) -> Self {
224        self.mmp = Some(mmp);
225        self
226    }
227}
228
229/// Edit order request parameters
230#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
231pub struct EditOrderRequest {
232    /// Order ID to edit
233    pub order_id: String,
234    /// New amount for the order
235    pub amount: f64,
236    /// New price for the order
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub price: Option<f64>,
239    /// Whether to only reduce the position
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub post_only: Option<bool>,
242    /// Whether this order only reduces position
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub reduce_only: Option<bool>,
245    /// Advanced order type
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub advanced: Option<String>,
248    /// New trigger price for conditional orders
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub trigger_price: Option<f64>,
251    /// Market maker protection flag
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub mmp: Option<bool>,
254    /// Order validity timestamp
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub valid_until: Option<u64>,
257}
258
259impl EditOrderRequest {
260    /// Create a new edit order request
261    #[must_use]
262    pub fn new(order_id: String, amount: f64) -> Self {
263        Self {
264            order_id,
265            amount,
266            price: None,
267            post_only: None,
268            reduce_only: None,
269            advanced: None,
270            trigger_price: None,
271            mmp: None,
272            valid_until: None,
273        }
274    }
275
276    /// Set new price
277    #[must_use]
278    pub fn with_price(mut self, price: f64) -> Self {
279        self.price = Some(price);
280        self
281    }
282
283    /// Set post-only flag
284    #[must_use]
285    pub fn with_post_only(mut self, post_only: bool) -> Self {
286        self.post_only = Some(post_only);
287        self
288    }
289
290    /// Set reduce-only flag
291    #[must_use]
292    pub fn with_reduce_only(mut self, reduce_only: bool) -> Self {
293        self.reduce_only = Some(reduce_only);
294        self
295    }
296}
297
298/// Trade execution information
299#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
300pub struct TradeExecution {
301    /// Trade ID
302    pub trade_id: String,
303    /// Instrument name
304    pub instrument_name: String,
305    /// Trade direction (buy/sell)
306    pub direction: String,
307    /// Trade amount
308    pub amount: f64,
309    /// Trade price
310    pub price: f64,
311    /// Trade fee
312    pub fee: f64,
313    /// Fee currency
314    pub fee_currency: String,
315    /// Order ID associated with this trade
316    pub order_id: String,
317    /// Order type
318    pub order_type: String,
319    /// Trade timestamp in milliseconds
320    pub timestamp: u64,
321    /// Liquidity type (maker/taker)
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub liquidity: Option<String>,
324    /// Index price at time of trade
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub index_price: Option<f64>,
327    /// Mark price at time of trade
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub mark_price: Option<f64>,
330    /// Profit/loss from the trade
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub profit_loss: Option<f64>,
333}
334
335/// Order information response
336#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
337pub struct OrderInfo {
338    /// Order ID
339    pub order_id: String,
340    /// Instrument name
341    pub instrument_name: String,
342    /// Order direction (buy/sell)
343    pub direction: String,
344    /// Order amount
345    pub amount: f64,
346    /// Filled amount
347    #[serde(default)]
348    pub filled_amount: f64,
349    /// Order price
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub price: Option<f64>,
352    /// Average fill price
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub average_price: Option<f64>,
355    /// Order type
356    pub order_type: String,
357    /// Order state (open, filled, cancelled, etc.)
358    pub order_state: String,
359    /// Time in force
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub time_in_force: Option<String>,
362    /// User label
363    #[serde(default)]
364    pub label: String,
365    /// Creation timestamp in milliseconds
366    pub creation_timestamp: u64,
367    /// Last update timestamp in milliseconds
368    pub last_update_timestamp: u64,
369    /// Whether placed via API
370    #[serde(default)]
371    pub api: bool,
372    /// Whether placed via web interface
373    #[serde(default)]
374    pub web: bool,
375    /// Whether this is a post-only order
376    #[serde(default)]
377    pub post_only: bool,
378    /// Whether this order only reduces position
379    #[serde(default)]
380    pub reduce_only: bool,
381    /// Whether this is a liquidation order
382    #[serde(default)]
383    pub is_liquidation: bool,
384    /// Maximum show amount
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub max_show: Option<f64>,
387    /// Profit/loss on this order
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub profit_loss: Option<f64>,
390    /// USD value
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub usd: Option<f64>,
393    /// Implied volatility (for options)
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub implv: Option<f64>,
396    /// Trigger price for conditional orders
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub trigger_price: Option<f64>,
399    /// Trigger type
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub trigger: Option<String>,
402    /// Whether triggered
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub triggered: Option<bool>,
405    /// Whether replaced
406    #[serde(default)]
407    pub replaced: bool,
408    /// MMP flag
409    #[serde(default)]
410    pub mmp: bool,
411    /// MMP cancelled flag
412    #[serde(default)]
413    pub mmp_cancelled: bool,
414}
415
416/// Order response containing order info and trades
417#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
418pub struct OrderResponse {
419    /// Order information
420    pub order: OrderInfo,
421    /// List of trade executions for the order
422    #[serde(default)]
423    pub trades: Vec<TradeExecution>,
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_order_type_serialization() {
432        let order_type = OrderType::Limit;
433        let json = serde_json::to_string(&order_type).expect("serialize");
434        assert_eq!(json, "\"limit\"");
435
436        let order_type = OrderType::StopLimit;
437        let json = serde_json::to_string(&order_type).expect("serialize");
438        assert_eq!(json, "\"stop_limit\"");
439    }
440
441    #[test]
442    fn test_time_in_force_serialization() {
443        let tif = TimeInForce::GoodTilCancelled;
444        let json = serde_json::to_string(&tif).expect("serialize");
445        assert_eq!(json, "\"good_til_cancelled\"");
446
447        let tif = TimeInForce::ImmediateOrCancel;
448        let json = serde_json::to_string(&tif).expect("serialize");
449        assert_eq!(json, "\"immediate_or_cancel\"");
450    }
451
452    #[test]
453    fn test_order_request_limit() {
454        let request = OrderRequest::limit("BTC-PERPETUAL".to_string(), 100.0, 50000.0)
455            .with_label("test_order".to_string())
456            .with_post_only(true);
457
458        assert_eq!(request.instrument_name, "BTC-PERPETUAL");
459        assert_eq!(request.amount, 100.0);
460        assert_eq!(request.price, Some(50000.0));
461        assert_eq!(request.order_type, Some(OrderType::Limit));
462        assert_eq!(request.label, Some("test_order".to_string()));
463        assert_eq!(request.post_only, Some(true));
464    }
465
466    #[test]
467    fn test_order_request_market() {
468        let request =
469            OrderRequest::market("ETH-PERPETUAL".to_string(), 10.0).with_reduce_only(true);
470
471        assert_eq!(request.instrument_name, "ETH-PERPETUAL");
472        assert_eq!(request.amount, 10.0);
473        assert_eq!(request.price, None);
474        assert_eq!(request.order_type, Some(OrderType::Market));
475        assert_eq!(request.reduce_only, Some(true));
476    }
477
478    #[test]
479    fn test_edit_order_request() {
480        let request = EditOrderRequest::new("order123".to_string(), 200.0).with_price(51000.0);
481
482        assert_eq!(request.order_id, "order123");
483        assert_eq!(request.amount, 200.0);
484        assert_eq!(request.price, Some(51000.0));
485    }
486
487    #[test]
488    fn test_order_type_as_str() {
489        assert_eq!(OrderType::Limit.as_str(), "limit");
490        assert_eq!(OrderType::Market.as_str(), "market");
491        assert_eq!(OrderType::StopLimit.as_str(), "stop_limit");
492        assert_eq!(OrderType::TrailingStop.as_str(), "trailing_stop");
493    }
494
495    #[test]
496    fn test_time_in_force_as_str() {
497        assert_eq!(TimeInForce::GoodTilCancelled.as_str(), "good_til_cancelled");
498        assert_eq!(TimeInForce::FillOrKill.as_str(), "fill_or_kill");
499        assert_eq!(
500            TimeInForce::ImmediateOrCancel.as_str(),
501            "immediate_or_cancel"
502        );
503    }
504}