1use super::*;
10use crate::error::Result as DeribitFixResult;
11use crate::message::builder::MessageBuilder;
12use crate::model::types::{ExecType, MsgType};
13use chrono::{DateTime, Utc};
14use deribit_base::{impl_json_debug_pretty, impl_json_display};
15use serde::{Deserialize, Serialize};
16
17#[derive(Clone, PartialEq, Serialize, Deserialize)]
19pub struct ExecutionReport {
20 pub order_id: String,
22 pub cl_ord_id: String,
24 pub orig_cl_ord_id: Option<String>,
26 pub exec_id: String,
28 pub exec_type: ExecType,
30 pub ord_status: OrderStatus,
32 pub symbol: String,
34 pub side: OrderSide,
36 pub leaves_qty: f64,
38 pub cum_qty: f64,
40 pub avg_px: Option<f64>,
42 pub last_px: Option<f64>,
44 pub last_qty: Option<f64>,
46 pub order_qty: f64,
48 pub price: Option<f64>,
50 pub transact_time: DateTime<Utc>,
52 pub text: Option<String>,
54 pub ord_rej_reason: Option<OrderRejectReason>,
56 pub deribit_label: Option<String>,
58 pub secondary_exec_id: Option<String>,
60 pub ord_type: Option<OrderType>,
62 pub commission: Option<f64>,
64 pub security_exchange: Option<String>,
66 pub qty_type: Option<QuantityType>,
68 pub contract_multiplier: Option<f64>,
70 pub display_qty: Option<f64>,
72 pub deribit_adv_order_type: Option<char>,
74 pub volatility: Option<f64>,
76 pub pegged_price: Option<f64>,
78 pub trd_match_id: Option<String>,
80 pub deribit_mm_protection: Option<bool>,
82 pub mmp_group: Option<String>,
84 pub quote_set_id: Option<String>,
86 pub quote_id: Option<String>,
88 pub quote_entry_id: Option<String>,
90 pub exec_inst: Option<String>,
92 pub stop_px: Option<f64>,
94 pub condition_trigger_method: Option<i32>,
96 pub last_liquidity_ind: Option<i32>,
98}
99
100impl ExecutionReport {
101 #[allow(clippy::too_many_arguments)]
103 pub fn new_order(
104 order_id: String,
105 cl_ord_id: String,
106 exec_id: String,
107 symbol: String,
108 side: OrderSide,
109 order_qty: f64,
110 leaves_qty: f64,
111 price: Option<f64>,
112 ) -> Self {
113 Self {
114 order_id,
115 cl_ord_id,
116 orig_cl_ord_id: None,
117 exec_id,
118 exec_type: ExecType::New,
119 ord_status: OrderStatus::New,
120 symbol,
121 side,
122 leaves_qty,
123 cum_qty: 0.0,
124 avg_px: None,
125 last_px: None,
126 last_qty: None,
127 order_qty,
128 price,
129 transact_time: Utc::now(),
130 text: None,
131 ord_rej_reason: None,
132 deribit_label: None,
133 secondary_exec_id: None,
134 ord_type: None,
135 commission: None,
136 security_exchange: None,
137 qty_type: None,
138 contract_multiplier: None,
139 display_qty: None,
140 deribit_adv_order_type: None,
141 volatility: None,
142 pegged_price: None,
143 trd_match_id: None,
144 deribit_mm_protection: None,
145 mmp_group: None,
146 quote_set_id: None,
147 quote_id: None,
148 quote_entry_id: None,
149 exec_inst: None,
150 stop_px: None,
151 condition_trigger_method: None,
152 last_liquidity_ind: None,
153 }
154 }
155
156 #[allow(clippy::too_many_arguments)]
158 pub fn fill(
159 order_id: String,
160 cl_ord_id: String,
161 exec_id: String,
162 symbol: String,
163 side: OrderSide,
164 order_qty: f64,
165 leaves_qty: f64,
166 cum_qty: f64,
167 last_px: f64,
168 last_qty: f64,
169 avg_px: f64,
170 ) -> Self {
171 Self {
172 order_id,
173 cl_ord_id,
174 orig_cl_ord_id: None,
175 exec_id,
176 exec_type: ExecType::Trade,
177 ord_status: if leaves_qty > 0.0 {
178 OrderStatus::PartiallyFilled
179 } else {
180 OrderStatus::Filled
181 },
182 symbol,
183 side,
184 leaves_qty,
185 cum_qty,
186 avg_px: Some(avg_px),
187 last_px: Some(last_px),
188 last_qty: Some(last_qty),
189 order_qty,
190 price: Some(last_px),
191 transact_time: Utc::now(),
192 text: None,
193 ord_rej_reason: None,
194 deribit_label: None,
195 secondary_exec_id: None,
196 ord_type: None,
197 commission: None,
198 security_exchange: None,
199 qty_type: None,
200 contract_multiplier: None,
201 display_qty: None,
202 deribit_adv_order_type: None,
203 volatility: None,
204 pegged_price: None,
205 trd_match_id: None,
206 deribit_mm_protection: None,
207 mmp_group: None,
208 quote_set_id: None,
209 quote_id: None,
210 quote_entry_id: None,
211 exec_inst: None,
212 stop_px: None,
213 condition_trigger_method: None,
214 last_liquidity_ind: None,
215 }
216 }
217
218 pub fn reject(
220 cl_ord_id: String,
221 symbol: String,
222 side: OrderSide,
223 order_qty: f64,
224 reason: OrderRejectReason,
225 text: Option<String>,
226 ) -> Self {
227 Self {
228 order_id: String::new(),
229 cl_ord_id,
230 orig_cl_ord_id: None,
231 exec_id: format!("REJ{}", Utc::now().timestamp_millis()),
232 exec_type: ExecType::Rejected,
233 ord_status: OrderStatus::Rejected,
234 symbol,
235 side,
236 leaves_qty: 0.0,
237 cum_qty: 0.0,
238 avg_px: None,
239 last_px: None,
240 last_qty: None,
241 order_qty,
242 price: None,
243 transact_time: Utc::now(),
244 text,
245 ord_rej_reason: Some(reason),
246 deribit_label: None,
247 secondary_exec_id: None,
248 ord_type: None,
249 commission: None,
250 security_exchange: None,
251 qty_type: None,
252 contract_multiplier: None,
253 display_qty: None,
254 deribit_adv_order_type: None,
255 volatility: None,
256 pegged_price: None,
257 trd_match_id: None,
258 deribit_mm_protection: None,
259 mmp_group: None,
260 quote_set_id: None,
261 quote_id: None,
262 quote_entry_id: None,
263 exec_inst: None,
264 stop_px: None,
265 condition_trigger_method: None,
266 last_liquidity_ind: None,
267 }
268 }
269
270 pub fn with_label(mut self, label: String) -> Self {
272 self.deribit_label = Some(label);
273 self
274 }
275
276 pub fn to_fix_message(
278 &self,
279 sender_comp_id: &str,
280 target_comp_id: &str,
281 msg_seq_num: u32,
282 ) -> DeribitFixResult<String> {
283 let mut builder = MessageBuilder::new()
284 .msg_type(MsgType::ExecutionReport)
285 .sender_comp_id(sender_comp_id.to_string())
286 .target_comp_id(target_comp_id.to_string())
287 .msg_seq_num(msg_seq_num)
288 .sending_time(Utc::now());
289
290 builder = builder
292 .field(37, self.order_id.clone()) .field(11, self.cl_ord_id.clone()) .field(17, self.exec_id.clone()) .field(150, char::from(self.exec_type).to_string()) .field(39, char::from(self.ord_status).to_string()) .field(55, self.symbol.clone()) .field(54, char::from(self.side).to_string()) .field(151, self.leaves_qty.to_string()) .field(14, self.cum_qty.to_string()) .field(38, self.order_qty.to_string()) .field(
303 60,
304 self.transact_time.format("%Y%m%d-%H:%M:%S%.3f").to_string(),
305 ); if let Some(orig_cl_ord_id) = &self.orig_cl_ord_id {
309 builder = builder.field(41, orig_cl_ord_id.clone());
310 }
311
312 if let Some(avg_px) = &self.avg_px {
313 builder = builder.field(6, avg_px.to_string());
314 }
315
316 if let Some(last_px) = &self.last_px {
317 builder = builder.field(31, last_px.to_string());
318 }
319
320 if let Some(last_qty) = &self.last_qty {
321 builder = builder.field(32, last_qty.to_string());
322 }
323
324 if let Some(price) = &self.price {
325 builder = builder.field(44, price.to_string());
326 }
327
328 if let Some(text) = &self.text {
329 builder = builder.field(58, text.clone());
330 }
331
332 if let Some(reason) = &self.ord_rej_reason {
333 builder = builder.field(103, i32::from(*reason).to_string());
334 }
335
336 if let Some(deribit_label) = &self.deribit_label {
337 builder = builder.field(100010, deribit_label.clone());
338 }
339
340 if let Some(secondary_exec_id) = &self.secondary_exec_id {
342 builder = builder.field(527, secondary_exec_id.clone());
343 }
344
345 if let Some(ord_type) = &self.ord_type {
346 builder = builder.field(40, char::from(*ord_type).to_string());
347 }
348
349 if let Some(commission) = &self.commission {
350 builder = builder.field(12, commission.to_string());
351 }
352
353 if let Some(security_exchange) = &self.security_exchange {
354 builder = builder.field(207, security_exchange.clone());
355 }
356
357 if let Some(qty_type) = &self.qty_type {
358 builder = builder.field(854, i32::from(*qty_type).to_string());
359 }
360
361 if let Some(contract_multiplier) = &self.contract_multiplier {
362 builder = builder.field(231, contract_multiplier.to_string());
363 }
364
365 if let Some(display_qty) = &self.display_qty {
366 builder = builder.field(1138, display_qty.to_string());
367 }
368
369 if let Some(deribit_adv_order_type) = &self.deribit_adv_order_type {
370 builder = builder.field(100012, deribit_adv_order_type.to_string());
371 }
372
373 if let Some(volatility) = &self.volatility {
374 builder = builder.field(1188, volatility.to_string());
375 }
376
377 if let Some(pegged_price) = &self.pegged_price {
378 builder = builder.field(839, pegged_price.to_string());
379 }
380
381 if let Some(trd_match_id) = &self.trd_match_id {
382 builder = builder.field(880, trd_match_id.clone());
383 }
384
385 if let Some(deribit_mm_protection) = &self.deribit_mm_protection {
386 builder = builder.field(
387 9008,
388 if *deribit_mm_protection { "Y" } else { "N" }.to_string(),
389 );
390 }
391
392 if let Some(mmp_group) = &self.mmp_group {
393 builder = builder.field(9019, mmp_group.clone());
394 }
395
396 if let Some(quote_set_id) = &self.quote_set_id {
397 builder = builder.field(302, quote_set_id.clone());
398 }
399
400 if let Some(quote_id) = &self.quote_id {
401 builder = builder.field(117, quote_id.clone());
402 }
403
404 if let Some(quote_entry_id) = &self.quote_entry_id {
405 builder = builder.field(299, quote_entry_id.clone());
406 }
407
408 if let Some(exec_inst) = &self.exec_inst {
409 builder = builder.field(18, exec_inst.clone());
410 }
411
412 if let Some(stop_px) = &self.stop_px {
413 builder = builder.field(99, stop_px.to_string());
414 }
415
416 if let Some(condition_trigger_method) = &self.condition_trigger_method {
417 builder = builder.field(5127, condition_trigger_method.to_string());
418 }
419
420 if let Some(last_liquidity_ind) = &self.last_liquidity_ind {
421 builder = builder.field(851, last_liquidity_ind.to_string());
422 }
423
424 Ok(builder.build()?.to_string())
425 }
426}
427
428impl_json_display!(ExecutionReport);
429impl_json_debug_pretty!(ExecutionReport);
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn test_execution_report_new_order() {
437 let report = ExecutionReport::new_order(
438 "ORD123".to_string(),
439 "CLORD123".to_string(),
440 "EXEC123".to_string(),
441 "BTC-PERPETUAL".to_string(),
442 OrderSide::Buy,
443 10.0,
444 10.0,
445 Some(50000.0),
446 );
447
448 assert_eq!(report.order_id, "ORD123");
449 assert_eq!(report.cl_ord_id, "CLORD123");
450 assert_eq!(report.exec_type, ExecType::New);
451 assert_eq!(report.ord_status, OrderStatus::New);
452 assert_eq!(report.symbol, "BTC-PERPETUAL");
453 assert_eq!(report.side, OrderSide::Buy);
454 assert_eq!(report.order_qty, 10.0);
455 assert_eq!(report.leaves_qty, 10.0);
456 assert_eq!(report.cum_qty, 0.0);
457 assert_eq!(report.price, Some(50000.0));
458 }
459
460 #[test]
461 fn test_execution_report_fill() {
462 let report = ExecutionReport::fill(
463 "ORD123".to_string(),
464 "CLORD123".to_string(),
465 "EXEC123".to_string(),
466 "BTC-PERPETUAL".to_string(),
467 OrderSide::Buy,
468 10.0,
469 5.0,
470 5.0,
471 50000.0,
472 5.0,
473 50000.0,
474 );
475
476 assert_eq!(report.exec_type, ExecType::Trade);
477 assert_eq!(report.ord_status, OrderStatus::PartiallyFilled);
478 assert_eq!(report.cum_qty, 5.0);
479 assert_eq!(report.leaves_qty, 5.0);
480 assert_eq!(report.last_px, Some(50000.0));
481 assert_eq!(report.last_qty, Some(5.0));
482 assert_eq!(report.avg_px, Some(50000.0));
483 }
484
485 #[test]
486 fn test_execution_report_fill_complete() {
487 let report = ExecutionReport::fill(
488 "ORD123".to_string(),
489 "CLORD123".to_string(),
490 "EXEC123".to_string(),
491 "BTC-PERPETUAL".to_string(),
492 OrderSide::Sell,
493 10.0,
494 0.0, 10.0,
496 49500.0,
497 10.0,
498 49500.0,
499 );
500
501 assert_eq!(report.exec_type, ExecType::Trade);
502 assert_eq!(report.ord_status, OrderStatus::Filled);
503 assert_eq!(report.cum_qty, 10.0);
504 assert_eq!(report.leaves_qty, 0.0);
505 }
506
507 #[test]
508 fn test_execution_report_reject() {
509 let report = ExecutionReport::reject(
510 "CLORD123".to_string(),
511 "BTC-PERPETUAL".to_string(),
512 OrderSide::Buy,
513 10.0,
514 OrderRejectReason::OrderExceedsLimit,
515 Some("Insufficient margin".to_string()),
516 );
517
518 assert_eq!(report.exec_type, ExecType::Rejected);
519 assert_eq!(report.ord_status, OrderStatus::Rejected);
520 assert_eq!(
521 report.ord_rej_reason,
522 Some(OrderRejectReason::OrderExceedsLimit)
523 );
524 assert_eq!(report.text, Some("Insufficient margin".to_string()));
525 assert_eq!(report.leaves_qty, 0.0);
526 assert_eq!(report.cum_qty, 0.0);
527 }
528
529 #[test]
530 fn test_execution_report_with_label() {
531 let report = ExecutionReport::new_order(
532 "ORD123".to_string(),
533 "CLORD123".to_string(),
534 "EXEC123".to_string(),
535 "BTC-PERPETUAL".to_string(),
536 OrderSide::Buy,
537 10.0,
538 10.0,
539 Some(50000.0),
540 )
541 .with_label("my-strategy".to_string());
542
543 assert_eq!(report.deribit_label, Some("my-strategy".to_string()));
544 }
545
546 #[test]
547 fn test_execution_report_to_fix_message() {
548 let report = ExecutionReport::new_order(
549 "ORD123".to_string(),
550 "CLORD123".to_string(),
551 "EXEC123".to_string(),
552 "BTC-PERPETUAL".to_string(),
553 OrderSide::Buy,
554 10.0,
555 10.0,
556 Some(50000.0),
557 );
558
559 let fix_message = report.to_fix_message("SENDER", "TARGET", 1).unwrap();
560
561 assert!(fix_message.contains("35=8")); assert!(fix_message.contains("37=ORD123")); assert!(fix_message.contains("11=CLORD123")); assert!(fix_message.contains("17=EXEC123")); assert!(fix_message.contains("150=0")); assert!(fix_message.contains("39=0")); assert!(fix_message.contains("55=BTC-PERPETUAL")); assert!(fix_message.contains("54=1")); assert!(fix_message.contains("151=10")); assert!(fix_message.contains("14=0")); assert!(fix_message.contains("38=10")); assert!(fix_message.contains("44=50000")); }
575}