rustrade_execution/client/ibkr/
account.rs1use crate::balance::{AssetBalance, Balance};
2use chrono::Utc;
3use fnv::FnvHashMap;
4use rust_decimal::Decimal;
5use rustrade_instrument::asset::name::AssetNameExchange;
6use smol_str::SmolStr;
7use std::str::FromStr;
8use tracing::warn;
9
10#[derive(Debug, Default)]
12pub struct BalanceAggregator {
13 balances: FnvHashMap<SmolStr, CurrencyBalance>,
15}
16
17#[derive(Debug, Default, Clone)]
18struct CurrencyBalance {
19 total_cash: Option<Decimal>,
20 available_funds: Option<Decimal>,
21}
22
23impl BalanceAggregator {
24 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn process(&mut self, summary: &ibapi::accounts::AccountSummary) {
30 let currency = SmolStr::new(&summary.currency);
31 let entry = self.balances.entry(currency).or_default();
32
33 match summary.tag.as_str() {
34 "TotalCashValue" => {
35 match Decimal::from_str(&summary.value) {
37 Ok(val) => entry.total_cash = Some(val),
38 Err(e) => {
39 warn!(tag = %summary.tag, value = %summary.value, error = %e, "Failed to parse balance")
40 }
41 }
42 }
43 "AvailableFunds" => match Decimal::from_str(&summary.value) {
44 Ok(val) => entry.available_funds = Some(val),
45 Err(e) => {
46 warn!(tag = %summary.tag, value = %summary.value, error = %e, "Failed to parse balance")
47 }
48 },
49 _ => {}
50 }
51 }
52
53 pub fn to_balances(&self) -> Vec<AssetBalance<AssetNameExchange>> {
55 let now = Utc::now();
56 self.balances
57 .iter()
58 .filter_map(|(currency, bal)| {
59 let total = bal.total_cash?;
60 let free = bal.available_funds.unwrap_or(total);
61
62 Some(AssetBalance {
63 asset: AssetNameExchange::from(currency.as_str()),
64 balance: Balance::new(total, free),
65 time_exchange: now,
66 })
67 })
68 .collect()
69 }
70
71 pub fn clear(&mut self) {
73 self.balances.clear();
74 }
75}
76
77#[cfg(test)]
78#[allow(clippy::unwrap_used)] mod tests {
80 use super::*;
81 use ibapi::accounts::AccountSummary;
82 use std::str::FromStr;
83
84 fn mock_account_summary(tag: &str, value: &str, currency: &str) -> AccountSummary {
85 AccountSummary {
86 account: "DU123456".to_string(),
87 tag: tag.to_string(),
88 value: value.to_string(),
89 currency: currency.to_string(),
90 }
91 }
92
93 #[test]
94 fn test_balance_aggregator() {
95 let mut agg = BalanceAggregator::new();
96
97 agg.process(&mock_account_summary("TotalCashValue", "10000.50", "USD"));
98 agg.process(&mock_account_summary("AvailableFunds", "8000.25", "USD"));
99 agg.process(&mock_account_summary("TotalCashValue", "5000.00", "EUR"));
100
101 let balances = agg.to_balances();
102 assert_eq!(balances.len(), 2);
103
104 let usd = balances.iter().find(|b| b.asset.as_ref() == "USD").unwrap();
105 assert_eq!(usd.balance.total, Decimal::from_str("10000.50").unwrap());
106 assert_eq!(usd.balance.free, Decimal::from_str("8000.25").unwrap());
107
108 let eur = balances.iter().find(|b| b.asset.as_ref() == "EUR").unwrap();
109 assert_eq!(eur.balance.total, Decimal::from_str("5000.00").unwrap());
110 assert_eq!(eur.balance.free, Decimal::from_str("5000.00").unwrap());
111 }
112
113 #[test]
114 fn test_balance_aggregator_clear() {
115 let mut agg = BalanceAggregator::new();
116 agg.process(&mock_account_summary("TotalCashValue", "1000", "USD"));
117 assert_eq!(agg.to_balances().len(), 1);
118
119 agg.clear();
120 assert!(agg.to_balances().is_empty());
121 }
122}