use crate::error::{CoreError, Result};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarginAccount {
pub account_id: String,
pub user_id: String,
pub collateral: Decimal,
pub borrowed: Decimal,
pub positions: Vec<MarginPosition>,
pub max_leverage: Decimal,
pub maintenance_margin_ratio: Decimal,
pub initial_margin_ratio: Decimal,
pub status: MarginAccountStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MarginAccountStatus {
Active,
MarginCall,
Liquidating,
Suspended,
Closed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarginPosition {
pub position_id: String,
pub token_symbol: String,
pub size: Decimal,
pub entry_price: Decimal,
pub current_price: Decimal,
pub leverage: Decimal,
pub borrowed: Decimal,
pub unrealized_pnl: Decimal,
pub side: PositionSide,
pub opened_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PositionSide {
Long,
Short,
}
impl MarginPosition {
pub fn calculate_unrealized_pnl(&self) -> Decimal {
match self.side {
PositionSide::Long => (self.current_price - self.entry_price) * self.size.abs(),
PositionSide::Short => (self.entry_price - self.current_price) * self.size.abs(),
}
}
pub fn update_price(&mut self, current_price: Decimal) {
self.current_price = current_price;
self.unrealized_pnl = self.calculate_unrealized_pnl();
}
}
impl MarginAccount {
pub fn new(
account_id: String,
user_id: String,
collateral: Decimal,
max_leverage: Decimal,
) -> Self {
Self {
account_id,
user_id,
collateral,
borrowed: Decimal::ZERO,
positions: Vec::new(),
max_leverage,
maintenance_margin_ratio: dec!(0.25), initial_margin_ratio: dec!(0.5), status: MarginAccountStatus::Active,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
pub fn calculate_account_value(&self) -> Decimal {
let unrealized_pnl: Decimal = self.positions.iter().map(|p| p.unrealized_pnl).sum();
self.collateral + unrealized_pnl
}
pub fn calculate_equity(&self) -> Decimal {
self.calculate_account_value() - self.borrowed
}
pub fn calculate_margin_ratio(&self) -> Decimal {
let position_value: Decimal = self
.positions
.iter()
.map(|p| p.current_price * p.size.abs())
.sum();
if position_value.is_zero() {
return Decimal::ONE;
}
self.calculate_equity() / position_value
}
pub fn calculate_current_leverage(&self) -> Decimal {
let equity = self.calculate_equity();
if equity <= Decimal::ZERO {
return Decimal::MAX;
}
let position_value: Decimal = self
.positions
.iter()
.map(|p| p.current_price * p.size.abs())
.sum();
if position_value.is_zero() {
return Decimal::ZERO;
}
position_value / equity
}
pub fn is_margin_call(&self) -> bool {
let margin_ratio = self.calculate_margin_ratio();
margin_ratio < self.maintenance_margin_ratio && !self.positions.is_empty()
}
pub fn should_liquidate(&self) -> bool {
let equity = self.calculate_equity();
equity <= Decimal::ZERO || self.calculate_margin_ratio() < self.maintenance_margin_ratio
}
pub fn calculate_max_position_size(
&self,
price: Decimal,
leverage: Decimal,
) -> Result<Decimal> {
if leverage > self.max_leverage {
return Err(CoreError::Validation(format!(
"Leverage {} exceeds maximum {}",
leverage, self.max_leverage
)));
}
let available_equity = self.calculate_equity();
let max_position_value = available_equity * leverage;
Ok(max_position_value / price)
}
pub fn open_position(
&mut self,
position_id: String,
token_symbol: String,
size: Decimal,
price: Decimal,
leverage: Decimal,
side: PositionSide,
) -> Result<()> {
if leverage > self.max_leverage {
return Err(CoreError::Validation(format!(
"Leverage {} exceeds maximum {}",
leverage, self.max_leverage
)));
}
let position_value = size.abs() * price;
let required_collateral = position_value / leverage;
let available_equity = self.calculate_equity();
if required_collateral > available_equity {
return Err(CoreError::InsufficientBalance {
required: required_collateral,
available: available_equity,
});
}
let borrowed = position_value - required_collateral;
let position = MarginPosition {
position_id,
token_symbol,
size,
entry_price: price,
current_price: price,
leverage,
borrowed,
unrealized_pnl: Decimal::ZERO,
side,
opened_at: Utc::now(),
};
self.positions.push(position);
self.borrowed += borrowed;
self.updated_at = Utc::now();
if self.is_margin_call() {
self.status = MarginAccountStatus::MarginCall;
}
Ok(())
}
pub fn close_position(&mut self, position_id: &str, closing_price: Decimal) -> Result<Decimal> {
let position_idx = self
.positions
.iter()
.position(|p| p.position_id == position_id)
.ok_or_else(|| CoreError::NotFound(format!("Position {} not found", position_id)))?;
let mut position = self.positions.remove(position_idx);
position.update_price(closing_price);
let realized_pnl = position.unrealized_pnl;
self.collateral += realized_pnl;
self.borrowed -= position.borrowed;
self.updated_at = Utc::now();
if !self.is_margin_call() && self.status == MarginAccountStatus::MarginCall {
self.status = MarginAccountStatus::Active;
}
Ok(realized_pnl)
}
pub fn update_prices(&mut self, prices: &HashMap<String, Decimal>) {
for position in &mut self.positions {
if let Some(&price) = prices.get(&position.token_symbol) {
position.update_price(price);
}
}
self.updated_at = Utc::now();
if self.is_margin_call() {
self.status = MarginAccountStatus::MarginCall;
} else if self.status == MarginAccountStatus::MarginCall {
self.status = MarginAccountStatus::Active;
}
}
pub fn add_collateral(&mut self, amount: Decimal) -> Result<()> {
if amount <= Decimal::ZERO {
return Err(CoreError::Validation("Amount must be positive".to_string()));
}
self.collateral += amount;
self.updated_at = Utc::now();
if !self.is_margin_call() && self.status == MarginAccountStatus::MarginCall {
self.status = MarginAccountStatus::Active;
}
Ok(())
}
pub fn withdraw_collateral(&mut self, amount: Decimal) -> Result<()> {
if amount <= Decimal::ZERO {
return Err(CoreError::Validation("Amount must be positive".to_string()));
}
let available = self.calculate_equity();
if amount > available {
return Err(CoreError::InsufficientBalance {
required: amount,
available,
});
}
self.collateral -= amount;
self.updated_at = Utc::now();
if self.is_margin_call() {
return Err(CoreError::Validation(
"Withdrawal would trigger margin call".to_string(),
));
}
Ok(())
}
}
pub struct MarginTradingManager {
accounts: HashMap<String, MarginAccount>,
default_max_leverage: Decimal,
daily_interest_rate: Decimal,
}
impl MarginTradingManager {
pub fn new(default_max_leverage: Decimal, daily_interest_rate: Decimal) -> Self {
Self {
accounts: HashMap::new(),
default_max_leverage,
daily_interest_rate,
}
}
pub fn create_account(
&mut self,
account_id: String,
user_id: String,
collateral: Decimal,
max_leverage: Option<Decimal>,
) -> Result<MarginAccount> {
if self.accounts.contains_key(&account_id) {
return Err(CoreError::Validation(format!(
"Account {} already exists",
account_id
)));
}
let account = MarginAccount::new(
account_id.clone(),
user_id,
collateral,
max_leverage.unwrap_or(self.default_max_leverage),
);
self.accounts.insert(account_id, account.clone());
Ok(account)
}
pub fn get_account(&self, account_id: &str) -> Option<&MarginAccount> {
self.accounts.get(account_id)
}
pub fn get_account_mut(&mut self, account_id: &str) -> Option<&mut MarginAccount> {
self.accounts.get_mut(account_id)
}
pub fn process_margin_calls(&mut self, prices: &HashMap<String, Decimal>) -> Vec<String> {
let mut margin_call_accounts = Vec::new();
for (account_id, account) in &mut self.accounts {
account.update_prices(prices);
if account.is_margin_call() && account.status != MarginAccountStatus::MarginCall {
account.status = MarginAccountStatus::MarginCall;
margin_call_accounts.push(account_id.clone());
}
}
margin_call_accounts
}
pub fn get_liquidation_candidates(&self) -> Vec<String> {
self.accounts
.iter()
.filter(|(_, account)| account.should_liquidate())
.map(|(id, _)| id.clone())
.collect()
}
pub fn liquidate_account(
&mut self,
account_id: &str,
prices: &HashMap<String, Decimal>,
) -> Result<LiquidationResult> {
let account = self
.accounts
.get_mut(account_id)
.ok_or_else(|| CoreError::NotFound(format!("Account {} not found", account_id)))?;
account.status = MarginAccountStatus::Liquidating;
account.update_prices(prices);
let mut total_pnl = Decimal::ZERO;
let positions_closed = account.positions.len();
while !account.positions.is_empty() {
let position = account.positions.remove(0);
let _closing_price = prices
.get(&position.token_symbol)
.copied()
.unwrap_or(position.current_price);
total_pnl += position.calculate_unrealized_pnl();
account.borrowed -= position.borrowed;
}
account.collateral += total_pnl;
account.status = MarginAccountStatus::Closed;
account.updated_at = Utc::now();
Ok(LiquidationResult {
account_id: account_id.to_string(),
positions_closed,
total_pnl,
remaining_equity: account.calculate_equity(),
})
}
pub fn apply_interest(&mut self) {
for account in self.accounts.values_mut() {
let interest = account.borrowed * self.daily_interest_rate;
account.borrowed += interest;
account.updated_at = Utc::now();
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidationResult {
pub account_id: String,
pub positions_closed: usize,
pub total_pnl: Decimal,
pub remaining_equity: Decimal,
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_margin_account_creation() {
let account =
MarginAccount::new("acc1".to_string(), "user1".to_string(), dec!(1000), dec!(5));
assert_eq!(account.collateral, dec!(1000));
assert_eq!(account.max_leverage, dec!(5));
assert_eq!(account.status, MarginAccountStatus::Active);
}
#[test]
fn test_open_long_position() {
let mut account = MarginAccount::new(
"acc1".to_string(),
"user1".to_string(),
dec!(30000), dec!(5),
);
account
.open_position(
"pos1".to_string(),
"BTC".to_string(),
dec!(1),
dec!(50000),
dec!(2),
PositionSide::Long,
)
.unwrap();
assert_eq!(account.positions.len(), 1);
assert!(account.borrowed > Decimal::ZERO);
}
#[test]
fn test_margin_call() {
let mut account = MarginAccount::new(
"acc1".to_string(),
"user1".to_string(),
dec!(15000), dec!(5),
);
account
.open_position(
"pos1".to_string(),
"BTC".to_string(),
dec!(1),
dec!(50000),
dec!(4),
PositionSide::Long,
)
.unwrap();
let mut prices = HashMap::new();
prices.insert("BTC".to_string(), dec!(40000));
account.update_prices(&prices);
assert!(account.is_margin_call());
}
#[test]
fn test_close_position_with_profit() {
let mut account = MarginAccount::new(
"acc1".to_string(),
"user1".to_string(),
dec!(30000), dec!(5),
);
account
.open_position(
"pos1".to_string(),
"BTC".to_string(),
dec!(1),
dec!(50000),
dec!(2),
PositionSide::Long,
)
.unwrap();
let initial_collateral = account.collateral;
let pnl = account.close_position("pos1", dec!(55000)).unwrap();
assert!(pnl > Decimal::ZERO);
assert!(account.collateral > initial_collateral);
assert_eq!(account.positions.len(), 0);
}
#[test]
fn test_margin_trading_manager() {
let mut manager = MarginTradingManager::new(dec!(5), dec!(0.001));
let account = manager
.create_account("acc1".to_string(), "user1".to_string(), dec!(1000), None)
.unwrap();
assert_eq!(account.account_id, "acc1");
assert!(manager.get_account("acc1").is_some());
}
}