hypercall_margin/standard/
types.rs1use 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}