use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct AccountSummary {
pub currency: String,
pub balance: f64,
pub equity: f64,
pub available_funds: f64,
pub margin_balance: f64,
pub unrealized_pnl: f64,
pub realized_pnl: f64,
pub total_pl: f64,
pub session_funding: f64,
pub session_rpl: f64,
pub session_upl: f64,
pub maintenance_margin: f64,
pub initial_margin: f64,
pub available_withdrawal_funds: Option<f64>,
pub cross_collateral_enabled: Option<bool>,
pub delta_total: Option<f64>,
pub futures_pl: Option<f64>,
pub futures_session_rpl: Option<f64>,
pub futures_session_upl: Option<f64>,
pub options_delta: Option<f64>,
pub options_gamma: Option<f64>,
pub options_pl: Option<f64>,
pub options_session_rpl: Option<f64>,
pub options_session_upl: Option<f64>,
pub options_theta: Option<f64>,
pub options_vega: Option<f64>,
pub portfolio_margining_enabled: Option<bool>,
pub projected_delta_total: Option<f64>,
pub projected_initial_margin: Option<f64>,
pub projected_maintenance_margin: Option<f64>,
pub system_name: Option<String>,
#[serde(rename = "type")]
pub account_type: String,
pub delta_total_map: std::collections::HashMap<String, f64>,
pub deposit_address: String,
pub fees: Vec<std::collections::HashMap<String, f64>>,
pub limits: std::collections::HashMap<String, f64>,
}
impl AccountSummary {
pub fn margin_utilization(&self) -> f64 {
if self.equity != 0.0 {
(self.initial_margin / self.equity) * 100.0
} else {
0.0
}
}
pub fn available_margin(&self) -> f64 {
self.equity - self.initial_margin
}
pub fn is_at_risk(&self, threshold: f64) -> bool {
self.margin_utilization() > threshold
}
pub fn return_on_equity(&self) -> f64 {
if self.equity != 0.0 {
(self.total_pl / self.equity) * 100.0
} else {
0.0
}
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct Subaccount {
pub email: String,
pub id: u64,
pub login_enabled: bool,
pub portfolio: Option<PortfolioInfo>,
pub receive_notifications: bool,
pub system_name: String,
pub tif: Option<String>,
#[serde(rename = "type")]
pub subaccount_type: String,
pub username: String,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct PortfolioInfo {
pub available_funds: f64,
pub available_withdrawal_funds: f64,
pub balance: f64,
pub currency: String,
pub delta_total: f64,
pub equity: f64,
pub initial_margin: f64,
pub maintenance_margin: f64,
pub margin_balance: f64,
pub session_rpl: f64,
pub session_upl: f64,
pub total_pl: f64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct Portfolio {
pub currency: String,
pub accounts: Vec<AccountSummary>,
pub total_usd_value: Option<f64>,
pub cross_margin_enabled: bool,
}
impl Portfolio {
pub fn new(currency: String) -> Self {
Self {
currency,
accounts: Vec::new(),
total_usd_value: None,
cross_margin_enabled: false,
}
}
pub fn add_account(&mut self, account: AccountSummary) {
self.accounts.push(account);
}
pub fn get_account(&self, currency: &String) -> Option<&AccountSummary> {
self.accounts.iter().find(|acc| &acc.currency == currency)
}
pub fn total_equity(&self) -> f64 {
self.accounts.iter().map(|acc| acc.equity).sum()
}
pub fn total_unrealized_pnl(&self) -> f64 {
self.accounts.iter().map(|acc| acc.unrealized_pnl).sum()
}
pub fn total_realized_pnl(&self) -> f64 {
self.accounts.iter().map(|acc| acc.realized_pnl).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn create_test_account_summary() -> AccountSummary {
AccountSummary {
currency: "BTC".to_string(),
balance: 1.5,
equity: 1.4,
available_funds: 1.2,
margin_balance: 0.3,
unrealized_pnl: -0.1,
realized_pnl: 0.05,
total_pl: -0.05,
session_funding: 0.001,
session_rpl: 0.02,
session_upl: -0.08,
maintenance_margin: 0.1,
initial_margin: 0.2,
available_withdrawal_funds: Some(1.0),
cross_collateral_enabled: Some(true),
delta_total: Some(0.5),
futures_pl: Some(0.03),
futures_session_rpl: Some(0.01),
futures_session_upl: Some(-0.02),
options_delta: Some(0.3),
options_gamma: Some(0.05),
options_pl: Some(-0.08),
options_session_rpl: Some(0.01),
options_session_upl: Some(-0.06),
options_theta: Some(-0.02),
options_vega: Some(0.1),
portfolio_margining_enabled: Some(false),
projected_delta_total: Some(0.6),
projected_initial_margin: Some(0.25),
projected_maintenance_margin: Some(0.12),
system_name: Some("deribit".to_string()),
account_type: "main".to_string(),
delta_total_map: HashMap::new(),
deposit_address: "bc1qtest123".to_string(),
fees: vec![HashMap::new()],
limits: HashMap::new(),
}
}
#[test]
fn test_account_summary_margin_utilization() {
let account = create_test_account_summary();
let utilization = account.margin_utilization();
assert!((utilization - 14.285714285714286).abs() < 0.0001); }
#[test]
fn test_account_summary_margin_utilization_zero_equity() {
let mut account = create_test_account_summary();
account.equity = 0.0;
assert_eq!(account.margin_utilization(), 0.0);
}
#[test]
fn test_account_summary_available_margin() {
let account = create_test_account_summary();
assert_eq!(account.available_margin(), 1.2); }
#[test]
fn test_account_summary_is_at_risk() {
let account = create_test_account_summary();
assert!(!account.is_at_risk(20.0)); assert!(account.is_at_risk(10.0)); }
#[test]
fn test_account_summary_return_on_equity() {
let account = create_test_account_summary();
let roe = account.return_on_equity();
assert!((roe - (-3.571428571428571)).abs() < 0.0001); }
#[test]
fn test_account_summary_return_on_equity_zero_equity() {
let mut account = create_test_account_summary();
account.equity = 0.0;
assert_eq!(account.return_on_equity(), 0.0);
}
#[test]
fn test_portfolio_new() {
let portfolio = Portfolio::new("USD".to_string());
assert_eq!(portfolio.currency, "USD");
assert!(portfolio.accounts.is_empty());
assert_eq!(portfolio.total_usd_value, None);
assert!(!portfolio.cross_margin_enabled);
}
#[test]
fn test_portfolio_add_account() {
let mut portfolio = Portfolio::new("USD".to_string());
let account = create_test_account_summary();
portfolio.add_account(account);
assert_eq!(portfolio.accounts.len(), 1);
}
#[test]
fn test_portfolio_get_account() {
let mut portfolio = Portfolio::new("USD".to_string());
let account = create_test_account_summary();
portfolio.add_account(account);
let found = portfolio.get_account(&"BTC".to_string());
assert!(found.is_some());
assert_eq!(found.unwrap().currency, "BTC");
let not_found = portfolio.get_account(&"ETH".to_string());
assert!(not_found.is_none());
}
#[test]
fn test_portfolio_total_equity() {
let mut portfolio = Portfolio::new("USD".to_string());
let mut account1 = create_test_account_summary();
account1.equity = 1.0;
let mut account2 = create_test_account_summary();
account2.equity = 2.0;
portfolio.add_account(account1);
portfolio.add_account(account2);
assert_eq!(portfolio.total_equity(), 3.0);
}
#[test]
fn test_portfolio_total_unrealized_pnl() {
let mut portfolio = Portfolio::new("USD".to_string());
let mut account1 = create_test_account_summary();
account1.unrealized_pnl = 0.1;
let mut account2 = create_test_account_summary();
account2.unrealized_pnl = -0.2;
portfolio.add_account(account1);
portfolio.add_account(account2);
assert_eq!(portfolio.total_unrealized_pnl(), -0.1);
}
#[test]
fn test_portfolio_total_realized_pnl() {
let mut portfolio = Portfolio::new("USD".to_string());
let mut account1 = create_test_account_summary();
account1.realized_pnl = 0.05;
let mut account2 = create_test_account_summary();
account2.realized_pnl = 0.03;
portfolio.add_account(account1);
portfolio.add_account(account2);
assert_eq!(portfolio.total_realized_pnl(), 0.08);
}
#[test]
fn test_account_summary_serialization() {
let account = create_test_account_summary();
let json = serde_json::to_string(&account).unwrap();
let deserialized: AccountSummary = serde_json::from_str(&json).unwrap();
assert_eq!(account.currency, deserialized.currency);
assert_eq!(account.balance, deserialized.balance);
}
#[test]
fn test_portfolio_serialization() {
let portfolio = Portfolio::new("USD".to_string());
let json = serde_json::to_string(&portfolio).unwrap();
let deserialized: Portfolio = serde_json::from_str(&json).unwrap();
assert_eq!(portfolio.currency, deserialized.currency);
}
#[test]
fn test_subaccount_creation() {
let subaccount = Subaccount {
email: "test@example.com".to_string(),
id: 12345,
login_enabled: true,
portfolio: None,
receive_notifications: false,
system_name: "deribit".to_string(),
tif: Some("GTC".to_string()),
subaccount_type: "subaccount".to_string(),
username: "testuser".to_string(),
};
assert_eq!(subaccount.email, "test@example.com");
assert_eq!(subaccount.id, 12345);
assert!(subaccount.login_enabled);
}
#[test]
fn test_portfolio_info_creation() {
let portfolio_info = PortfolioInfo {
available_funds: 1000.0,
available_withdrawal_funds: 900.0,
balance: 1100.0,
currency: "BTC".to_string(),
delta_total: 0.5,
equity: 1050.0,
initial_margin: 100.0,
maintenance_margin: 50.0,
margin_balance: 150.0,
session_rpl: 10.0,
session_upl: -5.0,
total_pl: 5.0,
};
assert_eq!(portfolio_info.currency, "BTC");
assert_eq!(portfolio_info.balance, 1100.0);
}
#[test]
fn test_debug_and_display_implementations() {
let account = create_test_account_summary();
let debug_str = format!("{:?}", account);
let display_str = format!("{}", account);
assert!(debug_str.contains("BTC"));
assert!(display_str.contains("BTC"));
}
}