Skip to main content

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