1use crate::error::Result;
15use crate::error::{DeribitFixError, Result as DeribitFixResult};
16use crate::message::MessageBuilder;
17use crate::model::message::FixMessage;
18use crate::model::types::MsgType;
19
20use crate::model::position::{Direction, Position};
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum PosReqType {
26 Positions,
28 Trades,
30 Exercises,
32 Assignments,
34}
35
36impl From<PosReqType> for i32 {
37 fn from(value: PosReqType) -> Self {
38 match value {
39 PosReqType::Positions => 0,
40 PosReqType::Trades => 1,
41 PosReqType::Exercises => 2,
42 PosReqType::Assignments => 3,
43 }
44 }
45}
46
47impl TryFrom<i32> for PosReqType {
48 type Error = DeribitFixError;
49
50 fn try_from(value: i32) -> Result<Self> {
51 match value {
52 0 => Ok(PosReqType::Positions),
53 1 => Ok(PosReqType::Trades),
54 2 => Ok(PosReqType::Exercises),
55 3 => Ok(PosReqType::Assignments),
56 _ => Err(DeribitFixError::MessageParsing(format!(
57 "Invalid PosReqType: {}",
58 value
59 ))),
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum SubscriptionRequestType {
67 Snapshot,
69 SnapshotPlusUpdates,
71 DisablePreviousSnapshotPlusUpdates,
73}
74
75impl From<SubscriptionRequestType> for i32 {
76 fn from(value: SubscriptionRequestType) -> Self {
77 match value {
78 SubscriptionRequestType::Snapshot => 0,
79 SubscriptionRequestType::SnapshotPlusUpdates => 1,
80 SubscriptionRequestType::DisablePreviousSnapshotPlusUpdates => 2,
81 }
82 }
83}
84
85impl TryFrom<i32> for SubscriptionRequestType {
86 type Error = DeribitFixError;
87
88 fn try_from(value: i32) -> Result<Self> {
89 match value {
90 0 => Ok(SubscriptionRequestType::Snapshot),
91 1 => Ok(SubscriptionRequestType::SnapshotPlusUpdates),
92 2 => Ok(SubscriptionRequestType::DisablePreviousSnapshotPlusUpdates),
93 _ => Err(DeribitFixError::MessageParsing(format!(
94 "Invalid SubscriptionRequestType: {}",
95 value
96 ))),
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct RequestForPositions {
104 pub pos_req_id: String,
106 pub pos_req_type: PosReqType,
108 pub subscription_request_type: Option<SubscriptionRequestType>,
110 pub clearing_business_date: Option<String>,
112 pub symbols: Vec<String>,
114}
115
116impl RequestForPositions {
117 pub fn all_positions(pos_req_id: String) -> Self {
119 Self {
120 pos_req_id,
121 pos_req_type: PosReqType::Positions,
122 subscription_request_type: Some(SubscriptionRequestType::Snapshot),
123 clearing_business_date: None,
124 symbols: Vec::new(),
125 }
126 }
127
128 pub fn positions_with_updates(pos_req_id: String) -> Self {
130 Self {
131 pos_req_id,
132 pos_req_type: PosReqType::Positions,
133 subscription_request_type: Some(SubscriptionRequestType::SnapshotPlusUpdates),
134 clearing_business_date: None,
135 symbols: Vec::new(),
136 }
137 }
138
139 pub fn with_symbols(mut self, symbols: Vec<String>) -> Self {
141 self.symbols = symbols;
142 self
143 }
144
145 pub fn with_clearing_date(mut self, date: String) -> Self {
147 self.clearing_business_date = Some(date);
148 self
149 }
150
151 pub fn to_fix_message(
153 &self,
154 sender_comp_id: String,
155 target_comp_id: String,
156 msg_seq_num: u32,
157 ) -> DeribitFixResult<FixMessage> {
158 let mut builder = MessageBuilder::new()
159 .msg_type(MsgType::RequestForPositions)
160 .sender_comp_id(sender_comp_id)
161 .target_comp_id(target_comp_id)
162 .msg_seq_num(msg_seq_num)
163 .field(710, self.pos_req_id.clone()) .field(724, i32::from(self.pos_req_type).to_string()); if let Some(subscription_type) = self.subscription_request_type {
168 builder = builder.field(263, i32::from(subscription_type).to_string());
169 }
170
171 if let Some(ref date) = self.clearing_business_date {
173 builder = builder.field(715, date.clone());
174 }
175
176 if !self.symbols.is_empty() {
178 builder = builder.field(146, self.symbols.len().to_string()); for symbol in &self.symbols {
180 builder = builder.field(55, symbol.clone()); }
182 }
183
184 builder.build()
185 }
186}
187
188pub struct PositionReport;
190
191impl PositionReport {
192 pub fn try_from_fix_message(message: &FixMessage) -> Result<Position> {
235 let get_f64 = |tag| message.get_field(tag).and_then(|s| s.parse::<f64>().ok());
236 let get_string = |tag| message.get_field(tag).map(|s| s.to_string());
237
238 let instrument_name = get_string(55).ok_or_else(|| {
239 DeribitFixError::Generic("Missing instrument name (tag 55)".to_string())
240 })?;
241 let long_qty = get_f64(704).unwrap_or(0.0);
242 let short_qty = get_f64(705).unwrap_or(0.0);
243 let size = long_qty - short_qty;
244 let direction = if size > 0.0 {
245 Direction::Buy
246 } else {
247 Direction::Sell
248 };
249 let average_price = get_f64(730).unwrap_or(0.0);
250
251 Ok(Position {
252 instrument_name,
253 size,
254 direction,
255 average_price,
256 average_price_usd: None,
257 delta: get_f64(811), estimated_liquidation_price: get_f64(100088), floating_profit_loss: get_f64(707), floating_profit_loss_usd: None,
261 gamma: get_f64(812), index_price: get_f64(731), initial_margin: get_f64(899), interest_value: None,
265 kind: get_string(461), leverage: None,
267 maintenance_margin: get_f64(898), mark_price: get_f64(732), open_orders_margin: None,
270 realized_funding: None,
271 realized_profit_loss: get_f64(706), settlement_price: get_f64(730), size_currency: get_f64(100089), theta: get_f64(813), total_profit_loss: get_f64(708), vega: get_f64(814), unrealized_profit_loss: get_f64(707), })
279 }
280
281 pub fn from_deribit_position(
326 position: &Position,
327 sender_comp_id: String,
328 target_comp_id: String,
329 msg_seq_num: u32,
330 ) -> Result<String> {
331 let msg = MessageBuilder::new()
332 .msg_type(MsgType::PositionReport)
333 .sender_comp_id(sender_comp_id)
334 .target_comp_id(target_comp_id)
335 .msg_seq_num(msg_seq_num);
336
337 let msg = msg.field(55, position.instrument_name.clone()); let msg = msg.field(730, position.average_price.to_string()); let msg = match position.direction {
343 Direction::Buy => msg.field(704, position.size.to_string()), Direction::Sell => msg.field(705, position.size.abs().to_string()), };
346
347 let msg = if let Some(realized_pnl) = position.realized_profit_loss {
349 msg.field(706, realized_pnl.to_string())
350 } else {
351 msg
352 };
353
354 let msg = if let Some(floating_pnl) = position.floating_profit_loss {
355 msg.field(707, floating_pnl.to_string())
356 } else {
357 msg
358 };
359
360 let msg = if let Some(total_pnl) = position.total_profit_loss {
361 msg.field(708, total_pnl.to_string())
362 } else {
363 msg
364 };
365
366 let msg = if let Some(delta) = position.delta {
368 msg.field(811, delta.to_string())
369 } else {
370 msg
371 };
372
373 let msg = if let Some(gamma) = position.gamma {
374 msg.field(812, gamma.to_string())
375 } else {
376 msg
377 };
378
379 let msg = if let Some(theta) = position.theta {
380 msg.field(813, theta.to_string())
381 } else {
382 msg
383 };
384
385 let msg = if let Some(vega) = position.vega {
386 msg.field(814, vega.to_string())
387 } else {
388 msg
389 };
390
391 let msg = if let Some(index_price) = position.index_price {
393 msg.field(731, index_price.to_string())
394 } else {
395 msg
396 };
397
398 let msg = if let Some(mark_price) = position.mark_price {
399 msg.field(732, mark_price.to_string())
400 } else {
401 msg
402 };
403
404 let msg = if let Some(initial_margin) = position.initial_margin {
405 msg.field(899, initial_margin.to_string())
406 } else {
407 msg
408 };
409
410 let msg = if let Some(maintenance_margin) = position.maintenance_margin {
411 msg.field(898, maintenance_margin.to_string())
412 } else {
413 msg
414 };
415
416 let msg = msg.field(979, "FMTM".to_string()); let msg = if let Some(liquidation_price) = position.estimated_liquidation_price {
420 msg.field(100088, liquidation_price.to_string()) } else {
422 msg
423 };
424
425 let msg = if let Some(size_currency) = position.size_currency {
426 msg.field(100089, size_currency.to_string()) } else {
428 msg
429 };
430
431 Ok(msg.build()?.to_string())
432 }
433}
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::model::message::FixMessage;
438
439 #[test]
440 fn test_pos_req_type_conversion() {
441 assert_eq!(i32::from(PosReqType::Positions), 0);
442 assert_eq!(i32::from(PosReqType::Trades), 1);
443
444 assert_eq!(PosReqType::try_from(0).unwrap(), PosReqType::Positions);
445 assert_eq!(PosReqType::try_from(1).unwrap(), PosReqType::Trades);
446
447 assert!(PosReqType::try_from(99).is_err());
448 }
449
450 #[test]
451 fn test_subscription_request_type_conversion() {
452 assert_eq!(i32::from(SubscriptionRequestType::Snapshot), 0);
453 assert_eq!(i32::from(SubscriptionRequestType::SnapshotPlusUpdates), 1);
454
455 assert_eq!(
456 SubscriptionRequestType::try_from(0).unwrap(),
457 SubscriptionRequestType::Snapshot
458 );
459 assert_eq!(
460 SubscriptionRequestType::try_from(1).unwrap(),
461 SubscriptionRequestType::SnapshotPlusUpdates
462 );
463
464 assert!(SubscriptionRequestType::try_from(99).is_err());
465 }
466
467 #[test]
468 fn test_request_for_positions_creation() {
469 let request = RequestForPositions::all_positions("POS_123".to_string());
470 assert_eq!(request.pos_req_id, "POS_123");
471 assert_eq!(request.pos_req_type, PosReqType::Positions);
472 assert_eq!(
473 request.subscription_request_type,
474 Some(SubscriptionRequestType::Snapshot)
475 );
476 }
477
478 #[test]
479 fn test_request_for_positions_with_symbols() {
480 let request = RequestForPositions::all_positions("POS_123".to_string()).with_symbols(vec![
481 "BTC-PERPETUAL".to_string(),
482 "ETH-PERPETUAL".to_string(),
483 ]);
484
485 assert_eq!(request.symbols.len(), 2);
486 assert!(request.symbols.contains(&"BTC-PERPETUAL".to_string()));
487 }
488
489 #[test]
490 fn test_request_for_positions_to_fix_message() {
491 let request = RequestForPositions::all_positions("POS_123".to_string());
492 let fix_message = request
493 .to_fix_message("SENDER".to_string(), "TARGET".to_string(), 1)
494 .unwrap();
495
496 assert_eq!(fix_message.get_field(35), Some(&"AN".to_string())); assert_eq!(fix_message.get_field(710), Some(&"POS_123".to_string())); assert_eq!(fix_message.get_field(724), Some(&"0".to_string())); assert_eq!(fix_message.get_field(263), Some(&"0".to_string())); }
502
503 #[test]
504 fn test_position_report_try_from_fix_message() {
505 let mut fix_message = FixMessage::new();
507 fix_message.set_field(55, "BTC-PERPETUAL".to_string()); fix_message.set_field(704, "1.5".to_string()); fix_message.set_field(705, "0.0".to_string()); fix_message.set_field(730, "50000.0".to_string()); fix_message.set_field(707, "100.0".to_string()); fix_message.set_field(706, "50.0".to_string()); let position = PositionReport::try_from_fix_message(&fix_message).unwrap();
515
516 assert_eq!(position.instrument_name, "BTC-PERPETUAL");
517 assert_eq!(position.size, 1.5);
518 assert_eq!(position.average_price, 50000.0);
519 assert!(matches!(position.direction, Direction::Buy));
520 assert_eq!(position.floating_profit_loss, Some(100.0));
521 assert_eq!(position.realized_profit_loss, Some(50.0));
522 }
523
524 #[test]
525 fn test_position_report_from_deribit_position() {
526 let position = Position {
528 instrument_name: "ETH-PERPETUAL".to_string(),
529 size: 2.0,
530 direction: Direction::Buy,
531 average_price: 3500.0,
532 average_price_usd: None,
533 delta: Some(0.5),
534 estimated_liquidation_price: None,
535 floating_profit_loss: Some(150.0),
536 floating_profit_loss_usd: None,
537 gamma: Some(0.001),
538 index_price: Some(3520.0),
539 initial_margin: Some(100.0),
540 interest_value: None,
541 kind: Some("future".to_string()),
542 leverage: None,
543 maintenance_margin: Some(50.0),
544 mark_price: Some(3510.0),
545 open_orders_margin: None,
546 realized_funding: None,
547 realized_profit_loss: Some(50.0),
548 settlement_price: Some(3500.0),
549 size_currency: None,
550 theta: Some(-0.1),
551 total_profit_loss: Some(200.0),
552 vega: Some(0.05),
553 unrealized_profit_loss: Some(150.0),
554 };
555
556 let fix_message = PositionReport::from_deribit_position(
557 &position,
558 "SENDER".to_string(),
559 "TARGET".to_string(),
560 1,
561 )
562 .unwrap();
563
564 assert!(fix_message.contains("55=ETH-PERPETUAL")); assert!(fix_message.contains("704=2")); assert!(fix_message.contains("730=3500")); }
569
570 #[test]
571 fn test_position_direction_sell() {
572 let mut fix_message = FixMessage::new();
574 fix_message.set_field(55, "BTC-PERPETUAL".to_string()); fix_message.set_field(704, "0.0".to_string()); fix_message.set_field(705, "1.0".to_string()); fix_message.set_field(730, "45000.0".to_string()); let position = PositionReport::try_from_fix_message(&fix_message).unwrap();
580
581 assert_eq!(position.instrument_name, "BTC-PERPETUAL");
582 assert_eq!(position.size, -1.0); assert!(matches!(position.direction, Direction::Sell));
584 assert_eq!(position.average_price, 45000.0);
585 }
586
587 #[test]
588 fn test_position_report_with_deribit_custom_tags() {
589 let mut fix_message = FixMessage::new();
591 fix_message.set_field(55, "ETH-PERPETUAL".to_string()); fix_message.set_field(704, "2.5".to_string()); fix_message.set_field(730, "3500.0".to_string()); fix_message.set_field(100088, "3000.0".to_string()); fix_message.set_field(100089, "8750.0".to_string()); let position = PositionReport::try_from_fix_message(&fix_message).unwrap();
598
599 assert_eq!(position.instrument_name, "ETH-PERPETUAL");
600 assert_eq!(position.size, 2.5);
601 assert_eq!(position.estimated_liquidation_price, Some(3000.0));
602 assert_eq!(position.size_currency, Some(8750.0));
603 }
604
605 #[test]
606 fn test_position_report_emits_deribit_custom_tags() {
607 let position = Position {
609 instrument_name: "BTC-PERPETUAL".to_string(),
610 size: 1.0,
611 direction: Direction::Buy,
612 average_price: 50000.0,
613 average_price_usd: None,
614 delta: None,
615 estimated_liquidation_price: Some(45000.0),
616 floating_profit_loss: None,
617 floating_profit_loss_usd: None,
618 gamma: None,
619 index_price: None,
620 initial_margin: None,
621 interest_value: None,
622 kind: None,
623 leverage: None,
624 maintenance_margin: None,
625 mark_price: None,
626 open_orders_margin: None,
627 realized_funding: None,
628 realized_profit_loss: None,
629 settlement_price: None,
630 size_currency: Some(50000.0),
631 theta: None,
632 total_profit_loss: None,
633 vega: None,
634 unrealized_profit_loss: None,
635 };
636
637 let fix_message = PositionReport::from_deribit_position(
638 &position,
639 "SENDER".to_string(),
640 "TARGET".to_string(),
641 1,
642 )
643 .unwrap();
644
645 assert!(fix_message.contains("100088=45000")); assert!(fix_message.contains("100089=50000")); }
649}