use serde::{Deserialize, Serialize};
use solana_program::pubkey::Pubkey;
use crate::errors::{ProgramError, Result};
use crate::state::{Position, PositionSide, PositionStatus};
use crate::constants::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PositionUpdate {
pub position_id: Pubkey,
pub new_price: u64,
pub unrealized_pnl: i64,
pub health_factor: u16,
}
pub struct PositionManager;
impl PositionManager {
pub fn open_position(
trader: Pubkey,
market_id: Pubkey,
side: PositionSide,
size: u64,
entry_price: u64,
collateral: u64,
leverage: u8,
) -> Result<Position> {
if leverage < MIN_LEVERAGE || leverage > MAX_LEVERAGE {
return Err(ProgramError::ExcessiveLeverage);
}
if collateral < MIN_COLLATERAL {
return Err(ProgramError::InsufficientCollateral);
}
if size == 0 {
return Err(ProgramError::InvalidAmount);
}
let position = Position::new(
Pubkey::new_unique(),
trader,
market_id,
side,
size,
entry_price,
collateral,
leverage,
);
Ok(position)
}
pub fn calculate_pnl(position: &Position, current_price: u64) -> Result<i64> {
let price_diff = match position.side {
PositionSide::Long => {
if current_price >= position.entry_price {
(current_price - position.entry_price) as i64
} else {
-((position.entry_price - current_price) as i64)
}
}
PositionSide::Short => {
if current_price <= position.entry_price {
(position.entry_price - current_price) as i64
} else {
-((current_price - position.entry_price) as i64)
}
}
};
let position_value = (position.size as i128 * price_diff as i128) / POS_SCALE;
Ok(position_value as i64)
}
pub fn calculate_pnl_percent(pnl: i64, collateral: u64) -> Result<i32> {
if collateral == 0 {
return Ok(0);
}
let percent = (pnl as i128 * 10_000) / collateral as i128;
Ok(percent as i32)
}
pub fn calculate_unrealized_margin(position: &Position, current_price: u64) -> Result<u64> {
let pnl = Self::calculate_pnl(position, current_price)?;
if pnl < 0 {
let abs_loss = (-pnl) as u64;
Ok(position.collateral.saturating_sub(abs_loss))
} else {
Ok(position.collateral.saturating_add(pnl as u64))
}
}
pub fn can_close_position(position: &Position) -> Result<bool> {
if position.status != PositionStatus::Open {
return Err(ProgramError::PositionNotFound);
}
Ok(true)
}
pub fn close_position(
position: &mut Position,
exit_price: u64,
) -> Result<(i64, u64)> {
Self::can_close_position(position)?;
let pnl = Self::calculate_pnl(position, exit_price)?;
let settlement = if pnl > 0 {
position.collateral + (pnl as u64)
} else {
position.collateral.saturating_sub((-pnl) as u64)
};
position.current_price = exit_price;
position.pnl = pnl;
position.pnl_percent = Self::calculate_pnl_percent(pnl, position.collateral)?;
position.status = PositionStatus::Closed;
Ok((pnl, settlement))
}
pub fn update_position_price(
position: &mut Position,
new_price: u64,
) -> Result<u16> {
position.current_price = new_price;
position.pnl = Self::calculate_pnl(position, new_price)?;
position.pnl_percent = Self::calculate_pnl_percent(position.pnl, position.collateral)?;
let health_factor = Self::calculate_health_factor(position, new_price)?;
Ok(health_factor)
}
pub fn calculate_health_factor(position: &Position, current_price: u64) -> Result<u16> {
let margin = Self::calculate_unrealized_margin(position, current_price)?;
if position.collateral == 0 {
return Ok(0);
}
let factor = ((margin as u128 * 10_000) / position.collateral as u128) as u16;
Ok(factor.min(10_000))
}
pub fn is_liquidatable(position: &Position, current_price: u64) -> Result<bool> {
let health = Self::calculate_health_factor(position, current_price)?;
Ok(health < MAINTENANCE_MARGIN_BPS)
}
pub fn calculate_position_value(position: &Position) -> Result<u64> {
Ok((position.size as u128 * position.current_price as u128 / POS_SCALE as u128) as u64)
}
pub fn calculate_exposure(position: &Position) -> Result<u64> {
let exposure = (position.size as u128 * position.leverage as u128) / 1u128;
Ok(exposure as u64)
}
pub fn validate_position_sizing(
size: u64,
collateral: u64,
leverage: u8,
entry_price: u64,
) -> Result<()> {
if size == 0 || collateral == 0 {
return Err(ProgramError::InvalidAmount);
}
let position_value = (size as u128 * entry_price as u128) / POS_SCALE as u128;
let max_position = (collateral as u128 * leverage as u128) / 1u128;
if position_value > max_position {
return Err(ProgramError::ExcessiveLeverage);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_position() {
let result = PositionManager::open_position(
Pubkey::new_unique(),
Pubkey::new_unique(),
PositionSide::Long,
1_000_000,
100,
1_000_000,
10,
);
assert!(result.is_ok());
}
#[test]
fn test_calculate_pnl_long() {
let trader = Pubkey::new_unique();
let market = Pubkey::new_unique();
let position = Position::new(
Pubkey::new_unique(),
trader,
market,
PositionSide::Long,
1_000_000,
100,
1_000_000,
10,
);
let pnl = PositionManager::calculate_pnl(&position, 110);
assert!(pnl.is_ok());
}
}