Skip to main content

rustrade_execution/
position.rs

1use chrono::{DateTime, Utc};
2use derive_more::Constructor;
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6/// Represents an open position in a derivative instrument (perpetuals, futures, margin).
7///
8/// For spot instruments, positions are implicit in asset balances — this struct is only
9/// used for instruments that track position state separately from cash balances.
10#[derive(
11    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
12)]
13pub struct Position {
14    /// Signed quantity: positive = long, negative = short, zero = flat.
15    ///
16    /// Using signed quantity is the industry standard for derivatives and avoids
17    /// a separate `side` field.
18    pub quantity: Decimal,
19
20    /// Average entry price. `None` if position is flat or entry price unavailable.
21    pub entry_price: Option<Decimal>,
22
23    /// Unrealized PnL in quote currency. `None` if not provided by exchange.
24    pub unrealized_pnl: Option<Decimal>,
25
26    /// Margin/collateral allocated to this position. `None` if not applicable.
27    pub margin_used: Option<Decimal>,
28
29    /// Liquidation price. `None` for cross-margin or if not provided.
30    pub liquidation_price: Option<Decimal>,
31
32    /// Leverage setting. `None` if not applicable (e.g., spot-margin).
33    pub leverage: Option<Decimal>,
34
35    /// Exchange timestamp when this position state was reported.
36    pub time_exchange: DateTime<Utc>,
37}
38
39impl Position {
40    /// Returns true if this position is flat (zero quantity).
41    pub fn is_flat(&self) -> bool {
42        self.quantity.is_zero()
43    }
44
45    /// Returns true if this is a long position (positive quantity).
46    pub fn is_long(&self) -> bool {
47        self.quantity.is_sign_positive() && !self.quantity.is_zero()
48    }
49
50    /// Returns true if this is a short position (negative quantity).
51    pub fn is_short(&self) -> bool {
52        self.quantity.is_sign_negative()
53    }
54
55    /// Returns the absolute position size.
56    pub fn abs_quantity(&self) -> Decimal {
57        self.quantity.abs()
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use rust_decimal_macros::dec;
65
66    #[test]
67    fn test_position_side_detection() {
68        let now = Utc::now();
69
70        let long = Position::new(dec!(1.5), None, None, None, None, None, now);
71        assert!(long.is_long());
72        assert!(!long.is_short());
73        assert!(!long.is_flat());
74
75        let short = Position::new(dec!(-1.5), None, None, None, None, None, now);
76        assert!(!short.is_long());
77        assert!(short.is_short());
78        assert!(!short.is_flat());
79
80        let flat = Position::new(dec!(0), None, None, None, None, None, now);
81        assert!(!flat.is_long());
82        assert!(!flat.is_short());
83        assert!(flat.is_flat());
84    }
85
86    #[test]
87    fn test_abs_quantity() {
88        let now = Utc::now();
89        let short = Position::new(dec!(-2.5), None, None, None, None, None, now);
90        assert_eq!(short.abs_quantity(), dec!(2.5));
91    }
92}