Skip to main content

precolator/
position.rs

1// Position module - Opening, closing, and managing trading positions
2
3use 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    /// Open a new position
21    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    /// Calculate unrealized P&L for a position
57    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    /// Calculate P&L percentage
80    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    /// Calculate unrealized margin
90    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    /// Check if position can be closed
102    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    /// Close a position and calculate settlement
110    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    /// Update position price and calculate health factor
133    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    /// Calculate health factor (0-10000 = 0-100%)
146    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    /// Check if position should be liquidated
158    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    /// Calculate position value
164    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    /// Calculate position exposure (size * leverage)
169    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    /// Validate position size relative to leverage
175    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}