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