1use pretty_simple_display::{DebugPretty, DisplaySimple};
7use serde::{Deserialize, Serialize};
8
9#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
11pub struct AccountSummary {
12 pub currency: String,
14 pub balance: f64,
16 pub equity: f64,
18 pub available_funds: f64,
20 pub margin_balance: f64,
22 pub unrealized_pnl: f64,
24 pub realized_pnl: f64,
26 pub total_pl: f64,
28 pub session_funding: f64,
30 pub session_rpl: f64,
32 pub session_upl: f64,
34 pub maintenance_margin: f64,
36 pub initial_margin: f64,
38 pub available_withdrawal_funds: Option<f64>,
40 pub cross_collateral_enabled: Option<bool>,
42 pub delta_total: Option<f64>,
44 pub futures_pl: Option<f64>,
46 pub futures_session_rpl: Option<f64>,
48 pub futures_session_upl: Option<f64>,
50 pub options_delta: Option<f64>,
52 pub options_gamma: Option<f64>,
54 pub options_pl: Option<f64>,
56 pub options_session_rpl: Option<f64>,
58 pub options_session_upl: Option<f64>,
60 pub options_theta: Option<f64>,
62 pub options_vega: Option<f64>,
64 pub portfolio_margining_enabled: Option<bool>,
66 pub projected_delta_total: Option<f64>,
68 pub projected_initial_margin: Option<f64>,
70 pub projected_maintenance_margin: Option<f64>,
72 pub system_name: Option<String>,
74 #[serde(rename = "type")]
76 pub account_type: String,
77 pub delta_total_map: std::collections::HashMap<String, f64>,
80 pub deposit_address: String,
82 pub fees: Vec<std::collections::HashMap<String, f64>>,
84 pub limits: std::collections::HashMap<String, f64>,
86}
87
88impl AccountSummary {
89 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 pub fn available_margin(&self) -> f64 {
100 self.equity - self.initial_margin
101 }
102
103 pub fn is_at_risk(&self, threshold: f64) -> bool {
105 self.margin_utilization() > threshold
106 }
107
108 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#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
120pub struct Subaccount {
121 pub email: String,
123 pub id: u64,
125 pub login_enabled: bool,
127 pub portfolio: Option<PortfolioInfo>,
129 pub receive_notifications: bool,
131 pub system_name: String,
133 pub tif: Option<String>,
135 #[serde(rename = "type")]
137 pub subaccount_type: String,
138 pub username: String,
140}
141
142#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
144pub struct PortfolioInfo {
145 pub available_funds: f64,
147 pub available_withdrawal_funds: f64,
149 pub balance: f64,
151 pub currency: String,
153 pub delta_total: f64,
155 pub equity: f64,
157 pub initial_margin: f64,
159 pub maintenance_margin: f64,
161 pub margin_balance: f64,
163 pub session_rpl: f64,
165 pub session_upl: f64,
167 pub total_pl: f64,
169}
170
171#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
173pub struct Portfolio {
174 pub currency: String,
176 pub accounts: Vec<AccountSummary>,
178 pub total_usd_value: Option<f64>,
180 pub cross_margin_enabled: bool,
182}
183
184impl Portfolio {
185 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 pub fn add_account(&mut self, account: AccountSummary) {
197 self.accounts.push(account);
198 }
199
200 pub fn get_account(&self, currency: &String) -> Option<&AccountSummary> {
202 self.accounts.iter().find(|acc| &acc.currency == currency)
203 }
204
205 pub fn total_equity(&self) -> f64 {
207 self.accounts.iter().map(|acc| acc.equity).sum()
208 }
209
210 pub fn total_unrealized_pnl(&self) -> f64 {
212 self.accounts.iter().map(|acc| acc.unrealized_pnl).sum()
213 }
214
215 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); }
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); }
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)); assert!(account.is_at_risk(10.0)); }
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); }
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}