1use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use smartstring::alias::String as SmartString;
8
9use crate::types::PositionId;
10use crate::venues::Venue;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum PositionSide {
15 Long,
17 Short,
19 Both,
21}
22
23impl PositionSide {
24 #[must_use]
26 pub const fn opposite(self) -> Self {
27 match self {
28 Self::Long => Self::Short,
29 Self::Short => Self::Long,
30 Self::Both => Self::Both,
31 }
32 }
33}
34
35impl std::fmt::Display for PositionSide {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::Long => write!(f, "LONG"),
39 Self::Short => write!(f, "SHORT"),
40 Self::Both => write!(f, "BOTH"),
41 }
42 }
43}
44
45impl std::str::FromStr for PositionSide {
46 type Err = anyhow::Error;
47
48 fn from_str(s: &str) -> Result<Self, Self::Err> {
49 match s.to_uppercase().as_str() {
50 "LONG" => Ok(Self::Long),
51 "SHORT" => Ok(Self::Short),
52 "BOTH" => Ok(Self::Both),
53 _ => Err(anyhow::anyhow!("Invalid position side: {}", s)),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub enum MarginType {
61 Isolated,
63 Cross,
65}
66
67impl std::fmt::Display for MarginType {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::Isolated => write!(f, "isolated"),
71 Self::Cross => write!(f, "cross"),
72 }
73 }
74}
75
76impl std::str::FromStr for MarginType {
77 type Err = anyhow::Error;
78
79 fn from_str(s: &str) -> Result<Self, Self::Err> {
80 match s.to_lowercase().as_str() {
81 "isolated" => Ok(Self::Isolated),
82 "cross" => Ok(Self::Cross),
83 _ => Err(anyhow::anyhow!("Invalid margin type: {}", s)),
84 }
85 }
86}
87
88#[repr(align(64))] #[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct FuturesPosition {
92 pub id: PositionId,
94
95 pub venue: Venue,
97
98 pub symbol: SmartString,
100
101 pub side: PositionSide,
103
104 pub amount: Decimal,
106
107 pub entry_price: Decimal,
109
110 pub breakeven_price: Decimal,
112
113 pub unrealized_pnl: Decimal,
115
116 pub realized_pnl: Decimal,
118
119 pub margin_type: MarginType,
121
122 pub isolated_wallet: Decimal,
124
125 pub creation_time_ns: u64,
127
128 pub update_time_ns: u64,
130
131 pub metadata: rusty_common::json::Value,
133}
134
135impl FuturesPosition {
136 #[must_use]
138 pub fn new(
139 venue: Venue,
140 symbol: impl AsRef<str>,
141 side: PositionSide,
142 amount: Decimal,
143 entry_price: Decimal,
144 margin_type: MarginType,
145 ) -> Self {
146 let now = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
147 u64::try_from(
148 std::time::SystemTime::now()
149 .duration_since(std::time::UNIX_EPOCH)
150 .unwrap_or_default()
151 .as_nanos(),
152 )
153 .unwrap_or(u64::MAX)
154 });
155
156 Self {
157 id: PositionId::new(),
158 venue,
159 symbol: SmartString::from(symbol.as_ref()),
160 side,
161 amount,
162 entry_price,
163 breakeven_price: entry_price, unrealized_pnl: Decimal::ZERO,
165 realized_pnl: Decimal::ZERO,
166 margin_type,
167 isolated_wallet: Decimal::ZERO,
168 creation_time_ns: now,
169 update_time_ns: now,
170 metadata: rusty_common::json::json!(null),
171 }
172 }
173
174 pub fn update(
223 &mut self,
224 amount: Decimal,
225 entry_price: Decimal,
226 breakeven_price: Decimal,
227 unrealized_pnl: Decimal,
228 realized_pnl: Decimal,
229 isolated_wallet: Decimal,
230 ) {
231 self.amount = amount;
232 self.entry_price = entry_price;
233 self.breakeven_price = breakeven_price;
234 self.unrealized_pnl = unrealized_pnl;
235 self.realized_pnl = realized_pnl;
236 self.isolated_wallet = isolated_wallet;
237
238 self.update_time_ns = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
240 u64::try_from(
241 std::time::SystemTime::now()
242 .duration_since(std::time::UNIX_EPOCH)
243 .unwrap_or_default()
244 .as_nanos(),
245 )
246 .unwrap_or(u64::MAX)
247 });
248 }
249
250 #[must_use]
252 pub const fn is_closed(&self) -> bool {
253 self.amount.is_zero()
254 }
255
256 #[must_use]
258 pub fn get_notional_value(&self, current_price: Decimal) -> Decimal {
259 self.amount.abs() * current_price
260 }
261
262 #[must_use]
264 pub fn get_pnl(&self, current_price: Decimal) -> Decimal {
265 let price_diff = current_price - self.entry_price;
266 match self.side {
267 PositionSide::Long => self.amount * price_diff,
268 PositionSide::Short => -self.amount * price_diff,
269 PositionSide::Both => {
270 self.amount * price_diff
273 }
274 }
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct PositionUpdate {
281 pub position_id: PositionId,
283
284 pub venue: Venue,
286
287 pub symbol: SmartString,
289
290 pub side: PositionSide,
292
293 pub amount: Decimal,
295
296 pub entry_price: Decimal,
298
299 pub breakeven_price: Decimal,
301
302 pub unrealized_pnl: Decimal,
304
305 pub realized_pnl: Decimal,
307
308 pub margin_type: MarginType,
310
311 pub isolated_wallet: Decimal,
313
314 pub timestamp_ns: u64,
316}
317
318impl PositionUpdate {
319 #[must_use]
321 pub fn from_position(position: &FuturesPosition) -> Self {
322 Self {
323 position_id: position.id,
324 venue: position.venue,
325 symbol: position.symbol.clone(),
326 side: position.side,
327 amount: position.amount,
328 entry_price: position.entry_price,
329 breakeven_price: position.breakeven_price,
330 unrealized_pnl: position.unrealized_pnl,
331 realized_pnl: position.realized_pnl,
332 margin_type: position.margin_type,
333 isolated_wallet: position.isolated_wallet,
334 timestamp_ns: rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
335 u64::try_from(
336 std::time::SystemTime::now()
337 .duration_since(std::time::UNIX_EPOCH)
338 .unwrap_or_default()
339 .as_nanos(),
340 )
341 .unwrap_or(u64::MAX)
342 }),
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::venues::Venue;
351 use rust_decimal_macros::dec;
352
353 #[test]
354 fn test_position_side_display() {
355 assert_eq!(PositionSide::Long.to_string(), "LONG");
356 assert_eq!(PositionSide::Short.to_string(), "SHORT");
357 assert_eq!(PositionSide::Both.to_string(), "BOTH");
358 }
359
360 #[test]
361 fn test_position_side_from_str() {
362 assert_eq!("LONG".parse::<PositionSide>().unwrap(), PositionSide::Long);
363 assert_eq!(
364 "SHORT".parse::<PositionSide>().unwrap(),
365 PositionSide::Short
366 );
367 assert_eq!("BOTH".parse::<PositionSide>().unwrap(), PositionSide::Both);
368 assert_eq!("long".parse::<PositionSide>().unwrap(), PositionSide::Long);
369 assert!("INVALID".parse::<PositionSide>().is_err());
370 }
371
372 #[test]
373 fn test_position_side_opposite() {
374 assert_eq!(PositionSide::Long.opposite(), PositionSide::Short);
375 assert_eq!(PositionSide::Short.opposite(), PositionSide::Long);
376 assert_eq!(PositionSide::Both.opposite(), PositionSide::Both);
377 }
378
379 #[test]
380 fn test_position_side_opposite_const_context() {
381 const LONG: PositionSide = PositionSide::Long;
383 const SHORT: PositionSide = PositionSide::Short;
384 const BOTH: PositionSide = PositionSide::Both;
385 const LONG_OPPOSITE: PositionSide = LONG.opposite();
386 const SHORT_OPPOSITE: PositionSide = SHORT.opposite();
387 const BOTH_OPPOSITE: PositionSide = BOTH.opposite();
388
389 assert_eq!(LONG_OPPOSITE, PositionSide::Short);
390 assert_eq!(SHORT_OPPOSITE, PositionSide::Long);
391 assert_eq!(BOTH_OPPOSITE, PositionSide::Both);
392 }
393
394 #[test]
395 fn test_margin_type_display() {
396 assert_eq!(MarginType::Isolated.to_string(), "isolated");
397 assert_eq!(MarginType::Cross.to_string(), "cross");
398 }
399
400 #[test]
401 fn test_margin_type_from_str() {
402 assert_eq!(
403 "isolated".parse::<MarginType>().unwrap(),
404 MarginType::Isolated
405 );
406 assert_eq!("cross".parse::<MarginType>().unwrap(), MarginType::Cross);
407 assert_eq!(
408 "ISOLATED".parse::<MarginType>().unwrap(),
409 MarginType::Isolated
410 );
411 assert!("INVALID".parse::<MarginType>().is_err());
412 }
413
414 #[test]
415 fn test_futures_position_new() {
416 let position = FuturesPosition::new(
417 Venue::Binance,
418 "BTCUSDT",
419 PositionSide::Long,
420 dec!(1.0),
421 dec!(50000.0),
422 MarginType::Cross,
423 );
424
425 assert_eq!(position.venue, Venue::Binance);
426 assert_eq!(position.symbol.as_str(), "BTCUSDT");
427 assert_eq!(position.side, PositionSide::Long);
428 assert_eq!(position.amount, dec!(1.0));
429 assert_eq!(position.entry_price, dec!(50000.0));
430 assert_eq!(position.breakeven_price, dec!(50000.0));
431 assert_eq!(position.unrealized_pnl, dec!(0.0));
432 assert_eq!(position.margin_type, MarginType::Cross);
433 assert!(!position.is_closed());
434 }
435
436 #[test]
437 fn test_futures_position_update() {
438 let mut position = FuturesPosition::new(
439 Venue::Binance,
440 "BTCUSDT",
441 PositionSide::Long,
442 dec!(1.0),
443 dec!(50000.0),
444 MarginType::Cross,
445 );
446
447 let initial_update_time = position.update_time_ns;
448
449 position.update(
450 dec!(1.5),
451 dec!(50500.0),
452 dec!(50600.0),
453 dec!(750.0),
454 dec!(0.0),
455 dec!(0.0),
456 );
457
458 assert_eq!(position.amount, dec!(1.5));
459 assert_eq!(position.entry_price, dec!(50500.0));
460 assert_eq!(position.breakeven_price, dec!(50600.0));
461 assert_eq!(position.unrealized_pnl, dec!(750.0));
462 assert!(position.update_time_ns > initial_update_time);
463 }
464
465 #[test]
466 fn test_futures_position_is_closed() {
467 let mut position = FuturesPosition::new(
468 Venue::Binance,
469 "BTCUSDT",
470 PositionSide::Long,
471 dec!(1.0),
472 dec!(50000.0),
473 MarginType::Cross,
474 );
475
476 assert!(!position.is_closed());
477
478 position.amount = dec!(0.0);
479 assert!(position.is_closed());
480 }
481
482 #[test]
483 fn test_futures_position_get_pnl() {
484 let position = FuturesPosition::new(
485 Venue::Binance,
486 "BTCUSDT",
487 PositionSide::Long,
488 dec!(1.0),
489 dec!(50000.0),
490 MarginType::Cross,
491 );
492
493 let pnl = position.get_pnl(dec!(51000.0));
495 assert_eq!(pnl, dec!(1000.0));
496
497 let pnl = position.get_pnl(dec!(49000.0));
499 assert_eq!(pnl, dec!(-1000.0));
500
501 let short_position = FuturesPosition::new(
503 Venue::Binance,
504 "BTCUSDT",
505 PositionSide::Short,
506 dec!(1.0),
507 dec!(50000.0),
508 MarginType::Cross,
509 );
510
511 let pnl = short_position.get_pnl(dec!(49000.0));
513 assert_eq!(pnl, dec!(1000.0));
514
515 let pnl = short_position.get_pnl(dec!(51000.0));
517 assert_eq!(pnl, dec!(-1000.0));
518
519 let hedge_long_position = FuturesPosition::new(
521 Venue::Binance,
522 "BTCUSDT",
523 PositionSide::Both,
524 dec!(1.0), dec!(50000.0),
526 MarginType::Cross,
527 );
528
529 let pnl = hedge_long_position.get_pnl(dec!(51000.0));
531 assert_eq!(pnl, dec!(1000.0));
532
533 let pnl = hedge_long_position.get_pnl(dec!(49000.0));
535 assert_eq!(pnl, dec!(-1000.0));
536
537 let hedge_short_position = FuturesPosition::new(
539 Venue::Binance,
540 "BTCUSDT",
541 PositionSide::Both,
542 dec!(-1.0), dec!(50000.0),
544 MarginType::Cross,
545 );
546
547 let pnl = hedge_short_position.get_pnl(dec!(49000.0));
549 assert_eq!(pnl, dec!(1000.0)); let pnl = hedge_short_position.get_pnl(dec!(51000.0));
553 assert_eq!(pnl, dec!(-1000.0)); }
555
556 #[test]
557 fn test_futures_position_get_notional_value() {
558 let long_position = FuturesPosition::new(
560 Venue::Binance,
561 "BTCUSDT",
562 PositionSide::Long,
563 dec!(1.5),
564 dec!(50000.0),
565 MarginType::Cross,
566 );
567
568 let notional = long_position.get_notional_value(dec!(52000.0));
569 assert_eq!(notional, dec!(78000.0)); let short_position = FuturesPosition::new(
573 Venue::Binance,
574 "BTCUSDT",
575 PositionSide::Short,
576 dec!(2.0),
577 dec!(50000.0),
578 MarginType::Cross,
579 );
580
581 let notional = short_position.get_notional_value(dec!(48000.0));
582 assert_eq!(notional, dec!(96000.0)); let hedge_long = FuturesPosition::new(
586 Venue::Binance,
587 "BTCUSDT",
588 PositionSide::Both,
589 dec!(0.5),
590 dec!(50000.0),
591 MarginType::Cross,
592 );
593
594 let notional = hedge_long.get_notional_value(dec!(51000.0));
595 assert_eq!(notional, dec!(25500.0)); let hedge_short = FuturesPosition::new(
599 Venue::Binance,
600 "BTCUSDT",
601 PositionSide::Both,
602 dec!(-1.0),
603 dec!(50000.0),
604 MarginType::Cross,
605 );
606
607 let notional = hedge_short.get_notional_value(dec!(49000.0));
608 assert_eq!(notional, dec!(49000.0)); let zero_position = FuturesPosition::new(
612 Venue::Binance,
613 "BTCUSDT",
614 PositionSide::Long,
615 dec!(0.0),
616 dec!(50000.0),
617 MarginType::Cross,
618 );
619
620 let notional = zero_position.get_notional_value(dec!(55000.0));
621 assert_eq!(notional, dec!(0.0)); let position = FuturesPosition::new(
625 Venue::Binance,
626 "BTCUSDT",
627 PositionSide::Long,
628 dec!(1.0),
629 dec!(50000.0),
630 MarginType::Cross,
631 );
632
633 let notional = position.get_notional_value(dec!(0.0));
634 assert_eq!(notional, dec!(0.0)); let fractional_position = FuturesPosition::new(
638 Venue::Binance,
639 "BTCUSDT",
640 PositionSide::Long,
641 dec!(0.123),
642 dec!(50000.0),
643 MarginType::Cross,
644 );
645
646 let notional = fractional_position.get_notional_value(dec!(50123.45));
647 assert_eq!(notional, dec!(6165.18435)); let large_position = FuturesPosition::new(
651 Venue::Binance,
652 "BTCUSDT",
653 PositionSide::Long,
654 dec!(100.0),
655 dec!(50000.0),
656 MarginType::Cross,
657 );
658
659 let notional = large_position.get_notional_value(dec!(60000.0));
660 assert_eq!(notional, dec!(6000000.0)); let isolated_position = FuturesPosition::new(
664 Venue::Binance,
665 "BTCUSDT",
666 PositionSide::Long,
667 dec!(2.5),
668 dec!(50000.0),
669 MarginType::Isolated,
670 );
671
672 let notional = isolated_position.get_notional_value(dec!(51500.0));
673 assert_eq!(notional, dec!(128750.0)); }
675
676 #[test]
677 fn test_position_update_from_position() {
678 let position = FuturesPosition::new(
679 Venue::Binance,
680 "BTCUSDT",
681 PositionSide::Long,
682 dec!(1.0),
683 dec!(50000.0),
684 MarginType::Cross,
685 );
686
687 let update = PositionUpdate::from_position(&position);
688
689 assert_eq!(update.position_id, position.id);
690 assert_eq!(update.venue, position.venue);
691 assert_eq!(update.symbol, position.symbol);
692 assert_eq!(update.side, position.side);
693 assert_eq!(update.amount, position.amount);
694 assert_eq!(update.entry_price, position.entry_price);
695 assert_eq!(update.margin_type, position.margin_type);
696 }
697
698 #[test]
699 fn test_position_update_from_position_timestamp_verification() {
700 let position = FuturesPosition::new(
701 Venue::Binance,
702 "BTCUSDT",
703 PositionSide::Long,
704 dec!(1.0),
705 dec!(50000.0),
706 MarginType::Cross,
707 );
708
709 let timestamp_before = rusty_common::time::get_timestamp_ns_result().unwrap();
711 let update = PositionUpdate::from_position(&position);
712 let timestamp_after = rusty_common::time::get_timestamp_ns_result().unwrap();
713
714 assert_ne!(update.timestamp_ns, 0, "Timestamp should not be zero");
716
717 assert!(
719 update.timestamp_ns >= timestamp_before && update.timestamp_ns <= timestamp_after,
720 "Timestamp {} should be between {} and {}",
721 update.timestamp_ns,
722 timestamp_before,
723 timestamp_after
724 );
725
726 assert!(
729 update.timestamp_ns > 1_600_000_000_000_000_000u64,
730 "Timestamp {} seems too small to be a valid nanosecond timestamp",
731 update.timestamp_ns
732 );
733 }
734}