rusty_model/
position.rs

1//! Position-related data structures
2//!
3//! Contains types for representing futures positions and position updates
4
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use smartstring::alias::String as SmartString;
8
9use crate::types::PositionId;
10use crate::venues::Venue;
11
12/// Position side for futures trading
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum PositionSide {
15    /// Long position
16    Long,
17    /// Short position
18    Short,
19    /// Both sides (hedge mode)
20    Both,
21}
22
23impl PositionSide {
24    /// Get the opposite side
25    #[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/// Margin type for futures positions
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub enum MarginType {
61    /// Isolated margin
62    Isolated,
63    /// Cross margin
64    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/// Represents a futures position in the system
89#[repr(align(64))] // Cache-line aligned for HFT performance
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct FuturesPosition {
92    /// Unique position ID
93    pub id: PositionId,
94
95    /// Exchange identifier
96    pub venue: Venue,
97
98    /// Trading symbol (e.g. "BTCUSDT")
99    pub symbol: SmartString,
100
101    /// Position side (long/short/both)
102    pub side: PositionSide,
103
104    /// Position amount (positive for long, negative for short in unified margin)
105    pub amount: Decimal,
106
107    /// Entry price
108    pub entry_price: Decimal,
109
110    /// Breakeven price
111    pub breakeven_price: Decimal,
112
113    /// Unrealized `PnL`
114    pub unrealized_pnl: Decimal,
115
116    /// Accumulated realized `PnL`
117    pub realized_pnl: Decimal,
118
119    /// Margin type
120    pub margin_type: MarginType,
121
122    /// Isolated wallet amount (for isolated margin)
123    pub isolated_wallet: Decimal,
124
125    /// Position creation time in nanoseconds
126    pub creation_time_ns: u64,
127
128    /// Position update time in nanoseconds
129    pub update_time_ns: u64,
130
131    /// Additional position metadata
132    pub metadata: rusty_common::json::Value,
133}
134
135impl FuturesPosition {
136    /// Create a new futures position
137    #[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, // Initially same as entry price
164            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    /// Update position with new data from exchange position reports
175    ///
176    /// This method performs a complete update of all mutable position fields based on
177    /// fresh data received from the exchange. It's designed for high-frequency updates
178    /// where position state changes rapidly.
179    ///
180    /// # Parameters
181    ///
182    /// * `amount` - New position size (positive for long, negative for short in unified margin)
183    /// * `entry_price` - Updated average entry price after fills or position adjustments
184    /// * `breakeven_price` - Price at which the position breaks even (includes fees)
185    /// * `unrealized_pnl` - Current unrealized profit/loss at market price
186    /// * `realized_pnl` - Accumulated realized profit/loss from closed portions
187    /// * `isolated_wallet` - Available margin for isolated positions (0 for cross margin)
188    ///
189    /// # Field Mutations
190    ///
191    /// This method updates the following fields:
192    /// - `amount`: Position size with sign indicating direction
193    /// - `entry_price`: Average entry price (weighted by fills)
194    /// - `breakeven_price`: Break-even price including fees and funding
195    /// - `unrealized_pnl`: Mark-to-market P&L at current price
196    /// - `realized_pnl`: Cumulative realized P&L from position reductions
197    /// - `isolated_wallet`: Margin available for isolated positions
198    /// - `update_time_ns`: Automatically set to current nanosecond timestamp
199    ///
200    /// # Performance Notes
201    ///
202    /// - Cache-line aligned struct (`#[repr(align(64))]`) for optimal memory access
203    /// - Uses high-precision `Decimal` arithmetic for financial calculations
204    /// - Nanosecond timestamp precision for HFT latency tracking
205    /// - Designed for frequent updates in high-frequency trading scenarios
206    ///
207    /// # Example
208    ///
209    /// ```ignore
210    /// let mut position = FuturesPosition::new(/*...*/);
211    ///
212    /// // Update with fresh exchange data
213    /// position.update(
214    ///     dec!(1.5),      // New position size
215    ///     dec!(50200.0),  // Updated entry price
216    ///     dec!(50250.0),  // Break-even price
217    ///     dec!(750.0),    // Unrealized P&L
218    ///     dec!(100.0),    // Realized P&L
219    ///     dec!(1000.0),   // Isolated wallet
220    /// );
221    /// ```
222    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        // Update timestamp
239        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    /// Check if position is closed (amount is zero)
251    #[must_use]
252    pub const fn is_closed(&self) -> bool {
253        self.amount.is_zero()
254    }
255
256    /// Get position value at current price
257    #[must_use]
258    pub fn get_notional_value(&self, current_price: Decimal) -> Decimal {
259        self.amount.abs() * current_price
260    }
261
262    /// Get position `PnL` at current price
263    #[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                // For hedge mode, amount can be positive (long) or negative (short).
271                // The formula `amount * price_diff` works for both cases.
272                self.amount * price_diff
273            }
274        }
275    }
276}
277
278/// Position update event from exchange
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct PositionUpdate {
281    /// Position ID
282    pub position_id: PositionId,
283
284    /// Exchange identifier
285    pub venue: Venue,
286
287    /// Trading symbol
288    pub symbol: SmartString,
289
290    /// Position side
291    pub side: PositionSide,
292
293    /// Position amount
294    pub amount: Decimal,
295
296    /// Entry price
297    pub entry_price: Decimal,
298
299    /// Breakeven price
300    pub breakeven_price: Decimal,
301
302    /// Unrealized `PnL`
303    pub unrealized_pnl: Decimal,
304
305    /// Accumulated realized `PnL`
306    pub realized_pnl: Decimal,
307
308    /// Margin type
309    pub margin_type: MarginType,
310
311    /// Isolated wallet amount
312    pub isolated_wallet: Decimal,
313
314    /// Update timestamp in nanoseconds
315    pub timestamp_ns: u64,
316}
317
318impl PositionUpdate {
319    /// Create a new position update from a futures position
320    #[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        // Verify that opposite() can be used in const contexts
382        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        // Long position profit
494        let pnl = position.get_pnl(dec!(51000.0));
495        assert_eq!(pnl, dec!(1000.0));
496
497        // Long position loss
498        let pnl = position.get_pnl(dec!(49000.0));
499        assert_eq!(pnl, dec!(-1000.0));
500
501        // Short position
502        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        // Short position profit
512        let pnl = short_position.get_pnl(dec!(49000.0));
513        assert_eq!(pnl, dec!(1000.0));
514
515        // Short position loss
516        let pnl = short_position.get_pnl(dec!(51000.0));
517        assert_eq!(pnl, dec!(-1000.0));
518
519        // Test hedge mode (PositionSide::Both) with positive amount (long)
520        let hedge_long_position = FuturesPosition::new(
521            Venue::Binance,
522            "BTCUSDT",
523            PositionSide::Both,
524            dec!(1.0), // Positive amount = long position
525            dec!(50000.0),
526            MarginType::Cross,
527        );
528
529        // Hedge mode long position profit
530        let pnl = hedge_long_position.get_pnl(dec!(51000.0));
531        assert_eq!(pnl, dec!(1000.0));
532
533        // Hedge mode long position loss
534        let pnl = hedge_long_position.get_pnl(dec!(49000.0));
535        assert_eq!(pnl, dec!(-1000.0));
536
537        // Test hedge mode (PositionSide::Both) with negative amount (short)
538        let hedge_short_position = FuturesPosition::new(
539            Venue::Binance,
540            "BTCUSDT",
541            PositionSide::Both,
542            dec!(-1.0), // Negative amount = short position
543            dec!(50000.0),
544            MarginType::Cross,
545        );
546
547        // Hedge mode short position profit (price goes down)
548        let pnl = hedge_short_position.get_pnl(dec!(49000.0));
549        assert_eq!(pnl, dec!(1000.0)); // (-1.0) * (-1000.0) = 1000.0
550
551        // Hedge mode short position loss (price goes up)
552        let pnl = hedge_short_position.get_pnl(dec!(51000.0));
553        assert_eq!(pnl, dec!(-1000.0)); // (-1.0) * (1000.0) = -1000.0
554    }
555
556    #[test]
557    fn test_futures_position_get_notional_value() {
558        // Test long position with positive amount
559        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)); // 1.5 * 52000
570
571        // Test short position with positive amount
572        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)); // 2.0 * 48000
583
584        // Test hedge mode with positive amount (long position)
585        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)); // 0.5 * 51000
596
597        // Test hedge mode with negative amount (short position)
598        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)); // |-1.0| * 49000 = 1.0 * 49000
609
610        // Test zero amount position
611        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)); // 0.0 * 55000 = 0
622
623        // Test with zero price
624        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)); // 1.0 * 0 = 0
635
636        // Test with fractional amounts and prices
637        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)); // 0.123 * 50123.45
648
649        // Test large amounts
650        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)); // 100.0 * 60000
661
662        // Test with isolated margin type
663        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)); // 2.5 * 51500
674    }
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        // Capture timestamp before and after creating the update
710        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        // Verify timestamp is not zero (indicating a valid timestamp)
715        assert_ne!(update.timestamp_ns, 0, "Timestamp should not be zero");
716
717        // Verify timestamp is within reasonable range (between before and after)
718        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        // Verify timestamp is in nanoseconds (should be a very large number for recent times)
727        // A timestamp from 2020 onwards should be > 1.6e18 nanoseconds
728        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}