deribit_base/model/
account.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/7/25
5******************************************************************************/
6use pretty_simple_display::{DebugPretty, DisplaySimple};
7use serde::{Deserialize, Serialize};
8
9/// Account summary information
10#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
11pub struct AccountSummary {
12    /// Account currency (kept as Currencies enum for compatibility)
13    pub currency: String,
14    /// Total balance
15    pub balance: f64,
16    /// Account equity
17    pub equity: f64,
18    /// Available funds for trading
19    pub available_funds: f64,
20    /// Margin balance
21    pub margin_balance: f64,
22    /// Unrealized profit and loss
23    pub unrealized_pnl: f64,
24    /// Realized profit and loss
25    pub realized_pnl: f64,
26    /// Total profit and loss
27    pub total_pl: f64,
28    /// Session funding
29    pub session_funding: f64,
30    /// Session realized P&L
31    pub session_rpl: f64,
32    /// Session unrealized P&L
33    pub session_upl: f64,
34    /// Maintenance margin requirement
35    pub maintenance_margin: f64,
36    /// Initial margin requirement
37    pub initial_margin: f64,
38    /// Available withdrawal funds
39    pub available_withdrawal_funds: Option<f64>,
40    /// Cross collateral enabled
41    pub cross_collateral_enabled: Option<bool>,
42    /// Delta total
43    pub delta_total: Option<f64>,
44    /// Futures profit and loss
45    pub futures_pl: Option<f64>,
46    /// Futures session realized profit and loss
47    pub futures_session_rpl: Option<f64>,
48    /// Futures session unrealized profit and loss
49    pub futures_session_upl: Option<f64>,
50    /// Options delta
51    pub options_delta: Option<f64>,
52    /// Options gamma
53    pub options_gamma: Option<f64>,
54    /// Options profit and loss
55    pub options_pl: Option<f64>,
56    /// Options session realized profit and loss
57    pub options_session_rpl: Option<f64>,
58    /// Options session unrealized profit and loss
59    pub options_session_upl: Option<f64>,
60    /// Options theta
61    pub options_theta: Option<f64>,
62    /// Options vega
63    pub options_vega: Option<f64>,
64    /// Portfolio margin enabled
65    pub portfolio_margining_enabled: Option<bool>,
66    /// Projected delta total
67    pub projected_delta_total: Option<f64>,
68    /// Projected initial margin
69    pub projected_initial_margin: Option<f64>,
70    /// Projected maintenance margin
71    pub projected_maintenance_margin: Option<f64>,
72    /// System name
73    pub system_name: Option<String>,
74    /// Type of account
75    #[serde(rename = "type")]
76    pub account_type: String,
77    // Additional fields from deribit-http types.rs
78    /// Delta total map (currency -> delta)
79    pub delta_total_map: std::collections::HashMap<String, f64>,
80    /// Deposit address
81    pub deposit_address: String,
82    /// Fees structure
83    pub fees: Vec<std::collections::HashMap<String, f64>>,
84    /// Account limits
85    pub limits: std::collections::HashMap<String, f64>,
86}
87
88impl AccountSummary {
89    /// Calculate margin utilization as percentage
90    pub fn margin_utilization(&self) -> f64 {
91        if self.equity != 0.0 {
92            (self.initial_margin / self.equity) * 100.0
93        } else {
94            0.0
95        }
96    }
97
98    /// Calculate available margin
99    pub fn available_margin(&self) -> f64 {
100        self.equity - self.initial_margin
101    }
102
103    /// Check if account is at risk (high margin utilization)
104    pub fn is_at_risk(&self, threshold: f64) -> bool {
105        self.margin_utilization() > threshold
106    }
107
108    /// Calculate return on equity
109    pub fn return_on_equity(&self) -> f64 {
110        if self.equity != 0.0 {
111            (self.total_pl / self.equity) * 100.0
112        } else {
113            0.0
114        }
115    }
116}
117
118/// Subaccount information
119#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
120pub struct Subaccount {
121    /// Subaccount email
122    pub email: String,
123    /// Subaccount ID
124    pub id: u64,
125    /// Whether login is enabled
126    pub login_enabled: bool,
127    /// Portfolio information (optional)
128    pub portfolio: Option<PortfolioInfo>,
129    /// Whether to receive notifications
130    pub receive_notifications: bool,
131    /// System name
132    pub system_name: String,
133    /// Time in force (optional)
134    pub tif: Option<String>,
135    /// Subaccount type
136    #[serde(rename = "type")]
137    pub subaccount_type: String,
138    /// Username
139    pub username: String,
140}
141
142/// Portfolio information
143#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
144pub struct PortfolioInfo {
145    /// Available funds
146    pub available_funds: f64,
147    /// Available withdrawal funds
148    pub available_withdrawal_funds: f64,
149    /// Balance
150    pub balance: f64,
151    /// Currency
152    pub currency: String,
153    /// Delta total
154    pub delta_total: f64,
155    /// Equity
156    pub equity: f64,
157    /// Initial margin
158    pub initial_margin: f64,
159    /// Maintenance margin
160    pub maintenance_margin: f64,
161    /// Margin balance
162    pub margin_balance: f64,
163    /// Session realized P&L
164    pub session_rpl: f64,
165    /// Session unrealized P&L
166    pub session_upl: f64,
167    /// Total P&L
168    pub total_pl: f64,
169}
170
171/// Portfolio information
172#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
173pub struct Portfolio {
174    /// Currency of the portfolio
175    pub currency: String,
176    /// Account summaries for different currencies
177    pub accounts: Vec<AccountSummary>,
178    /// Total portfolio value in USD
179    pub total_usd_value: Option<f64>,
180    /// Cross-currency margin enabled
181    pub cross_margin_enabled: bool,
182}
183
184impl Portfolio {
185    /// Create a new empty portfolio
186    pub fn new(currency: String) -> Self {
187        Self {
188            currency,
189            accounts: Vec::new(),
190            total_usd_value: None,
191            cross_margin_enabled: false,
192        }
193    }
194
195    /// Add an account summary to the portfolio
196    pub fn add_account(&mut self, account: AccountSummary) {
197        self.accounts.push(account);
198    }
199
200    /// Get account summary for a specific currency
201    pub fn get_account(&self, currency: &String) -> Option<&AccountSummary> {
202        self.accounts.iter().find(|acc| &acc.currency == currency)
203    }
204
205    /// Calculate total equity across all accounts
206    pub fn total_equity(&self) -> f64 {
207        self.accounts.iter().map(|acc| acc.equity).sum()
208    }
209
210    /// Calculate total unrealized PnL across all accounts
211    pub fn total_unrealized_pnl(&self) -> f64 {
212        self.accounts.iter().map(|acc| acc.unrealized_pnl).sum()
213    }
214
215    /// Calculate total realized PnL across all accounts
216    pub fn total_realized_pnl(&self) -> f64 {
217        self.accounts.iter().map(|acc| acc.realized_pnl).sum()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use std::collections::HashMap;
225
226    fn create_test_account_summary() -> AccountSummary {
227        AccountSummary {
228            currency: "BTC".to_string(),
229            balance: 1.5,
230            equity: 1.4,
231            available_funds: 1.2,
232            margin_balance: 0.3,
233            unrealized_pnl: -0.1,
234            realized_pnl: 0.05,
235            total_pl: -0.05,
236            session_funding: 0.001,
237            session_rpl: 0.02,
238            session_upl: -0.08,
239            maintenance_margin: 0.1,
240            initial_margin: 0.2,
241            available_withdrawal_funds: Some(1.0),
242            cross_collateral_enabled: Some(true),
243            delta_total: Some(0.5),
244            futures_pl: Some(0.03),
245            futures_session_rpl: Some(0.01),
246            futures_session_upl: Some(-0.02),
247            options_delta: Some(0.3),
248            options_gamma: Some(0.05),
249            options_pl: Some(-0.08),
250            options_session_rpl: Some(0.01),
251            options_session_upl: Some(-0.06),
252            options_theta: Some(-0.02),
253            options_vega: Some(0.1),
254            portfolio_margining_enabled: Some(false),
255            projected_delta_total: Some(0.6),
256            projected_initial_margin: Some(0.25),
257            projected_maintenance_margin: Some(0.12),
258            system_name: Some("deribit".to_string()),
259            account_type: "main".to_string(),
260            delta_total_map: HashMap::new(),
261            deposit_address: "bc1qtest123".to_string(),
262            fees: vec![HashMap::new()],
263            limits: HashMap::new(),
264        }
265    }
266
267    #[test]
268    fn test_account_summary_margin_utilization() {
269        let account = create_test_account_summary();
270        let utilization = account.margin_utilization();
271        assert!((utilization - 14.285714285714286).abs() < 0.0001); // 0.2 / 1.4 * 100
272    }
273
274    #[test]
275    fn test_account_summary_margin_utilization_zero_equity() {
276        let mut account = create_test_account_summary();
277        account.equity = 0.0;
278        assert_eq!(account.margin_utilization(), 0.0);
279    }
280
281    #[test]
282    fn test_account_summary_available_margin() {
283        let account = create_test_account_summary();
284        assert_eq!(account.available_margin(), 1.2); // 1.4 - 0.2
285    }
286
287    #[test]
288    fn test_account_summary_is_at_risk() {
289        let account = create_test_account_summary();
290        assert!(!account.is_at_risk(20.0)); // 14.28% < 20%
291        assert!(account.is_at_risk(10.0)); // 14.28% > 10%
292    }
293
294    #[test]
295    fn test_account_summary_return_on_equity() {
296        let account = create_test_account_summary();
297        let roe = account.return_on_equity();
298        assert!((roe - (-3.571428571428571)).abs() < 0.0001); // -0.05 / 1.4 * 100
299    }
300
301    #[test]
302    fn test_account_summary_return_on_equity_zero_equity() {
303        let mut account = create_test_account_summary();
304        account.equity = 0.0;
305        assert_eq!(account.return_on_equity(), 0.0);
306    }
307
308    #[test]
309    fn test_portfolio_new() {
310        let portfolio = Portfolio::new("USD".to_string());
311        assert_eq!(portfolio.currency, "USD");
312        assert!(portfolio.accounts.is_empty());
313        assert_eq!(portfolio.total_usd_value, None);
314        assert!(!portfolio.cross_margin_enabled);
315    }
316
317    #[test]
318    fn test_portfolio_add_account() {
319        let mut portfolio = Portfolio::new("USD".to_string());
320        let account = create_test_account_summary();
321        portfolio.add_account(account);
322        assert_eq!(portfolio.accounts.len(), 1);
323    }
324
325    #[test]
326    fn test_portfolio_get_account() {
327        let mut portfolio = Portfolio::new("USD".to_string());
328        let account = create_test_account_summary();
329        portfolio.add_account(account);
330
331        let found = portfolio.get_account(&"BTC".to_string());
332        assert!(found.is_some());
333        assert_eq!(found.unwrap().currency, "BTC");
334
335        let not_found = portfolio.get_account(&"ETH".to_string());
336        assert!(not_found.is_none());
337    }
338
339    #[test]
340    fn test_portfolio_total_equity() {
341        let mut portfolio = Portfolio::new("USD".to_string());
342        let mut account1 = create_test_account_summary();
343        account1.equity = 1.0;
344        let mut account2 = create_test_account_summary();
345        account2.equity = 2.0;
346
347        portfolio.add_account(account1);
348        portfolio.add_account(account2);
349
350        assert_eq!(portfolio.total_equity(), 3.0);
351    }
352
353    #[test]
354    fn test_portfolio_total_unrealized_pnl() {
355        let mut portfolio = Portfolio::new("USD".to_string());
356        let mut account1 = create_test_account_summary();
357        account1.unrealized_pnl = 0.1;
358        let mut account2 = create_test_account_summary();
359        account2.unrealized_pnl = -0.2;
360
361        portfolio.add_account(account1);
362        portfolio.add_account(account2);
363
364        assert_eq!(portfolio.total_unrealized_pnl(), -0.1);
365    }
366
367    #[test]
368    fn test_portfolio_total_realized_pnl() {
369        let mut portfolio = Portfolio::new("USD".to_string());
370        let mut account1 = create_test_account_summary();
371        account1.realized_pnl = 0.05;
372        let mut account2 = create_test_account_summary();
373        account2.realized_pnl = 0.03;
374
375        portfolio.add_account(account1);
376        portfolio.add_account(account2);
377
378        assert_eq!(portfolio.total_realized_pnl(), 0.08);
379    }
380
381    #[test]
382    fn test_account_summary_serialization() {
383        let account = create_test_account_summary();
384        let json = serde_json::to_string(&account).unwrap();
385        let deserialized: AccountSummary = serde_json::from_str(&json).unwrap();
386        assert_eq!(account.currency, deserialized.currency);
387        assert_eq!(account.balance, deserialized.balance);
388    }
389
390    #[test]
391    fn test_portfolio_serialization() {
392        let portfolio = Portfolio::new("USD".to_string());
393        let json = serde_json::to_string(&portfolio).unwrap();
394        let deserialized: Portfolio = serde_json::from_str(&json).unwrap();
395        assert_eq!(portfolio.currency, deserialized.currency);
396    }
397
398    #[test]
399    fn test_subaccount_creation() {
400        let subaccount = Subaccount {
401            email: "test@example.com".to_string(),
402            id: 12345,
403            login_enabled: true,
404            portfolio: None,
405            receive_notifications: false,
406            system_name: "deribit".to_string(),
407            tif: Some("GTC".to_string()),
408            subaccount_type: "subaccount".to_string(),
409            username: "testuser".to_string(),
410        };
411
412        assert_eq!(subaccount.email, "test@example.com");
413        assert_eq!(subaccount.id, 12345);
414        assert!(subaccount.login_enabled);
415    }
416
417    #[test]
418    fn test_portfolio_info_creation() {
419        let portfolio_info = PortfolioInfo {
420            available_funds: 1000.0,
421            available_withdrawal_funds: 900.0,
422            balance: 1100.0,
423            currency: "BTC".to_string(),
424            delta_total: 0.5,
425            equity: 1050.0,
426            initial_margin: 100.0,
427            maintenance_margin: 50.0,
428            margin_balance: 150.0,
429            session_rpl: 10.0,
430            session_upl: -5.0,
431            total_pl: 5.0,
432        };
433
434        assert_eq!(portfolio_info.currency, "BTC");
435        assert_eq!(portfolio_info.balance, 1100.0);
436    }
437
438    #[test]
439    fn test_debug_and_display_implementations() {
440        let account = create_test_account_summary();
441        let debug_str = format!("{:?}", account);
442        let display_str = format!("{}", account);
443
444        assert!(debug_str.contains("BTC"));
445        assert!(display_str.contains("BTC"));
446    }
447}