precolator-program 1.0.0

Core Rust library for the Precolator perpetual futures trading protocol on Solana — oracle management, position handling, risk engine, and liquidation system.
Documentation
// Position module - Opening, closing, and managing trading positions

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 {
    /// Open a new position
    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)
    }

    /// Calculate unrealized P&L for a 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)
    }

    /// Calculate P&L percentage
    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)
    }

    /// Calculate unrealized margin
    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))
        }
    }

    /// Check if position can be closed
    pub fn can_close_position(position: &Position) -> Result<bool> {
        if position.status != PositionStatus::Open {
            return Err(ProgramError::PositionNotFound);
        }
        Ok(true)
    }

    /// Close a position and calculate settlement
    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))
    }

    /// Update position price and calculate health factor
    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)
    }

    /// Calculate health factor (0-10000 = 0-100%)
    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))
    }

    /// Check if position should be liquidated
    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)
    }

    /// Calculate position value
    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)
    }

    /// Calculate position exposure (size * leverage)
    pub fn calculate_exposure(position: &Position) -> Result<u64> {
        let exposure = (position.size as u128 * position.leverage as u128) / 1u128;
        Ok(exposure as u64)
    }

    /// Validate position size relative to leverage
    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());
    }
}