deribit_fix/message/orders/
cancel_replace_request.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 10/8/25
5******************************************************************************/
6
7//! Order Cancel/Replace Request FIX Message Implementation
8
9use super::*;
10use crate::error::Result as DeribitFixResult;
11use crate::message::builder::MessageBuilder;
12use crate::model::types::MsgType;
13use chrono::{DateTime, Utc};
14use deribit_base::{impl_json_debug_pretty, impl_json_display};
15use serde::{Deserialize, Serialize};
16
17/// Order Cancel/Replace Request message (MsgType = 'G')
18#[derive(Clone, PartialEq, Serialize, Deserialize)]
19pub struct OrderCancelReplaceRequest {
20    /// Original client order ID
21    pub orig_cl_ord_id: String,
22    /// New client order ID
23    pub cl_ord_id: String,
24    /// Instrument symbol
25    pub symbol: String,
26    /// Side of order
27    pub side: OrderSide,
28    /// Transaction time
29    pub transact_time: DateTime<Utc>,
30    /// New order quantity
31    pub order_qty: Option<f64>,
32    /// New price
33    pub price: Option<f64>,
34    /// Order type
35    pub ord_type: Option<OrderType>,
36    /// Time in force
37    pub time_in_force: Option<TimeInForce>,
38    /// Stop price
39    pub stop_px: Option<f64>,
40    /// Display quantity
41    pub display_qty: Option<f64>,
42    /// Quantity type
43    pub qty_type: Option<QuantityType>,
44    /// Custom label
45    pub deribit_label: Option<String>,
46    /// Market Maker Protection flag
47    pub deribit_mm_protection: Option<bool>,
48}
49
50impl OrderCancelReplaceRequest {
51    /// Create a new cancel/replace request
52    pub fn new(orig_cl_ord_id: String, cl_ord_id: String, symbol: String, side: OrderSide) -> Self {
53        Self {
54            orig_cl_ord_id,
55            cl_ord_id,
56            symbol,
57            side,
58            transact_time: Utc::now(),
59            order_qty: None,
60            price: None,
61            ord_type: None,
62            time_in_force: None,
63            stop_px: None,
64            display_qty: None,
65            qty_type: None,
66            deribit_label: None,
67            deribit_mm_protection: None,
68        }
69    }
70
71    /// Set new order quantity
72    pub fn with_qty(mut self, qty: f64) -> Self {
73        self.order_qty = Some(qty);
74        self
75    }
76
77    /// Set new price
78    pub fn with_price(mut self, price: f64) -> Self {
79        self.price = Some(price);
80        self
81    }
82
83    /// Set order type
84    pub fn with_order_type(mut self, ord_type: OrderType) -> Self {
85        self.ord_type = Some(ord_type);
86        self
87    }
88
89    /// Set time in force
90    pub fn with_time_in_force(mut self, tif: TimeInForce) -> Self {
91        self.time_in_force = Some(tif);
92        self
93    }
94
95    /// Set stop price
96    pub fn with_stop_price(mut self, stop_px: f64) -> Self {
97        self.stop_px = Some(stop_px);
98        self
99    }
100
101    /// Set display quantity
102    pub fn with_display_qty(mut self, display_qty: f64) -> Self {
103        self.display_qty = Some(display_qty);
104        self
105    }
106
107    /// Set quantity type
108    pub fn with_qty_type(mut self, qty_type: QuantityType) -> Self {
109        self.qty_type = Some(qty_type);
110        self
111    }
112
113    /// Set custom label
114    pub fn with_label(mut self, label: String) -> Self {
115        self.deribit_label = Some(label);
116        self
117    }
118
119    /// Set Market Maker Protection flag
120    pub fn with_mmp(mut self, enabled: bool) -> Self {
121        self.deribit_mm_protection = Some(enabled);
122        self
123    }
124
125    /// Convert to FIX message
126    pub fn to_fix_message(
127        &self,
128        sender_comp_id: &str,
129        target_comp_id: &str,
130        msg_seq_num: u32,
131    ) -> DeribitFixResult<String> {
132        let mut builder = MessageBuilder::new()
133            .msg_type(MsgType::OrderCancelReplaceRequest)
134            .sender_comp_id(sender_comp_id.to_string())
135            .target_comp_id(target_comp_id.to_string())
136            .msg_seq_num(msg_seq_num)
137            .sending_time(Utc::now());
138
139        // Required fields
140        builder = builder
141            .field(41, self.orig_cl_ord_id.clone()) // OrigClOrdID
142            .field(11, self.cl_ord_id.clone()) // ClOrdID
143            .field(55, self.symbol.clone()) // Symbol
144            .field(54, char::from(self.side).to_string()) // Side
145            .field(
146                60,
147                self.transact_time.format("%Y%m%d-%H:%M:%S%.3f").to_string(),
148            ); // TransactTime
149
150        // Optional fields
151        if let Some(order_qty) = &self.order_qty {
152            builder = builder.field(38, order_qty.to_string());
153        }
154
155        if let Some(price) = &self.price {
156            builder = builder.field(44, price.to_string());
157        }
158
159        if let Some(ord_type) = &self.ord_type {
160            builder = builder.field(40, char::from(*ord_type).to_string());
161        }
162
163        if let Some(time_in_force) = &self.time_in_force {
164            builder = builder.field(59, char::from(*time_in_force).to_string());
165        }
166
167        if let Some(stop_px) = &self.stop_px {
168            builder = builder.field(99, stop_px.to_string());
169        }
170
171        if let Some(display_qty) = &self.display_qty {
172            builder = builder.field(1138, display_qty.to_string());
173        }
174
175        if let Some(qty_type) = &self.qty_type {
176            builder = builder.field(854, i32::from(*qty_type).to_string());
177        }
178
179        if let Some(deribit_label) = &self.deribit_label {
180            builder = builder.field(100010, deribit_label.clone());
181        }
182
183        if let Some(deribit_mm_protection) = &self.deribit_mm_protection {
184            builder = builder.field(
185                9008,
186                if *deribit_mm_protection { "Y" } else { "N" }.to_string(),
187            );
188        }
189
190        Ok(builder.build()?.to_string())
191    }
192}
193
194impl_json_display!(OrderCancelReplaceRequest);
195impl_json_debug_pretty!(OrderCancelReplaceRequest);
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_cancel_replace_request_creation() {
203        let request = OrderCancelReplaceRequest::new(
204            "ORIG123".to_string(),
205            "NEW123".to_string(),
206            "BTC-PERPETUAL".to_string(),
207            OrderSide::Buy,
208        );
209
210        assert_eq!(request.orig_cl_ord_id, "ORIG123");
211        assert_eq!(request.cl_ord_id, "NEW123");
212        assert_eq!(request.symbol, "BTC-PERPETUAL");
213        assert_eq!(request.side, OrderSide::Buy);
214        assert!(request.order_qty.is_none());
215        assert!(request.price.is_none());
216        assert!(request.ord_type.is_none());
217    }
218
219    #[test]
220    fn test_cancel_replace_request_with_quantity_and_price() {
221        let request = OrderCancelReplaceRequest::new(
222            "ORIG123".to_string(),
223            "NEW123".to_string(),
224            "BTC-PERPETUAL".to_string(),
225            OrderSide::Sell,
226        )
227        .with_qty(15.0)
228        .with_price(48000.0);
229
230        assert_eq!(request.order_qty, Some(15.0));
231        assert_eq!(request.price, Some(48000.0));
232    }
233
234    #[test]
235    fn test_cancel_replace_request_with_all_options() {
236        let request = OrderCancelReplaceRequest::new(
237            "ORIG123".to_string(),
238            "NEW123".to_string(),
239            "ETH-PERPETUAL".to_string(),
240            OrderSide::Buy,
241        )
242        .with_qty(20.0)
243        .with_price(3200.0)
244        .with_order_type(OrderType::Limit)
245        .with_time_in_force(TimeInForce::GoodTillCancelled)
246        .with_stop_price(3100.0)
247        .with_display_qty(10.0)
248        .with_qty_type(QuantityType::Contracts)
249        .with_label("updated-order".to_string())
250        .with_mmp(true);
251
252        assert_eq!(request.order_qty, Some(20.0));
253        assert_eq!(request.price, Some(3200.0));
254        assert_eq!(request.ord_type, Some(OrderType::Limit));
255        assert_eq!(request.time_in_force, Some(TimeInForce::GoodTillCancelled));
256        assert_eq!(request.stop_px, Some(3100.0));
257        assert_eq!(request.display_qty, Some(10.0));
258        assert_eq!(request.qty_type, Some(QuantityType::Contracts));
259        assert_eq!(request.deribit_label, Some("updated-order".to_string()));
260        assert_eq!(request.deribit_mm_protection, Some(true));
261    }
262
263    #[test]
264    fn test_cancel_replace_request_to_fix_message() {
265        let request = OrderCancelReplaceRequest::new(
266            "ORIG123".to_string(),
267            "NEW123".to_string(),
268            "BTC-PERPETUAL".to_string(),
269            OrderSide::Buy,
270        )
271        .with_qty(10.0)
272        .with_price(51000.0)
273        .with_order_type(OrderType::Limit);
274
275        let fix_message = request.to_fix_message("SENDER", "TARGET", 1).unwrap();
276
277        // Check that the message contains required fields
278        assert!(fix_message.contains("35=G")); // MsgType
279        assert!(fix_message.contains("41=ORIG123")); // OrigClOrdID
280        assert!(fix_message.contains("11=NEW123")); // ClOrdID
281        assert!(fix_message.contains("55=BTC-PERPETUAL")); // Symbol
282        assert!(fix_message.contains("54=1")); // Side=Buy
283        assert!(fix_message.contains("38=10")); // OrderQty
284        assert!(fix_message.contains("44=51000")); // Price
285        assert!(fix_message.contains("40=2")); // OrdType=Limit
286    }
287
288    #[test]
289    fn test_cancel_replace_request_minimal_fix_message() {
290        let request = OrderCancelReplaceRequest::new(
291            "ORIG456".to_string(),
292            "NEW456".to_string(),
293            "ETH-PERPETUAL".to_string(),
294            OrderSide::Sell,
295        );
296
297        let fix_message = request.to_fix_message("SENDER", "TARGET", 2).unwrap();
298
299        // Check required fields only
300        assert!(fix_message.contains("35=G")); // MsgType
301        assert!(fix_message.contains("41=ORIG456")); // OrigClOrdID
302        assert!(fix_message.contains("11=NEW456")); // ClOrdID
303        assert!(fix_message.contains("55=ETH-PERPETUAL")); // Symbol
304        assert!(fix_message.contains("54=2")); // Side=Sell
305
306        // Check optional fields are not present when not set
307        assert!(!fix_message.contains("38=")); // OrderQty not set
308        assert!(!fix_message.contains("44=")); // Price not set
309        assert!(!fix_message.contains("40=")); // OrdType not set
310    }
311
312    #[test]
313    fn test_cancel_replace_request_with_label_and_mmp() {
314        let request = OrderCancelReplaceRequest::new(
315            "ORIG789".to_string(),
316            "NEW789".to_string(),
317            "BTC-PERPETUAL".to_string(),
318            OrderSide::Buy,
319        )
320        .with_label("strategy-v2".to_string())
321        .with_mmp(false);
322
323        let fix_message = request.to_fix_message("SENDER", "TARGET", 3).unwrap();
324
325        assert!(fix_message.contains("100010=strategy-v2")); // Custom label
326        assert!(fix_message.contains("9008=N")); // MMP disabled
327    }
328}