1use serde::{Deserialize, Serialize};
4use solana_program::pubkey::Pubkey;
5use crate::errors::{ProgramError, Result};
6use crate::state::{Position, PositionSide, PositionStatus};
7use crate::constants::*;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PositionUpdate {
11 pub position_id: Pubkey,
12 pub new_price: u64,
13 pub unrealized_pnl: i64,
14 pub health_factor: u16,
15}
16
17pub struct PositionManager;
18
19impl PositionManager {
20 pub fn open_position(
22 trader: Pubkey,
23 market_id: Pubkey,
24 side: PositionSide,
25 size: u64,
26 entry_price: u64,
27 collateral: u64,
28 leverage: u8,
29 ) -> Result<Position> {
30 if leverage < MIN_LEVERAGE || leverage > MAX_LEVERAGE {
31 return Err(ProgramError::ExcessiveLeverage);
32 }
33
34 if collateral < MIN_COLLATERAL {
35 return Err(ProgramError::InsufficientCollateral);
36 }
37
38 if size == 0 {
39 return Err(ProgramError::InvalidAmount);
40 }
41
42 let position = Position::new(
43 Pubkey::new_unique(),
44 trader,
45 market_id,
46 side,
47 size,
48 entry_price,
49 collateral,
50 leverage,
51 );
52
53 Ok(position)
54 }
55
56 pub fn calculate_pnl(position: &Position, current_price: u64) -> Result<i64> {
58 let price_diff = match position.side {
59 PositionSide::Long => {
60 if current_price >= position.entry_price {
61 (current_price - position.entry_price) as i64
62 } else {
63 -((position.entry_price - current_price) as i64)
64 }
65 }
66 PositionSide::Short => {
67 if current_price <= position.entry_price {
68 (position.entry_price - current_price) as i64
69 } else {
70 -((current_price - position.entry_price) as i64)
71 }
72 }
73 };
74
75 let position_value = (position.size as i128 * price_diff as i128) / POS_SCALE;
76 Ok(position_value as i64)
77 }
78
79 pub fn calculate_pnl_percent(pnl: i64, collateral: u64) -> Result<i32> {
81 if collateral == 0 {
82 return Ok(0);
83 }
84
85 let percent = (pnl as i128 * 10_000) / collateral as i128;
86 Ok(percent as i32)
87 }
88
89 pub fn calculate_unrealized_margin(position: &Position, current_price: u64) -> Result<u64> {
91 let pnl = Self::calculate_pnl(position, current_price)?;
92
93 if pnl < 0 {
94 let abs_loss = (-pnl) as u64;
95 Ok(position.collateral.saturating_sub(abs_loss))
96 } else {
97 Ok(position.collateral.saturating_add(pnl as u64))
98 }
99 }
100
101 pub fn can_close_position(position: &Position) -> Result<bool> {
103 if position.status != PositionStatus::Open {
104 return Err(ProgramError::PositionNotFound);
105 }
106 Ok(true)
107 }
108
109 pub fn close_position(
111 position: &mut Position,
112 exit_price: u64,
113 ) -> Result<(i64, u64)> {
114 Self::can_close_position(position)?;
115
116 let pnl = Self::calculate_pnl(position, exit_price)?;
117
118 let settlement = if pnl > 0 {
119 position.collateral + (pnl as u64)
120 } else {
121 position.collateral.saturating_sub((-pnl) as u64)
122 };
123
124 position.current_price = exit_price;
125 position.pnl = pnl;
126 position.pnl_percent = Self::calculate_pnl_percent(pnl, position.collateral)?;
127 position.status = PositionStatus::Closed;
128
129 Ok((pnl, settlement))
130 }
131
132 pub fn update_position_price(
134 position: &mut Position,
135 new_price: u64,
136 ) -> Result<u16> {
137 position.current_price = new_price;
138 position.pnl = Self::calculate_pnl(position, new_price)?;
139 position.pnl_percent = Self::calculate_pnl_percent(position.pnl, position.collateral)?;
140
141 let health_factor = Self::calculate_health_factor(position, new_price)?;
142 Ok(health_factor)
143 }
144
145 pub fn calculate_health_factor(position: &Position, current_price: u64) -> Result<u16> {
147 let margin = Self::calculate_unrealized_margin(position, current_price)?;
148
149 if position.collateral == 0 {
150 return Ok(0);
151 }
152
153 let factor = ((margin as u128 * 10_000) / position.collateral as u128) as u16;
154 Ok(factor.min(10_000))
155 }
156
157 pub fn is_liquidatable(position: &Position, current_price: u64) -> Result<bool> {
159 let health = Self::calculate_health_factor(position, current_price)?;
160 Ok(health < MAINTENANCE_MARGIN_BPS)
161 }
162
163 pub fn calculate_position_value(position: &Position) -> Result<u64> {
165 Ok((position.size as u128 * position.current_price as u128 / POS_SCALE as u128) as u64)
166 }
167
168 pub fn calculate_exposure(position: &Position) -> Result<u64> {
170 let exposure = (position.size as u128 * position.leverage as u128) / 1u128;
171 Ok(exposure as u64)
172 }
173
174 pub fn validate_position_sizing(
176 size: u64,
177 collateral: u64,
178 leverage: u8,
179 entry_price: u64,
180 ) -> Result<()> {
181 if size == 0 || collateral == 0 {
182 return Err(ProgramError::InvalidAmount);
183 }
184
185 let position_value = (size as u128 * entry_price as u128) / POS_SCALE as u128;
186 let max_position = (collateral as u128 * leverage as u128) / 1u128;
187
188 if position_value > max_position {
189 return Err(ProgramError::ExcessiveLeverage);
190 }
191
192 Ok(())
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_open_position() {
202 let result = PositionManager::open_position(
203 Pubkey::new_unique(),
204 Pubkey::new_unique(),
205 PositionSide::Long,
206 1_000_000,
207 100,
208 1_000_000,
209 10,
210 );
211 assert!(result.is_ok());
212 }
213
214 #[test]
215 fn test_calculate_pnl_long() {
216 let trader = Pubkey::new_unique();
217 let market = Pubkey::new_unique();
218 let position = Position::new(
219 Pubkey::new_unique(),
220 trader,
221 market,
222 PositionSide::Long,
223 1_000_000,
224 100,
225 1_000_000,
226 10,
227 );
228
229 let pnl = PositionManager::calculate_pnl(&position, 110);
230 assert!(pnl.is_ok());
231 }
232}