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 deribit_base::model::position::Direction;
21use deribit_base::prelude::Position;
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub enum PosReqType {
27 Positions,
29 Trades,
31 Exercises,
33 Assignments,
35}
36
37impl From<PosReqType> for i32 {
38 fn from(value: PosReqType) -> Self {
39 match value {
40 PosReqType::Positions => 0,
41 PosReqType::Trades => 1,
42 PosReqType::Exercises => 2,
43 PosReqType::Assignments => 3,
44 }
45 }
46}
47
48impl TryFrom<i32> for PosReqType {
49 type Error = DeribitFixError;
50
51 fn try_from(value: i32) -> Result<Self> {
52 match value {
53 0 => Ok(PosReqType::Positions),
54 1 => Ok(PosReqType::Trades),
55 2 => Ok(PosReqType::Exercises),
56 3 => Ok(PosReqType::Assignments),
57 _ => Err(DeribitFixError::MessageParsing(format!(
58 "Invalid PosReqType: {}",
59 value
60 ))),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67pub enum SubscriptionRequestType {
68 Snapshot,
70 SnapshotPlusUpdates,
72 DisablePreviousSnapshotPlusUpdates,
74}
75
76impl From<SubscriptionRequestType> for i32 {
77 fn from(value: SubscriptionRequestType) -> Self {
78 match value {
79 SubscriptionRequestType::Snapshot => 0,
80 SubscriptionRequestType::SnapshotPlusUpdates => 1,
81 SubscriptionRequestType::DisablePreviousSnapshotPlusUpdates => 2,
82 }
83 }
84}
85
86impl TryFrom<i32> for SubscriptionRequestType {
87 type Error = DeribitFixError;
88
89 fn try_from(value: i32) -> Result<Self> {
90 match value {
91 0 => Ok(SubscriptionRequestType::Snapshot),
92 1 => Ok(SubscriptionRequestType::SnapshotPlusUpdates),
93 2 => Ok(SubscriptionRequestType::DisablePreviousSnapshotPlusUpdates),
94 _ => Err(DeribitFixError::MessageParsing(format!(
95 "Invalid SubscriptionRequestType: {}",
96 value
97 ))),
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct RequestForPositions {
105 pub pos_req_id: String,
107 pub pos_req_type: PosReqType,
109 pub subscription_request_type: Option<SubscriptionRequestType>,
111 pub clearing_business_date: Option<String>,
113 pub symbols: Vec<String>,
115}
116
117impl RequestForPositions {
118 pub fn all_positions(pos_req_id: String) -> Self {
120 Self {
121 pos_req_id,
122 pos_req_type: PosReqType::Positions,
123 subscription_request_type: Some(SubscriptionRequestType::Snapshot),
124 clearing_business_date: None,
125 symbols: Vec::new(),
126 }
127 }
128
129 pub fn positions_with_updates(pos_req_id: String) -> Self {
131 Self {
132 pos_req_id,
133 pos_req_type: PosReqType::Positions,
134 subscription_request_type: Some(SubscriptionRequestType::SnapshotPlusUpdates),
135 clearing_business_date: None,
136 symbols: Vec::new(),
137 }
138 }
139
140 pub fn with_symbols(mut self, symbols: Vec<String>) -> Self {
142 self.symbols = symbols;
143 self
144 }
145
146 pub fn with_clearing_date(mut self, date: String) -> Self {
148 self.clearing_business_date = Some(date);
149 self
150 }
151
152 pub fn to_fix_message(
154 &self,
155 sender_comp_id: String,
156 target_comp_id: String,
157 msg_seq_num: u32,
158 ) -> DeribitFixResult<FixMessage> {
159 let mut builder = MessageBuilder::new()
160 .msg_type(MsgType::RequestForPositions)
161 .sender_comp_id(sender_comp_id)
162 .target_comp_id(target_comp_id)
163 .msg_seq_num(msg_seq_num)
164 .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 {
169 builder = builder.field(263, i32::from(subscription_type).to_string());
170 }
171
172 if let Some(ref date) = self.clearing_business_date {
174 builder = builder.field(715, date.clone());
175 }
176
177 if !self.symbols.is_empty() {
179 builder = builder.field(146, self.symbols.len().to_string()); for symbol in &self.symbols {
181 builder = builder.field(55, symbol.clone()); }
183 }
184
185 builder.build()
186 }
187}
188
189pub struct PositionReport;
191
192impl PositionReport {
193 pub fn try_from_fix_message(message: &FixMessage) -> Result<Position> {
234 let get_f64 = |tag| message.get_field(tag).and_then(|s| s.parse::<f64>().ok());
235 let get_string = |tag| message.get_field(tag).map(|s| s.to_string());
236
237 let instrument_name = get_string(55).ok_or_else(|| {
238 DeribitFixError::Generic("Missing instrument name (tag 55)".to_string())
239 })?;
240 let long_qty = get_f64(704).unwrap_or(0.0);
241 let short_qty = get_f64(705).unwrap_or(0.0);
242 let size = long_qty - short_qty;
243 let direction = if size > 0.0 {
244 Direction::Buy
245 } else {
246 Direction::Sell
247 };
248 let average_price = get_f64(730).unwrap_or(0.0);
249
250 Ok(Position {
251 instrument_name,
252 size,
253 direction,
254 average_price,
255 average_price_usd: None,
256 delta: get_f64(811), estimated_liquidation_price: None,
258 floating_profit_loss: get_f64(707), floating_profit_loss_usd: None,
260 gamma: get_f64(812), index_price: get_f64(731), initial_margin: get_f64(899), interest_value: None,
264 kind: get_string(461), leverage: None,
266 maintenance_margin: get_f64(898), mark_price: get_f64(732), open_orders_margin: None,
269 realized_funding: None,
270 realized_profit_loss: get_f64(706), settlement_price: get_f64(730), size_currency: None,
273 theta: get_f64(813), total_profit_loss: get_f64(708), vega: get_f64(814), unrealized_profit_loss: get_f64(707), })
278 }
279
280 pub fn from_deribit_position(
325 position: &Position,
326 sender_comp_id: String,
327 target_comp_id: String,
328 msg_seq_num: u32,
329 ) -> Result<String> {
330 let msg = MessageBuilder::new()
331 .msg_type(MsgType::PositionReport)
332 .sender_comp_id(sender_comp_id)
333 .target_comp_id(target_comp_id)
334 .msg_seq_num(msg_seq_num);
335
336 let msg = msg.field(55, position.instrument_name.clone()); let msg = msg.field(730, position.average_price.to_string()); let msg = match position.direction {
342 Direction::Buy => msg.field(704, position.size.to_string()), Direction::Sell => msg.field(705, position.size.abs().to_string()), };
345
346 let msg = if let Some(realized_pnl) = position.realized_profit_loss {
348 msg.field(706, realized_pnl.to_string())
349 } else {
350 msg
351 };
352
353 let msg = if let Some(floating_pnl) = position.floating_profit_loss {
354 msg.field(707, floating_pnl.to_string())
355 } else {
356 msg
357 };
358
359 let msg = if let Some(total_pnl) = position.total_profit_loss {
360 msg.field(708, total_pnl.to_string())
361 } else {
362 msg
363 };
364
365 let msg = if let Some(delta) = position.delta {
367 msg.field(811, delta.to_string())
368 } else {
369 msg
370 };
371
372 let msg = if let Some(gamma) = position.gamma {
373 msg.field(812, gamma.to_string())
374 } else {
375 msg
376 };
377
378 let msg = if let Some(theta) = position.theta {
379 msg.field(813, theta.to_string())
380 } else {
381 msg
382 };
383
384 let msg = if let Some(vega) = position.vega {
385 msg.field(814, vega.to_string())
386 } else {
387 msg
388 };
389
390 let msg = if let Some(index_price) = position.index_price {
392 msg.field(731, index_price.to_string())
393 } else {
394 msg
395 };
396
397 let msg = if let Some(mark_price) = position.mark_price {
398 msg.field(732, mark_price.to_string())
399 } else {
400 msg
401 };
402
403 let msg = if let Some(initial_margin) = position.initial_margin {
404 msg.field(899, initial_margin.to_string())
405 } else {
406 msg
407 };
408
409 let msg = if let Some(maintenance_margin) = position.maintenance_margin {
410 msg.field(898, maintenance_margin.to_string())
411 } else {
412 msg
413 };
414
415 let msg = msg.field(979, "FMTM".to_string()); Ok(msg.build()?.to_string())
418 }
419}
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::model::message::FixMessage;
424
425 #[test]
426 fn test_pos_req_type_conversion() {
427 assert_eq!(i32::from(PosReqType::Positions), 0);
428 assert_eq!(i32::from(PosReqType::Trades), 1);
429
430 assert_eq!(PosReqType::try_from(0).unwrap(), PosReqType::Positions);
431 assert_eq!(PosReqType::try_from(1).unwrap(), PosReqType::Trades);
432
433 assert!(PosReqType::try_from(99).is_err());
434 }
435
436 #[test]
437 fn test_subscription_request_type_conversion() {
438 assert_eq!(i32::from(SubscriptionRequestType::Snapshot), 0);
439 assert_eq!(i32::from(SubscriptionRequestType::SnapshotPlusUpdates), 1);
440
441 assert_eq!(
442 SubscriptionRequestType::try_from(0).unwrap(),
443 SubscriptionRequestType::Snapshot
444 );
445 assert_eq!(
446 SubscriptionRequestType::try_from(1).unwrap(),
447 SubscriptionRequestType::SnapshotPlusUpdates
448 );
449
450 assert!(SubscriptionRequestType::try_from(99).is_err());
451 }
452
453 #[test]
454 fn test_request_for_positions_creation() {
455 let request = RequestForPositions::all_positions("POS_123".to_string());
456 assert_eq!(request.pos_req_id, "POS_123");
457 assert_eq!(request.pos_req_type, PosReqType::Positions);
458 assert_eq!(
459 request.subscription_request_type,
460 Some(SubscriptionRequestType::Snapshot)
461 );
462 }
463
464 #[test]
465 fn test_request_for_positions_with_symbols() {
466 let request = RequestForPositions::all_positions("POS_123".to_string()).with_symbols(vec![
467 "BTC-PERPETUAL".to_string(),
468 "ETH-PERPETUAL".to_string(),
469 ]);
470
471 assert_eq!(request.symbols.len(), 2);
472 assert!(request.symbols.contains(&"BTC-PERPETUAL".to_string()));
473 }
474
475 #[test]
476 fn test_request_for_positions_to_fix_message() {
477 let request = RequestForPositions::all_positions("POS_123".to_string());
478 let fix_message = request
479 .to_fix_message("SENDER".to_string(), "TARGET".to_string(), 1)
480 .unwrap();
481
482 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())); }
488
489 #[test]
490 fn test_position_report_try_from_fix_message() {
491 let mut fix_message = FixMessage::new();
493 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();
501
502 assert_eq!(position.instrument_name, "BTC-PERPETUAL");
503 assert_eq!(position.size, 1.5);
504 assert_eq!(position.average_price, 50000.0);
505 assert!(matches!(position.direction, Direction::Buy));
506 assert_eq!(position.floating_profit_loss, Some(100.0));
507 assert_eq!(position.realized_profit_loss, Some(50.0));
508 }
509
510 #[test]
511 fn test_position_report_from_deribit_position() {
512 let position = Position {
514 instrument_name: "ETH-PERPETUAL".to_string(),
515 size: 2.0,
516 direction: Direction::Buy,
517 average_price: 3500.0,
518 average_price_usd: None,
519 delta: Some(0.5),
520 estimated_liquidation_price: None,
521 floating_profit_loss: Some(150.0),
522 floating_profit_loss_usd: None,
523 gamma: Some(0.001),
524 index_price: Some(3520.0),
525 initial_margin: Some(100.0),
526 interest_value: None,
527 kind: Some("future".to_string()),
528 leverage: None,
529 maintenance_margin: Some(50.0),
530 mark_price: Some(3510.0),
531 open_orders_margin: None,
532 realized_funding: None,
533 realized_profit_loss: Some(50.0),
534 settlement_price: Some(3500.0),
535 size_currency: None,
536 theta: Some(-0.1),
537 total_profit_loss: Some(200.0),
538 vega: Some(0.05),
539 unrealized_profit_loss: Some(150.0),
540 };
541
542 let fix_message = PositionReport::from_deribit_position(
543 &position,
544 "SENDER".to_string(),
545 "TARGET".to_string(),
546 1,
547 )
548 .unwrap();
549
550 assert!(fix_message.contains("55=ETH-PERPETUAL")); assert!(fix_message.contains("704=2")); assert!(fix_message.contains("730=3500")); }
555
556 #[test]
557 fn test_position_direction_sell() {
558 let mut fix_message = FixMessage::new();
560 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();
566
567 assert_eq!(position.instrument_name, "BTC-PERPETUAL");
568 assert_eq!(position.size, -1.0); assert!(matches!(position.direction, Direction::Sell));
570 assert_eq!(position.average_price, 45000.0);
571 }
572}