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, Serialize, Deserialize)]
pub struct CrossMarginPosition {
pub id: String,
pub symbol: String,
pub side: Decimal,
pub size: Decimal,
pub entry_price: Decimal,
pub mark_price: Decimal,
pub unrealized_pnl: Decimal,
pub leverage: Decimal,
pub position_margin: Decimal,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl CrossMarginPosition {
pub fn new(
id: String,
symbol: String,
side: Decimal,
size: Decimal,
entry_price: Decimal,
leverage: Decimal,
) -> 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!(0) {
return Err(CoreError::Validation(
"Leverage must be positive".to_string(),
));
}
let notional = size * entry_price;
let position_margin = notional / leverage;
let now = Utc::now();
Ok(Self {
id,
symbol,
side,
size,
entry_price,
mark_price: entry_price,
unrealized_pnl: dec!(0),
leverage,
position_margin,
created_at: now,
updated_at: now,
})
}
pub fn update_mark_price(&mut self, mark_price: Decimal) {
self.mark_price = mark_price;
let price_diff = mark_price - self.entry_price;
self.unrealized_pnl = self.side * price_diff * self.size;
self.updated_at = Utc::now();
}
pub fn notional_value(&self) -> Decimal {
self.size * self.mark_price
}
pub fn margin_ratio(&self) -> Decimal {
let equity = self.position_margin + self.unrealized_pnl;
if self.notional_value() == dec!(0) {
return dec!(100);
}
(equity / self.notional_value()) * dec!(100)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortfolioMargin {
pub initial_margin: Decimal,
pub maintenance_margin: Decimal,
pub risk_adjusted_margin: Decimal,
pub utilization: Decimal,
}
impl PortfolioMargin {
pub fn meets_initial_margin(&self, available_balance: Decimal) -> bool {
available_balance >= self.initial_margin
}
pub fn meets_maintenance_margin(&self, account_equity: Decimal) -> bool {
account_equity >= self.maintenance_margin
}
pub fn excess_margin(&self, available_balance: Decimal) -> Decimal {
(available_balance - self.initial_margin).max(dec!(0))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiskFactor {
pub base: Decimal,
pub concentration: Decimal,
pub correlation_benefit: Decimal,
}
impl RiskFactor {
pub fn effective(&self) -> Decimal {
self.base * self.concentration * self.correlation_benefit
}
}
impl Default for RiskFactor {
fn default() -> Self {
Self {
base: dec!(1.0),
concentration: dec!(1.0),
correlation_benefit: dec!(1.0),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossMarginAccount {
pub id: String,
pub user_id: String,
pub collateral: Decimal,
pub positions: HashMap<String, CrossMarginPosition>,
pub total_unrealized_pnl: Decimal,
pub equity: Decimal,
pub margin: PortfolioMargin,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl CrossMarginAccount {
pub fn new(id: String, user_id: String, initial_collateral: Decimal) -> Result<Self> {
if initial_collateral < dec!(0) {
return Err(CoreError::Validation(
"Initial collateral cannot be negative".to_string(),
));
}
let now = Utc::now();
Ok(Self {
id,
user_id,
collateral: initial_collateral,
positions: HashMap::new(),
total_unrealized_pnl: dec!(0),
equity: initial_collateral,
margin: PortfolioMargin {
initial_margin: dec!(0),
maintenance_margin: dec!(0),
risk_adjusted_margin: dec!(0),
utilization: dec!(0),
},
created_at: now,
updated_at: now,
})
}
pub fn add_position(&mut self, position: CrossMarginPosition) -> Result<()> {
if self.positions.contains_key(&position.id) {
return Err(CoreError::Validation("Position already exists".to_string()));
}
self.positions.insert(position.id.clone(), position);
self.recalculate_margin()?;
Ok(())
}
pub fn remove_position(&mut self, position_id: &str) -> Result<CrossMarginPosition> {
let position = self
.positions
.remove(position_id)
.ok_or_else(|| CoreError::Validation("Position not found".to_string()))?;
self.recalculate_margin()?;
Ok(position)
}
pub fn update_mark_prices(&mut self, prices: &HashMap<String, Decimal>) -> Result<()> {
for position in self.positions.values_mut() {
if let Some(&mark_price) = prices.get(&position.symbol) {
position.update_mark_price(mark_price);
}
}
self.recalculate_margin()?;
Ok(())
}
fn recalculate_margin(&mut self) -> Result<()> {
self.total_unrealized_pnl = self.positions.values().map(|p| p.unrealized_pnl).sum();
self.equity = self.collateral + self.total_unrealized_pnl;
let mut total_position_margin = dec!(0);
let mut total_notional = dec!(0);
for position in self.positions.values() {
total_position_margin += position.position_margin;
total_notional += position.notional_value();
}
let netting_benefit = self.calculate_netting_benefit();
self.margin.risk_adjusted_margin = total_position_margin * netting_benefit;
self.margin.initial_margin = self.margin.risk_adjusted_margin;
self.margin.maintenance_margin = self.margin.initial_margin * dec!(0.5);
if self.equity > dec!(0) {
self.margin.utilization = (self.margin.initial_margin / self.equity) * dec!(100);
} else {
self.margin.utilization = dec!(999); }
self.updated_at = Utc::now();
Ok(())
}
fn calculate_netting_benefit(&self) -> Decimal {
let mut gross_exposure = dec!(0);
let mut symbol_exposures: HashMap<String, Decimal> = HashMap::new();
for position in self.positions.values() {
let exposure = position.side * position.notional_value();
gross_exposure += exposure.abs();
*symbol_exposures
.entry(position.symbol.clone())
.or_insert(dec!(0)) += exposure;
}
let net_exposure: Decimal = symbol_exposures.values().map(|e| e.abs()).sum();
if gross_exposure == dec!(0) {
return dec!(1.0);
}
let netting_ratio = net_exposure / gross_exposure;
dec!(0.5) + (netting_ratio * dec!(0.5))
}
pub fn can_open_position(&self, required_margin: Decimal) -> bool {
let available_margin = self.equity - self.margin.initial_margin;
available_margin >= required_margin
}
pub fn is_liquidatable(&self) -> bool {
self.equity < self.margin.maintenance_margin
}
pub fn available_balance(&self) -> Decimal {
(self.equity - self.margin.initial_margin).max(dec!(0))
}
pub fn margin_call_level(&self) -> Decimal {
if self.margin.initial_margin == dec!(0) {
return dec!(100);
}
(self.margin.maintenance_margin / self.margin.initial_margin) * dec!(100)
}
pub fn add_collateral(&mut self, amount: Decimal) -> Result<()> {
if amount <= dec!(0) {
return Err(CoreError::Validation("Amount must be positive".to_string()));
}
self.collateral += amount;
self.recalculate_margin()?;
Ok(())
}
pub fn withdraw_collateral(&mut self, amount: Decimal) -> Result<()> {
if amount <= dec!(0) {
return Err(CoreError::Validation("Amount must be positive".to_string()));
}
let new_collateral = self.collateral - amount;
let new_equity = new_collateral + self.total_unrealized_pnl;
if new_equity < self.margin.initial_margin {
return Err(CoreError::Validation(
"Withdrawal would violate margin requirements".to_string(),
));
}
self.collateral = new_collateral;
self.recalculate_margin()?;
Ok(())
}
pub fn get_summary(&self) -> PortfolioSummary {
PortfolioSummary {
account_id: self.id.clone(),
collateral: self.collateral,
equity: self.equity,
unrealized_pnl: self.total_unrealized_pnl,
initial_margin: self.margin.initial_margin,
maintenance_margin: self.margin.maintenance_margin,
available_balance: self.available_balance(),
margin_utilization: self.margin.utilization,
position_count: self.positions.len(),
is_liquidatable: self.is_liquidatable(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortfolioSummary {
pub account_id: String,
pub collateral: Decimal,
pub equity: Decimal,
pub unrealized_pnl: Decimal,
pub initial_margin: Decimal,
pub maintenance_margin: Decimal,
pub available_balance: Decimal,
pub margin_utilization: Decimal,
pub position_count: usize,
pub is_liquidatable: bool,
}
#[derive(Debug)]
pub struct CrossMarginManager {
accounts: HashMap<String, CrossMarginAccount>,
accounts_by_user: HashMap<String, Vec<String>>,
}
impl CrossMarginManager {
pub fn new() -> Self {
Self {
accounts: HashMap::new(),
accounts_by_user: HashMap::new(),
}
}
pub fn create_account(
&mut self,
account_id: String,
user_id: String,
initial_collateral: Decimal,
) -> Result<()> {
if self.accounts.contains_key(&account_id) {
return Err(CoreError::Validation("Account already exists".to_string()));
}
let account =
CrossMarginAccount::new(account_id.clone(), user_id.clone(), initial_collateral)?;
self.accounts.insert(account_id.clone(), account);
self.accounts_by_user
.entry(user_id)
.or_default()
.push(account_id);
Ok(())
}
pub fn get_account(&self, account_id: &str) -> Option<&CrossMarginAccount> {
self.accounts.get(account_id)
}
pub fn get_account_mut(&mut self, account_id: &str) -> Option<&mut CrossMarginAccount> {
self.accounts.get_mut(account_id)
}
pub fn get_user_accounts(&self, user_id: &str) -> Vec<&CrossMarginAccount> {
self.accounts_by_user
.get(user_id)
.map(|ids| ids.iter().filter_map(|id| self.accounts.get(id)).collect())
.unwrap_or_default()
}
pub fn update_all_mark_prices(&mut self, prices: &HashMap<String, Decimal>) -> Result<()> {
for account in self.accounts.values_mut() {
account.update_mark_prices(prices)?;
}
Ok(())
}
pub fn get_liquidatable_accounts(&self) -> Vec<&CrossMarginAccount> {
self.accounts
.values()
.filter(|acc| acc.is_liquidatable())
.collect()
}
}
impl Default for CrossMarginManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cross_margin_position() {
let position = CrossMarginPosition::new(
"pos1".to_string(),
"BTC".to_string(),
dec!(1), dec!(1),
dec!(50000),
dec!(10),
)
.unwrap();
assert_eq!(position.size, dec!(1));
assert_eq!(position.entry_price, dec!(50000));
assert_eq!(position.position_margin, dec!(5000)); }
#[test]
fn test_update_mark_price() {
let mut position = CrossMarginPosition::new(
"pos1".to_string(),
"BTC".to_string(),
dec!(1),
dec!(1),
dec!(50000),
dec!(10),
)
.unwrap();
position.update_mark_price(dec!(51000));
assert_eq!(position.unrealized_pnl, dec!(1000)); }
#[test]
fn test_cross_margin_account() {
let account =
CrossMarginAccount::new("acc1".to_string(), "user1".to_string(), dec!(10000)).unwrap();
assert_eq!(account.collateral, dec!(10000));
assert_eq!(account.equity, dec!(10000));
}
#[test]
fn test_add_position() {
let mut account =
CrossMarginAccount::new("acc1".to_string(), "user1".to_string(), dec!(10000)).unwrap();
let position = CrossMarginPosition::new(
"pos1".to_string(),
"BTC".to_string(),
dec!(1),
dec!(1),
dec!(50000),
dec!(10),
)
.unwrap();
account.add_position(position).unwrap();
assert_eq!(account.positions.len(), 1);
assert!(account.margin.initial_margin > dec!(0));
}
#[test]
fn test_netting_benefit() {
let mut account =
CrossMarginAccount::new("acc1".to_string(), "user1".to_string(), dec!(20000)).unwrap();
let pos1 = CrossMarginPosition::new(
"pos1".to_string(),
"BTC".to_string(),
dec!(1), dec!(1),
dec!(50000),
dec!(10),
)
.unwrap();
let pos2 = CrossMarginPosition::new(
"pos2".to_string(),
"BTC".to_string(),
dec!(-1), dec!(0.5),
dec!(50000),
dec!(10),
)
.unwrap();
account.add_position(pos1).unwrap();
account.add_position(pos2).unwrap();
let benefit = account.calculate_netting_benefit();
assert!(benefit < dec!(1.0)); }
#[test]
fn test_collateral_operations() {
let mut account =
CrossMarginAccount::new("acc1".to_string(), "user1".to_string(), dec!(10000)).unwrap();
account.add_collateral(dec!(5000)).unwrap();
assert_eq!(account.collateral, dec!(15000));
account.withdraw_collateral(dec!(3000)).unwrap();
assert_eq!(account.collateral, dec!(12000));
}
#[test]
fn test_cross_margin_manager() {
let mut manager = CrossMarginManager::new();
manager
.create_account("acc1".to_string(), "user1".to_string(), dec!(10000))
.unwrap();
assert!(manager.get_account("acc1").is_some());
let user_accounts = manager.get_user_accounts("user1");
assert_eq!(user_accounts.len(), 1);
}
}