Skip to main content

hypercall_margin/standard/
types.rs

1use rust_decimal::Decimal;
2use rust_decimal_macros::dec;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default)]
6pub struct StandardAccount {
7    pub wallet: String,
8    pub usdc_balance: Decimal,
9    pub perp_positions: Vec<PerpPosition>,
10    pub option_positions: Vec<OptionPosition>,
11}
12
13impl StandardAccount {
14    pub fn new(wallet: String, usdc_balance: Decimal) -> Self {
15        Self {
16            wallet,
17            usdc_balance,
18            perp_positions: Vec::new(),
19            option_positions: Vec::new(),
20        }
21    }
22
23    pub fn has_positions(&self) -> bool {
24        !self.perp_positions.is_empty() || !self.option_positions.is_empty()
25    }
26
27    pub fn short_options(&self) -> impl Iterator<Item = &OptionPosition> {
28        self.option_positions.iter().filter(|p| p.size < dec!(0))
29    }
30
31    pub fn long_options(&self) -> impl Iterator<Item = &OptionPosition> {
32        self.option_positions.iter().filter(|p| p.size > dec!(0))
33    }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct PerpPosition {
38    pub symbol: String,
39    pub underlying: String,
40    pub size: Decimal,
41    pub mark_price: Decimal,
42    pub entry_price: Decimal,
43}
44
45impl PerpPosition {
46    pub fn unrealized_pnl(&self) -> Decimal {
47        (self.mark_price - self.entry_price) * self.size
48    }
49
50    pub fn notional(&self) -> Decimal {
51        self.size.abs() * self.mark_price
52    }
53
54    pub fn is_long(&self) -> bool {
55        self.size > dec!(0)
56    }
57
58    pub fn is_short(&self) -> bool {
59        self.size < dec!(0)
60    }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct OptionPosition {
65    pub symbol: String,
66    pub underlying: String,
67    pub expiry_ts: i64,
68    pub strike: Decimal,
69    pub is_call: bool,
70    pub size: Decimal,
71    pub mark_price: Decimal,
72    pub entry_price: Decimal,
73    pub spot_price: Decimal,
74}
75
76impl OptionPosition {
77    pub fn signed_market_value(&self) -> Decimal {
78        self.mark_price * self.size
79    }
80
81    pub fn unrealized_pnl(&self) -> Decimal {
82        (self.mark_price - self.entry_price) * self.size
83    }
84
85    pub fn otm_amount(&self) -> Decimal {
86        if self.is_call {
87            (self.strike - self.spot_price).max(dec!(0))
88        } else {
89            (self.spot_price - self.strike).max(dec!(0))
90        }
91    }
92
93    pub fn is_long(&self) -> bool {
94        self.size > dec!(0)
95    }
96
97    pub fn is_short(&self) -> bool {
98        self.size < dec!(0)
99    }
100
101    pub fn abs_size(&self) -> Decimal {
102        self.size.abs()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_perp_position_unrealized_pnl() {
112        let long_profit = PerpPosition {
113            symbol: "ETH-PERP".to_string(),
114            underlying: "ETH".to_string(),
115            size: dec!(10),
116            mark_price: dec!(3500),
117            entry_price: dec!(3000),
118        };
119        assert_eq!(long_profit.unrealized_pnl(), dec!(5000));
120
121        let short_loss = PerpPosition {
122            symbol: "ETH-PERP".to_string(),
123            underlying: "ETH".to_string(),
124            size: dec!(-10),
125            mark_price: dec!(3500),
126            entry_price: dec!(3000),
127        };
128        assert_eq!(short_loss.unrealized_pnl(), dec!(-5000));
129    }
130
131    #[test]
132    fn test_option_position_otm_amount() {
133        let itm_call = OptionPosition {
134            symbol: "ETH-20251231-3000-C".to_string(),
135            underlying: "ETH".to_string(),
136            expiry_ts: 0,
137            strike: dec!(3000),
138            is_call: true,
139            size: dec!(1),
140            mark_price: dec!(600),
141            entry_price: dec!(500),
142            spot_price: dec!(3500),
143        };
144        assert_eq!(itm_call.otm_amount(), dec!(0));
145
146        let otm_call = OptionPosition {
147            symbol: "ETH-20251231-4000-C".to_string(),
148            underlying: "ETH".to_string(),
149            expiry_ts: 0,
150            strike: dec!(4000),
151            is_call: true,
152            size: dec!(1),
153            mark_price: dec!(100),
154            entry_price: dec!(150),
155            spot_price: dec!(3500),
156        };
157        assert_eq!(otm_call.otm_amount(), dec!(500));
158
159        let otm_put = OptionPosition {
160            symbol: "ETH-20251231-3000-P".to_string(),
161            underlying: "ETH".to_string(),
162            expiry_ts: 0,
163            strike: dec!(3000),
164            is_call: false,
165            size: dec!(1),
166            mark_price: dec!(50),
167            entry_price: dec!(100),
168            spot_price: dec!(3500),
169        };
170        assert_eq!(otm_put.otm_amount(), dec!(500));
171    }
172}