1use crate::error::{DeribitFixError, Result as DeribitFixResult};
15use crate::message::MessageBuilder;
16use crate::model::message::FixMessage;
17use crate::model::types::MsgType;
18use chrono::{DateTime, Utc};
19use deribit_base::prelude::Position;
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum PosReqType {
25 Positions,
27 Trades,
29 Exercises,
31 Assignments,
33}
34
35impl From<PosReqType> for i32 {
36 fn from(value: PosReqType) -> Self {
37 match value {
38 PosReqType::Positions => 0,
39 PosReqType::Trades => 1,
40 PosReqType::Exercises => 2,
41 PosReqType::Assignments => 3,
42 }
43 }
44}
45
46impl TryFrom<i32> for PosReqType {
47 type Error = DeribitFixError;
48
49 fn try_from(value: i32) -> Result<Self, Self::Error> {
50 match value {
51 0 => Ok(PosReqType::Positions),
52 1 => Ok(PosReqType::Trades),
53 2 => Ok(PosReqType::Exercises),
54 3 => Ok(PosReqType::Assignments),
55 _ => Err(DeribitFixError::MessageParsing(format!(
56 "Invalid PosReqType: {}",
57 value
58 ))),
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub enum SubscriptionRequestType {
66 Snapshot,
68 SnapshotPlusUpdates,
70 DisablePreviousSnapshotPlusUpdates,
72}
73
74impl From<SubscriptionRequestType> for i32 {
75 fn from(value: SubscriptionRequestType) -> Self {
76 match value {
77 SubscriptionRequestType::Snapshot => 0,
78 SubscriptionRequestType::SnapshotPlusUpdates => 1,
79 SubscriptionRequestType::DisablePreviousSnapshotPlusUpdates => 2,
80 }
81 }
82}
83
84impl TryFrom<i32> for SubscriptionRequestType {
85 type Error = DeribitFixError;
86
87 fn try_from(value: i32) -> Result<Self, Self::Error> {
88 match value {
89 0 => Ok(SubscriptionRequestType::Snapshot),
90 1 => Ok(SubscriptionRequestType::SnapshotPlusUpdates),
91 2 => Ok(SubscriptionRequestType::DisablePreviousSnapshotPlusUpdates),
92 _ => Err(DeribitFixError::MessageParsing(format!(
93 "Invalid SubscriptionRequestType: {}",
94 value
95 ))),
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct RequestForPositions {
103 pub pos_req_id: String,
105 pub pos_req_type: PosReqType,
107 pub subscription_request_type: Option<SubscriptionRequestType>,
109 pub clearing_business_date: Option<String>,
111 pub symbols: Vec<String>,
113}
114
115impl RequestForPositions {
116 pub fn all_positions(pos_req_id: String) -> Self {
118 Self {
119 pos_req_id,
120 pos_req_type: PosReqType::Positions,
121 subscription_request_type: Some(SubscriptionRequestType::Snapshot),
122 clearing_business_date: None,
123 symbols: Vec::new(),
124 }
125 }
126
127 pub fn positions_with_updates(pos_req_id: String) -> Self {
129 Self {
130 pos_req_id,
131 pos_req_type: PosReqType::Positions,
132 subscription_request_type: Some(SubscriptionRequestType::SnapshotPlusUpdates),
133 clearing_business_date: None,
134 symbols: Vec::new(),
135 }
136 }
137
138 pub fn with_symbols(mut self, symbols: Vec<String>) -> Self {
140 self.symbols = symbols;
141 self
142 }
143
144 pub fn with_clearing_date(mut self, date: String) -> Self {
146 self.clearing_business_date = Some(date);
147 self
148 }
149
150 pub fn to_fix_message(
152 &self,
153 sender_comp_id: String,
154 target_comp_id: String,
155 msg_seq_num: u32,
156 ) -> DeribitFixResult<FixMessage> {
157 let mut builder = MessageBuilder::new()
158 .msg_type(MsgType::RequestForPositions)
159 .sender_comp_id(sender_comp_id)
160 .target_comp_id(target_comp_id)
161 .msg_seq_num(msg_seq_num)
162 .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 {
167 builder = builder.field(263, i32::from(subscription_type).to_string());
168 }
169
170 if let Some(ref date) = self.clearing_business_date {
172 builder = builder.field(715, date.clone());
173 }
174
175 if !self.symbols.is_empty() {
177 builder = builder.field(146, self.symbols.len().to_string()); for symbol in &self.symbols {
179 builder = builder.field(55, symbol.clone()); }
181 }
182
183 builder.build()
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct PositionReport {
190 pub pos_req_id: String,
192 pub symbol: String,
194 pub position_qty: Option<f64>,
196 pub average_price: Option<f64>,
198 pub unrealized_pnl: Option<f64>,
200 pub realized_pnl: Option<f64>,
202 pub position_date: Option<String>,
204 pub last_update_time: Option<DateTime<Utc>>,
206}
207
208impl PositionReport {
209 pub fn new(pos_req_id: String, symbol: String) -> Self {
211 Self {
212 pos_req_id,
213 symbol,
214 position_qty: None,
215 average_price: None,
216 unrealized_pnl: None,
217 realized_pnl: None,
218 position_date: None,
219 last_update_time: Some(Utc::now()),
220 }
221 }
222
223 pub fn with_position_qty(mut self, position_qty: f64) -> Self {
225 self.position_qty = Some(position_qty);
226 self
227 }
228
229 pub fn with_average_price(mut self, average_price: f64) -> Self {
231 self.average_price = Some(average_price);
232 self
233 }
234
235 pub fn with_unrealized_pnl(mut self, unrealized_pnl: f64) -> Self {
237 self.unrealized_pnl = Some(unrealized_pnl);
238 self
239 }
240
241 pub fn with_realized_pnl(mut self, realized_pnl: f64) -> Self {
243 self.realized_pnl = Some(realized_pnl);
244 self
245 }
246
247 pub fn with_position_date(mut self, position_date: String) -> Self {
249 self.position_date = Some(position_date);
250 self
251 }
252
253 pub fn from_fix_message(message: &FixMessage) -> DeribitFixResult<Self> {
255 let pos_req_id = message
256 .get_field(710)
257 .ok_or_else(|| DeribitFixError::MessageParsing("Missing PosReqID (710)".to_string()))?
258 .clone();
259
260 let symbol = message
261 .get_field(55)
262 .ok_or_else(|| DeribitFixError::MessageParsing("Missing Symbol (55)".to_string()))?
263 .clone();
264
265 let position_qty = message.get_field(703).and_then(|s| s.parse::<f64>().ok());
266
267 let average_price = message.get_field(6).and_then(|s| s.parse::<f64>().ok());
268
269 let unrealized_pnl = message.get_field(1247).and_then(|s| s.parse::<f64>().ok());
270
271 let realized_pnl = message.get_field(1248).and_then(|s| s.parse::<f64>().ok());
272
273 let position_date = message.get_field(704).cloned();
274
275 Ok(Self {
276 pos_req_id,
277 symbol,
278 position_qty,
279 average_price,
280 unrealized_pnl,
281 realized_pnl,
282 position_date,
283 last_update_time: Some(Utc::now()),
284 })
285 }
286
287 pub fn to_fix_message(
289 &self,
290 sender_comp_id: String,
291 target_comp_id: String,
292 msg_seq_num: u32,
293 ) -> DeribitFixResult<FixMessage> {
294 let mut builder = MessageBuilder::new()
295 .msg_type(MsgType::PositionReport)
296 .sender_comp_id(sender_comp_id)
297 .target_comp_id(target_comp_id)
298 .msg_seq_num(msg_seq_num)
299 .field(710, self.pos_req_id.clone()) .field(55, self.symbol.clone()); if let Some(position_qty) = self.position_qty {
304 builder = builder.field(703, position_qty.to_string()); }
306
307 if let Some(average_price) = self.average_price {
308 builder = builder.field(6, average_price.to_string()); }
310
311 if let Some(unrealized_pnl) = self.unrealized_pnl {
312 builder = builder.field(1247, unrealized_pnl.to_string()); }
314
315 if let Some(realized_pnl) = self.realized_pnl {
316 builder = builder.field(1248, realized_pnl.to_string()); }
318
319 if let Some(ref position_date) = self.position_date {
320 builder = builder.field(704, position_date.clone()); }
322
323 builder.build()
324 }
325
326 pub fn to_position(&self) -> Position {
328 Position {
329 symbol: self.symbol.clone(),
330 quantity: self.position_qty.unwrap_or(0.0),
331 average_price: self.average_price.unwrap_or(0.0),
332 realized_pnl: self.realized_pnl.unwrap_or(0.0),
333 unrealized_pnl: self.unrealized_pnl.unwrap_or(0.0),
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_pos_req_type_conversion() {
344 assert_eq!(i32::from(PosReqType::Positions), 0);
345 assert_eq!(i32::from(PosReqType::Trades), 1);
346
347 assert_eq!(PosReqType::try_from(0).unwrap(), PosReqType::Positions);
348 assert_eq!(PosReqType::try_from(1).unwrap(), PosReqType::Trades);
349
350 assert!(PosReqType::try_from(99).is_err());
351 }
352
353 #[test]
354 fn test_subscription_request_type_conversion() {
355 assert_eq!(i32::from(SubscriptionRequestType::Snapshot), 0);
356 assert_eq!(i32::from(SubscriptionRequestType::SnapshotPlusUpdates), 1);
357
358 assert_eq!(
359 SubscriptionRequestType::try_from(0).unwrap(),
360 SubscriptionRequestType::Snapshot
361 );
362 assert_eq!(
363 SubscriptionRequestType::try_from(1).unwrap(),
364 SubscriptionRequestType::SnapshotPlusUpdates
365 );
366
367 assert!(SubscriptionRequestType::try_from(99).is_err());
368 }
369
370 #[test]
371 fn test_request_for_positions_creation() {
372 let request = RequestForPositions::all_positions("POS_123".to_string());
373 assert_eq!(request.pos_req_id, "POS_123");
374 assert_eq!(request.pos_req_type, PosReqType::Positions);
375 assert_eq!(
376 request.subscription_request_type,
377 Some(SubscriptionRequestType::Snapshot)
378 );
379 }
380
381 #[test]
382 fn test_request_for_positions_with_symbols() {
383 let request = RequestForPositions::all_positions("POS_123".to_string()).with_symbols(vec![
384 "BTC-PERPETUAL".to_string(),
385 "ETH-PERPETUAL".to_string(),
386 ]);
387
388 assert_eq!(request.symbols.len(), 2);
389 assert!(request.symbols.contains(&"BTC-PERPETUAL".to_string()));
390 }
391
392 #[test]
393 fn test_request_for_positions_to_fix_message() {
394 let request = RequestForPositions::all_positions("POS_123".to_string());
395 let fix_message = request
396 .to_fix_message("SENDER".to_string(), "TARGET".to_string(), 1)
397 .unwrap();
398
399 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())); }
405
406 #[test]
407 fn test_position_report_from_fix_message() {
408 let mut fix_message = FixMessage::new();
410 fix_message.set_field(710, "POS_123".to_string());
411 fix_message.set_field(55, "BTC-PERPETUAL".to_string());
412 fix_message.set_field(703, "1.5".to_string());
413 fix_message.set_field(6, "50000.0".to_string());
414 fix_message.set_field(1247, "100.0".to_string());
415 fix_message.set_field(1248, "50.0".to_string());
416
417 let position_report = PositionReport::from_fix_message(&fix_message).unwrap();
418
419 assert_eq!(position_report.pos_req_id, "POS_123");
420 assert_eq!(position_report.symbol, "BTC-PERPETUAL");
421 assert_eq!(position_report.position_qty, Some(1.5));
422 assert_eq!(position_report.average_price, Some(50000.0));
423 assert_eq!(position_report.unrealized_pnl, Some(100.0));
424 assert_eq!(position_report.realized_pnl, Some(50.0));
425 }
426
427 #[test]
428 fn test_position_report_to_position() {
429 let position_report = PositionReport {
430 pos_req_id: "POS_123".to_string(),
431 symbol: "BTC-PERPETUAL".to_string(),
432 position_qty: Some(1.5),
433 average_price: Some(50000.0),
434 unrealized_pnl: Some(100.0),
435 realized_pnl: Some(50.0),
436 position_date: None,
437 last_update_time: None,
438 };
439
440 let position = position_report.to_position();
441 assert_eq!(position.symbol, "BTC-PERPETUAL");
442 assert_eq!(position.quantity, 1.5);
443 assert_eq!(position.average_price, 50000.0);
444 assert_eq!(position.unrealized_pnl, 100.0);
445 assert_eq!(position.realized_pnl, 50.0);
446 }
447
448 #[test]
449 fn test_position_report_builder() {
450 let report = PositionReport::new("POS_789".to_string(), "ETH-PERPETUAL".to_string())
451 .with_position_qty(2.0)
452 .with_average_price(3500.0)
453 .with_unrealized_pnl(150.0)
454 .with_realized_pnl(50.0)
455 .with_position_date("20240102".to_string());
456
457 assert_eq!(report.pos_req_id, "POS_789");
458 assert_eq!(report.symbol, "ETH-PERPETUAL");
459 assert_eq!(report.position_qty, Some(2.0));
460 assert_eq!(report.average_price, Some(3500.0));
461 assert_eq!(report.unrealized_pnl, Some(150.0));
462 assert_eq!(report.realized_pnl, Some(50.0));
463 assert_eq!(report.position_date, Some("20240102".to_string()));
464 assert!(report.last_update_time.is_some());
465 }
466
467 #[test]
468 fn test_position_report_to_fix_message() {
469 let report = PositionReport::new("POS_123".to_string(), "BTC-PERPETUAL".to_string())
470 .with_position_qty(1.0)
471 .with_average_price(45000.0)
472 .with_unrealized_pnl(500.0)
473 .with_realized_pnl(-200.0)
474 .with_position_date("20240103".to_string());
475
476 let fix_message = report
477 .to_fix_message("SENDER".to_string(), "TARGET".to_string(), 1)
478 .unwrap();
479
480 assert_eq!(fix_message.get_field(35).unwrap(), "AP"); assert_eq!(fix_message.get_field(710).unwrap(), "POS_123"); assert_eq!(fix_message.get_field(55).unwrap(), "BTC-PERPETUAL"); assert_eq!(fix_message.get_field(703).unwrap(), "1"); assert_eq!(fix_message.get_field(6).unwrap(), "45000"); assert_eq!(fix_message.get_field(1247).unwrap(), "500"); assert_eq!(fix_message.get_field(1248).unwrap(), "-200"); assert_eq!(fix_message.get_field(704).unwrap(), "20240103"); }
492
493 #[test]
494 fn test_position_report_to_fix_message_minimal() {
495 let report = PositionReport::new("POS_MIN".to_string(), "ETH-PERPETUAL".to_string());
496
497 let fix_message = report
498 .to_fix_message("SENDER".to_string(), "TARGET".to_string(), 2)
499 .unwrap();
500
501 assert_eq!(fix_message.get_field(35).unwrap(), "AP"); assert_eq!(fix_message.get_field(710).unwrap(), "POS_MIN"); assert_eq!(fix_message.get_field(55).unwrap(), "ETH-PERPETUAL"); assert!(fix_message.get_field(703).is_none()); assert!(fix_message.get_field(6).is_none()); assert!(fix_message.get_field(1247).is_none()); assert!(fix_message.get_field(1248).is_none()); assert!(fix_message.get_field(704).is_none()); }
513}