use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::error::{CoreError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PositionSide {
Long,
Short,
}
impl PositionSide {
pub fn multiplier(&self) -> Decimal {
match self {
PositionSide::Long => dec!(1),
PositionSide::Short => dec!(-1),
}
}
pub fn opposite(&self) -> Self {
match self {
PositionSide::Long => PositionSide::Short,
PositionSide::Short => PositionSide::Long,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MarginMode {
Isolated,
Cross,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerpetualPosition {
pub id: String,
pub user_id: String,
pub symbol: String,
pub side: PositionSide,
pub size: Decimal,
pub entry_price: Decimal,
pub leverage: Decimal,
pub margin_mode: MarginMode,
pub initial_margin: Decimal,
pub maintenance_margin: Decimal,
pub unrealized_pnl: Decimal,
pub realized_pnl: Decimal,
pub liquidation_price: Decimal,
pub funding_payments: Decimal,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl PerpetualPosition {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
user_id: String,
symbol: String,
side: PositionSide,
size: Decimal,
entry_price: Decimal,
leverage: Decimal,
margin_mode: MarginMode,
) -> Result<Self> {
if size <= dec!(0) {
return Err(CoreError::Validation("Size must be positive".to_string()));
}
if entry_price <= dec!(0) {
return Err(CoreError::Validation(
"Entry price must be positive".to_string(),
));
}
if leverage < dec!(1) || leverage > dec!(125) {
return Err(CoreError::Validation(
"Leverage must be between 1x and 125x".to_string(),
));
}
let notional_value = size * entry_price;
let initial_margin = notional_value / leverage;
let maintenance_margin = initial_margin * dec!(0.5);
let liquidation_price = Self::calculate_liquidation_price(
side,
entry_price,
initial_margin,
maintenance_margin,
size,
)?;
let now = Utc::now();
Ok(Self {
id,
user_id,
symbol,
side,
size,
entry_price,
leverage,
margin_mode,
initial_margin,
maintenance_margin,
unrealized_pnl: dec!(0),
realized_pnl: dec!(0),
liquidation_price,
funding_payments: dec!(0),
created_at: now,
updated_at: now,
})
}
fn calculate_liquidation_price(
side: PositionSide,
entry_price: Decimal,
initial_margin: Decimal,
maintenance_margin: Decimal,
size: Decimal,
) -> Result<Decimal> {
if size == dec!(0) {
return Err(CoreError::Validation(
"Cannot calculate liquidation for zero size".to_string(),
));
}
let margin_diff = initial_margin - maintenance_margin;
match side {
PositionSide::Long => {
Ok(entry_price - (margin_diff / size))
}
PositionSide::Short => {
Ok(entry_price + (margin_diff / size))
}
}
}
pub fn update_unrealized_pnl(&mut self, mark_price: Decimal) {
let price_diff = mark_price - self.entry_price;
self.unrealized_pnl = self.side.multiplier() * price_diff * self.size;
self.updated_at = Utc::now();
}
pub fn should_liquidate(&self, mark_price: Decimal) -> bool {
match self.side {
PositionSide::Long => mark_price <= self.liquidation_price,
PositionSide::Short => mark_price >= self.liquidation_price,
}
}
pub fn margin_ratio(&self) -> Decimal {
let total_margin = self.initial_margin + self.unrealized_pnl - self.funding_payments;
if total_margin <= dec!(0) {
return dec!(0);
}
total_margin / (self.size * self.entry_price)
}
pub fn apply_funding(&mut self, funding_rate: Decimal, mark_price: Decimal) {
let notional = self.size * mark_price;
let payment = self.side.multiplier() * funding_rate * notional;
self.funding_payments += payment;
self.updated_at = Utc::now();
}
pub fn close(&mut self, exit_price: Decimal) -> Decimal {
let price_diff = exit_price - self.entry_price;
let pnl = self.side.multiplier() * price_diff * self.size;
self.realized_pnl += pnl + self.unrealized_pnl - self.funding_payments;
self.unrealized_pnl = dec!(0);
self.size = dec!(0);
self.updated_at = Utc::now();
self.realized_pnl
}
pub fn reduce_size(&mut self, reduce_amount: Decimal, exit_price: Decimal) -> Result<Decimal> {
if reduce_amount > self.size {
return Err(CoreError::Validation(
"Reduce amount exceeds position size".to_string(),
));
}
let price_diff = exit_price - self.entry_price;
let pnl = self.side.multiplier() * price_diff * reduce_amount;
self.realized_pnl += pnl;
self.size -= reduce_amount;
let size_ratio = self.size / (self.size + reduce_amount);
self.initial_margin *= size_ratio;
self.maintenance_margin *= size_ratio;
self.liquidation_price = Self::calculate_liquidation_price(
self.side,
self.entry_price,
self.initial_margin,
self.maintenance_margin,
self.size,
)?;
self.updated_at = Utc::now();
Ok(pnl)
}
pub fn increase_size(&mut self, add_amount: Decimal, add_price: Decimal) -> Result<()> {
if add_amount <= dec!(0) {
return Err(CoreError::Validation(
"Add amount must be positive".to_string(),
));
}
let old_notional = self.size * self.entry_price;
let new_notional = add_amount * add_price;
let total_size = self.size + add_amount;
self.entry_price = (old_notional + new_notional) / total_size;
self.size = total_size;
let add_notional = add_amount * add_price;
let add_margin = add_notional / self.leverage;
self.initial_margin += add_margin;
self.maintenance_margin = self.initial_margin * dec!(0.5);
self.liquidation_price = Self::calculate_liquidation_price(
self.side,
self.entry_price,
self.initial_margin,
self.maintenance_margin,
self.size,
)?;
self.updated_at = Utc::now();
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingRate {
pub symbol: String,
pub rate: Decimal,
pub mark_price: Decimal,
pub index_price: Decimal,
pub premium: Decimal,
pub next_funding_time: DateTime<Utc>,
pub timestamp: DateTime<Utc>,
}
impl FundingRate {
pub fn new(
symbol: String,
mark_price: Decimal,
index_price: Decimal,
funding_interval_hours: i64,
) -> Self {
let premium = mark_price - index_price;
let rate = if index_price > dec!(0) {
let raw_rate = premium / index_price;
raw_rate.max(dec!(-0.0005)).min(dec!(0.0005))
} else {
dec!(0)
};
let now = Utc::now();
let next_funding_time = now + chrono::Duration::hours(funding_interval_hours);
Self {
symbol,
rate,
mark_price,
index_price,
premium,
next_funding_time,
timestamp: now,
}
}
pub fn update(&mut self, mark_price: Decimal, index_price: Decimal) {
self.mark_price = mark_price;
self.index_price = index_price;
self.premium = mark_price - index_price;
self.rate = if index_price > dec!(0) {
let raw_rate = self.premium / index_price;
raw_rate.max(dec!(-0.0005)).min(dec!(0.0005))
} else {
dec!(0)
};
self.timestamp = Utc::now();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsuranceFund {
pub balance: Decimal,
pub total_contributions: Decimal,
pub total_payouts: Decimal,
pub liquidations_covered: u64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl InsuranceFund {
pub fn new(initial_balance: Decimal) -> Self {
let now = Utc::now();
Self {
balance: initial_balance,
total_contributions: initial_balance,
total_payouts: dec!(0),
liquidations_covered: 0,
created_at: now,
updated_at: now,
}
}
pub fn add_contribution(&mut self, amount: Decimal) -> Result<()> {
if amount < dec!(0) {
return Err(CoreError::Validation(
"Contribution must be non-negative".to_string(),
));
}
self.balance += amount;
self.total_contributions += amount;
self.updated_at = Utc::now();
Ok(())
}
pub fn cover_bankruptcy(&mut self, loss: Decimal) -> Result<bool> {
if loss < dec!(0) {
return Err(CoreError::Validation(
"Loss must be non-negative".to_string(),
));
}
if self.balance >= loss {
self.balance -= loss;
self.total_payouts += loss;
self.liquidations_covered += 1;
self.updated_at = Utc::now();
Ok(true)
} else {
Ok(false)
}
}
pub fn health_ratio(&self) -> Decimal {
if self.total_payouts == dec!(0) {
return dec!(100);
}
(self.balance / self.total_payouts) * dec!(100)
}
}
#[derive(Debug)]
pub struct PerpetualManager {
positions: HashMap<String, Vec<PerpetualPosition>>,
funding_rates: HashMap<String, FundingRate>,
insurance_fund: InsuranceFund,
funding_interval_hours: i64,
}
impl PerpetualManager {
pub fn new(initial_insurance_balance: Decimal, funding_interval_hours: i64) -> Self {
Self {
positions: HashMap::new(),
funding_rates: HashMap::new(),
insurance_fund: InsuranceFund::new(initial_insurance_balance),
funding_interval_hours,
}
}
#[allow(clippy::too_many_arguments)]
pub fn open_position(
&mut self,
id: String,
user_id: String,
symbol: String,
side: PositionSide,
size: Decimal,
entry_price: Decimal,
leverage: Decimal,
margin_mode: MarginMode,
) -> Result<PerpetualPosition> {
let position = PerpetualPosition::new(
id,
user_id.clone(),
symbol,
side,
size,
entry_price,
leverage,
margin_mode,
)?;
self.positions
.entry(user_id)
.or_default()
.push(position.clone());
Ok(position)
}
pub fn get_user_positions(&self, user_id: &str) -> Vec<&PerpetualPosition> {
self.positions
.get(user_id)
.map(|positions| positions.iter().collect())
.unwrap_or_default()
}
pub fn update_funding_rate(
&mut self,
symbol: String,
mark_price: Decimal,
index_price: Decimal,
) {
self.funding_rates
.entry(symbol.clone())
.and_modify(|fr| fr.update(mark_price, index_price))
.or_insert_with(|| {
FundingRate::new(symbol, mark_price, index_price, self.funding_interval_hours)
});
}
pub fn apply_funding(&mut self, symbol: &str) -> Result<()> {
let funding_rate = self.funding_rates.get(symbol).ok_or_else(|| {
CoreError::Validation(format!("No funding rate found for {}", symbol))
})?;
let rate = funding_rate.rate;
let mark_price = funding_rate.mark_price;
for positions in self.positions.values_mut() {
for position in positions.iter_mut() {
if position.symbol == symbol && position.size > dec!(0) {
position.apply_funding(rate, mark_price);
}
}
}
Ok(())
}
pub fn liquidate_positions(
&mut self,
symbol: &str,
mark_price: Decimal,
) -> Result<Vec<String>> {
let mut liquidated_ids = Vec::new();
for (user_id, positions) in self.positions.iter_mut() {
for position in positions.iter_mut() {
if position.symbol == symbol && position.should_liquidate(mark_price) {
position.update_unrealized_pnl(mark_price);
let loss = position.initial_margin + position.unrealized_pnl;
if loss < dec!(0) {
let bankruptcy_loss = -loss;
if !self.insurance_fund.cover_bankruptcy(bankruptcy_loss)? {
}
}
liquidated_ids.push(format!("{}:{}", user_id, position.id));
position.close(mark_price);
}
}
}
Ok(liquidated_ids)
}
pub fn close_position(
&mut self,
user_id: &str,
position_id: &str,
exit_price: Decimal,
) -> Result<Decimal> {
let positions = self.positions.get_mut(user_id).ok_or_else(|| {
CoreError::Validation(format!("No positions found for user {}", user_id))
})?;
let position = positions
.iter_mut()
.find(|p| p.id == position_id)
.ok_or_else(|| CoreError::Validation(format!("Position {} not found", position_id)))?;
Ok(position.close(exit_price))
}
pub fn get_funding_rate(&self, symbol: &str) -> Option<&FundingRate> {
self.funding_rates.get(symbol)
}
pub fn get_insurance_fund(&self) -> &InsuranceFund {
&self.insurance_fund
}
pub fn add_insurance_contribution(&mut self, amount: Decimal) -> Result<()> {
self.insurance_fund.add_contribution(amount)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perpetual_position_creation() {
let position = PerpetualPosition::new(
"pos1".to_string(),
"user1".to_string(),
"BTC-PERP".to_string(),
PositionSide::Long,
dec!(1),
dec!(50000),
dec!(10),
MarginMode::Isolated,
)
.unwrap();
assert_eq!(position.size, dec!(1));
assert_eq!(position.entry_price, dec!(50000));
assert_eq!(position.leverage, dec!(10));
assert_eq!(position.initial_margin, dec!(5000)); }
#[test]
fn test_unrealized_pnl_long() {
let mut position = PerpetualPosition::new(
"pos1".to_string(),
"user1".to_string(),
"BTC-PERP".to_string(),
PositionSide::Long,
dec!(1),
dec!(50000),
dec!(10),
MarginMode::Isolated,
)
.unwrap();
position.update_unrealized_pnl(dec!(51000));
assert_eq!(position.unrealized_pnl, dec!(1000)); }
#[test]
fn test_unrealized_pnl_short() {
let mut position = PerpetualPosition::new(
"pos1".to_string(),
"user1".to_string(),
"BTC-PERP".to_string(),
PositionSide::Short,
dec!(1),
dec!(50000),
dec!(10),
MarginMode::Isolated,
)
.unwrap();
position.update_unrealized_pnl(dec!(49000));
assert_eq!(position.unrealized_pnl, dec!(1000)); }
#[test]
fn test_liquidation_long() {
let position = PerpetualPosition::new(
"pos1".to_string(),
"user1".to_string(),
"BTC-PERP".to_string(),
PositionSide::Long,
dec!(1),
dec!(50000),
dec!(10),
MarginMode::Isolated,
)
.unwrap();
assert!(position.liquidation_price < dec!(50000));
assert!(position.should_liquidate(position.liquidation_price - dec!(1)));
}
#[test]
fn test_funding_rate_calculation() {
let funding = FundingRate::new(
"BTC-PERP".to_string(),
dec!(50100), dec!(50000), 8,
);
assert!(funding.rate > dec!(0));
assert!(funding.rate <= dec!(0.0005)); }
#[test]
fn test_funding_payment() {
let mut position = PerpetualPosition::new(
"pos1".to_string(),
"user1".to_string(),
"BTC-PERP".to_string(),
PositionSide::Long,
dec!(1),
dec!(50000),
dec!(10),
MarginMode::Isolated,
)
.unwrap();
position.apply_funding(dec!(0.0001), dec!(50000));
assert!(position.funding_payments > dec!(0));
}
#[test]
fn test_position_close() {
let mut position = PerpetualPosition::new(
"pos1".to_string(),
"user1".to_string(),
"BTC-PERP".to_string(),
PositionSide::Long,
dec!(1),
dec!(50000),
dec!(10),
MarginMode::Isolated,
)
.unwrap();
let realized_pnl = position.close(dec!(51000));
assert_eq!(realized_pnl, dec!(1000)); assert_eq!(position.size, dec!(0));
}
#[test]
fn test_insurance_fund() {
let mut fund = InsuranceFund::new(dec!(100000));
fund.add_contribution(dec!(10000)).unwrap();
assert_eq!(fund.balance, dec!(110000));
let covered = fund.cover_bankruptcy(dec!(5000)).unwrap();
assert!(covered);
assert_eq!(fund.balance, dec!(105000));
assert_eq!(fund.liquidations_covered, 1);
}
#[test]
fn test_perpetual_manager() {
let mut manager = PerpetualManager::new(dec!(100000), 8);
let position = manager
.open_position(
"pos1".to_string(),
"user1".to_string(),
"BTC-PERP".to_string(),
PositionSide::Long,
dec!(1),
dec!(50000),
dec!(10),
MarginMode::Isolated,
)
.unwrap();
assert_eq!(position.size, dec!(1));
let user_positions = manager.get_user_positions("user1");
assert_eq!(user_positions.len(), 1);
}
}