ibkr_flex_statement/
lib.rs1pub mod account_info;
2pub mod asset_category;
3pub mod cash_report;
4pub mod currency;
5pub mod equity_summary;
6pub mod fifo_performance_summary;
7pub mod net_stock_position;
8mod node_utils;
9pub mod open_position;
10pub mod statement_section;
11mod time_utils;
12pub mod trade;
13
14use account_info::AccountInfo;
15use anyhow::Result;
16use cash_report::CashReport;
17use chrono_tz::Tz;
18use equity_summary::EquitySummary;
19use fifo_performance_summary::FIFOPerformanceSummary;
20use net_stock_position::NetStockPosition;
21use node_utils::NodeWrapper;
22use open_position::OpenPosition;
23use roxmltree::{Document, Node};
24use statement_section::{StatementSection, StatementSectionWithTimezone};
25use std::collections::HashMap;
26use std::fmt::Debug;
27use trade::Trade;
28
29#[derive(Debug, PartialEq)]
30pub struct Statement {
31 pub account_info: AccountInfo,
32 pub cash_reports: Vec<CashReport>,
33 pub equity_summaries: Vec<EquitySummary>,
34 pub fifo_performance_summaries: Vec<FIFOPerformanceSummary>,
35 pub net_stock_positions: Vec<NetStockPosition>,
36 pub open_positions: Vec<OpenPosition>,
37 pub trades: Vec<Trade>,
38}
39
40pub struct Parser {
54 pub timezone_map: HashMap<String, Tz>,
55}
56
57impl Parser {
58 pub fn new() -> Result<Self> {
59 let new_york_tz: Tz = "America/New_York".parse().unwrap();
60 let timezone_map = HashMap::from([
61 ("EST".to_string(), new_york_tz),
62 ("EDT".to_string(), new_york_tz),
63 ]);
64
65 Ok(Parser { timezone_map })
66 }
67
68 fn parse_section<T: StatementSection>(
69 &self,
70 node: &Node,
71 section_name: &str,
72 ) -> Result<Vec<T>> {
73 node.descendants()
74 .filter(|n| n.tag_name().name() == section_name)
75 .map(|n| T::from_node(&NodeWrapper { node: n }).map_err(anyhow::Error::msg))
76 .collect::<Result<Vec<T>>>()
77 }
78
79 fn parse_section_with_timezone<T: StatementSectionWithTimezone>(
80 &self,
81 node: &Node,
82 section_name: &str,
83 ) -> Result<Vec<T>> {
84 node.descendants()
85 .filter(|n| n.tag_name().name() == section_name)
86 .map(|n| {
87 T::from_node(&NodeWrapper { node: n }, &self.timezone_map)
88 .map_err(anyhow::Error::msg)
89 })
90 .collect::<Result<Vec<T>>>()
91 }
92
93 fn parse_flex_statement(&self, node: &Node) -> Result<Statement> {
94 let account_infos = self.parse_section::<AccountInfo>(node, "AccountInformation")?;
95 if account_infos.len() > 1 {
96 return Err(anyhow::Error::msg(
97 "multiple account information sections found",
98 ));
99 } else if account_infos.is_empty() {
100 return Err(anyhow::Error::msg("no account information sections found"));
101 }
102 let account_info = account_infos[0].clone();
103
104 let cash_reports = self.parse_section(node, "CashReportCurrency")?;
105 let equity_summaries = self.parse_section(node, "EquitySummaryByReportDateInBase")?;
106 let fifo_performance_summaries =
107 self.parse_section(node, "FIFOPerformanceSummaryUnderlying")?;
108 let net_stock_positions = self.parse_section(node, "NetStockPosition")?;
109 let open_positions = self.parse_section(node, "OpenPosition")?;
110 let trades = self.parse_section_with_timezone(node, "Trade")?;
111
112 Ok(Statement {
113 account_info,
114 cash_reports,
115 equity_summaries,
116 fifo_performance_summaries,
117 net_stock_positions,
118 open_positions,
119 trades,
120 })
121 }
122
123 pub fn parse_flex_query_response(&self, flex_query_response: &str) -> Result<Vec<Statement>> {
124 let doc = Document::parse(flex_query_response)?;
125 doc.descendants()
126 .filter(|n| n.tag_name().name() == "FlexStatement")
127 .map(|n| self.parse_flex_statement(&n).map_err(anyhow::Error::msg))
128 .collect::<Result<Vec<Statement>>>()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use anyhow::Result;
136 use chrono_tz::Tz;
137
138 const FULL_STATEMENT_EXAMPLE: &str = r##"
139 <FlexQueryResponse queryName="example-query" type="AF">
140 <FlexStatements count="1">
141 <FlexStatement accountId="U1234567" fromDate="2025-04-25" toDate="2025-04-25" period="LastBusinessDay" whenGenerated="2025-04-26;13:34:28 EDT">
142 <AccountInformation accountId="U1234567" accountType="Individual" customerType="Individual" accountCapabilities="Portfolio Margin" tradingPermissions="Stocks,Options,Warrants,Forex,Futures,Crypto Currencies,Mutual Funds,Fully Paid Stock Loan" />
143 <EquitySummaryInBase>
144 <EquitySummaryByReportDateInBase accountId="U1234567" currency="USD" cash="-1755658.753685009" cashLong="0.000832235" cashShort="-1755658.754517244" commodities="0" commoditiesLong="0" commoditiesShort="0" dividendAccruals="0" dividendAccrualsLong="0" dividendAccrualsShort="0" interestAccruals="1292.18" interestAccrualsLong="1591.34" interestAccrualsShort="-299.16" stock="3441241" stockLong="3441241" stockShort="0" funds="0" fundsLong="0" fundsShort="0" brokerInterestAccrualsComponent="186.32" brokerInterestAccrualsComponentLong="485.48" brokerInterestAccrualsComponentShort="-299.16" brokerFeesAccrualsComponent="0" brokerFeesAccrualsComponentLong="0" brokerFeesAccrualsComponentShort="0" total="1686874.426314991" totalLong="3442832.340832235" totalShort="-1755957.914517244" reportDate="2025-04-24" />
145 <EquitySummaryByReportDateInBase accountId="U1234567" currency="USD" cash="-1856140.99825062" cashLong="0.000832132" cashShort="-1856140.999082752" commodities="0" commoditiesLong="0" commoditiesShort="0" dividendAccruals="0" dividendAccrualsLong="0" dividendAccrualsShort="0" interestAccruals="1051.42" interestAccrualsLong="1591.34" interestAccrualsShort="-539.92" stock="3664457" stockLong="3664457" stockShort="0" funds="0" fundsLong="0" fundsShort="0" brokerInterestAccrualsComponent="-54.44" brokerInterestAccrualsComponentLong="485.48" brokerInterestAccrualsComponentShort="-539.92" brokerFeesAccrualsComponent="0" brokerFeesAccrualsComponentLong="0" brokerFeesAccrualsComponentShort="0" total="1809367.421749379" totalLong="3666048.340832131" totalShort="-1856680.919082752" reportDate="2025-04-25" />
146 </EquitySummaryInBase>
147 <FIFOPerformanceSummaryInBase>
148 <FIFOPerformanceSummaryUnderlying accountId="U1234567" assetCategory="STK" symbol="TTWO" conid="6478131" listingExchange="NASDAQ" reportDate="2025-04-25" realizedSTProfit="0" realizedSTLoss="0" realizedLTProfit="0" realizedLTLoss="0" totalRealizedPnl="0" unrealizedProfit="4089.983554" unrealizedLoss="0" unrealizedSTProfit="4089.983554" unrealizedSTLoss="0" unrealizedLTProfit="0" unrealizedLTLoss="0" totalFifoPnl="4089.983554" />
149 <FIFOPerformanceSummaryUnderlying accountId="U1234567" assetCategory="" symbol="" conid="" listingExchange="" reportDate="2025-04-25" realizedSTProfit="0" realizedSTLoss="-205.04987357" realizedLTProfit="0" realizedLTLoss="0" totalRealizedPnl="-205.04987357" unrealizedProfit="131057.571473" unrealizedLoss="-44834.337024864" unrealizedSTProfit="131057.571473" unrealizedSTLoss="-44834.337024864" unrealizedLTProfit="0" unrealizedLTLoss="0" totalFifoPnl="86018.184574566" />
150 </FIFOPerformanceSummaryInBase>
151 <CashReport>
152 <CashReportCurrency accountId="U1234567" currency="BASE_SUMMARY" fromDate="2025-04-25" toDate="2025-04-25" netTradesSales="0" netTradesPurchases="0" startingCash="-1755658.753685008" startingCashSec="-1755658.753685008" startingCashCom="0" commissions="-56.26956551" commissionsSec="-56.26956551" commissionsCom="0" commissionsMTD="-11167.4772929" commissionsYTD="-25339.56064716" depositWithdrawals="0" depositWithdrawalsSec="0" depositWithdrawalsCom="0" depositWithdrawalsMTD="0" depositWithdrawalsYTD="1650000" debitCardActivity="0" debitCardActivitySec="0" debitCardActivityCom="0" debitCardActivityMTD="0" debitCardActivityYTD="0" dividends="0" dividendsSec="0" dividendsCom="0" dividendsMTD="0" dividendsYTD="110.7" otherFees="-19.77" otherFeesSec="-19.77" otherFeesCom="0" otherFeesMTD="-121.27" otherFeesYTD="-486.9" otherIncome="0" otherIncomeSec="0" otherIncomeCom="0" otherIncomeMTD="0" otherIncomeYTD="0" endingCash="-1856140.99825062" endingCashSec="-1856140.99825062" endingCashCom="0" endingSettledCash="-1755734.79325062" endingSettledCashSec="-1755734.79325062" endingSettledCashCom="0" brokerInterest="0" brokerInterestSec="0" brokerInterestCom="0" brokerInterestMTD="-545.49" brokerInterestYTD="-1341.59" brokerFees="0" brokerFeesSec="0" brokerFeesCom="0" brokerFeesMTD="0" brokerFeesYTD="0" deposits="0" depositsSec="0" depositsCom="0" depositsMTD="0" depositsYTD="1650000" withdrawals="0" withdrawalsSec="0" withdrawalsCom="0" withdrawalsMTD="0" withdrawalsYTD="0" />
153 <CashReportCurrency accountId="U1234567" currency="CAD" fromDate="2025-04-25" toDate="2025-04-25" netTradesSales="0" netTradesPurchases="0" startingCash="0.001153" startingCashSec="0.001153" startingCashCom="0" commissions="0" commissionsSec="0" commissionsCom="0" commissionsMTD="0" commissionsYTD="0" depositWithdrawals="0" depositWithdrawalsSec="0" depositWithdrawalsCom="0" depositWithdrawalsMTD="0" depositWithdrawalsYTD="0" debitCardActivity="0" debitCardActivitySec="0" debitCardActivityCom="0" debitCardActivityMTD="0" debitCardActivityYTD="0" dividends="0" dividendsSec="0" dividendsCom="0" dividendsMTD="0" dividendsYTD="0" otherFees="0" otherFeesSec="0" otherFeesCom="0" otherFeesMTD="0" otherFeesYTD="0" otherIncome="0" otherIncomeSec="0" otherIncomeCom="0" otherIncomeMTD="0" otherIncomeYTD="0" endingCash="0.001153" endingCashSec="0.001153" endingCashCom="0" endingSettledCash="0.001153" endingSettledCashSec="0.001153" endingSettledCashCom="0" brokerInterest="0" brokerInterestSec="0" brokerInterestCom="0" brokerInterestMTD="0" brokerInterestYTD="0" brokerFees="0" brokerFeesSec="0" brokerFeesCom="0" brokerFeesMTD="0" brokerFeesYTD="0" deposits="0" depositsSec="0" depositsCom="0" depositsMTD="0" depositsYTD="0" withdrawals="0" withdrawalsSec="0" withdrawalsCom="0" withdrawalsMTD="0" withdrawalsYTD="0" />
154 <CashReportCurrency accountId="U1234567" currency="USD" fromDate="2025-04-25" toDate="2025-04-25" netTradesSales="0" netTradesPurchases="0" startingCash="-1755658.754517244" startingCashSec="-1755658.754517244" startingCashCom="0" commissions="-56.26956551" commissionsSec="-56.26956551" commissionsCom="0" commissionsMTD="-11167.4772929" commissionsYTD="-25339.56064716" depositWithdrawals="0" depositWithdrawalsSec="0" depositWithdrawalsCom="0" depositWithdrawalsMTD="0" depositWithdrawalsYTD="1650000" debitCardActivity="0" debitCardActivitySec="0" debitCardActivityCom="0" debitCardActivityMTD="0" debitCardActivityYTD="0" dividends="0" dividendsSec="0" dividendsCom="0" dividendsMTD="0" dividendsYTD="110.7" otherFees="-19.77" otherFeesSec="-19.77" otherFeesCom="0" otherFeesMTD="-121.27" otherFeesYTD="-486.9" otherIncome="0" otherIncomeSec="0" otherIncomeCom="0" otherIncomeMTD="0" otherIncomeYTD="0" endingCash="-1856140.999082752" endingCashSec="-1856140.999082752" endingCashCom="0" endingSettledCash="-1755734.794082752" endingSettledCashSec="-1755734.794082752" endingSettledCashCom="0" brokerInterest="0" brokerInterestSec="0" brokerInterestCom="0" brokerInterestMTD="-545.49" brokerInterestYTD="-1341.59" brokerFees="0" brokerFeesSec="0" brokerFeesCom="0" brokerFeesMTD="0" brokerFeesYTD="0" deposits="0" depositsSec="0" depositsCom="0" depositsMTD="0" depositsYTD="1650000" withdrawals="0" withdrawalsSec="0" withdrawalsCom="0" withdrawalsMTD="0" withdrawalsYTD="0" />
155 </CashReport>
156 <OpenPositions>
157 <OpenPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="GRPN" conid="426480582" listingExchange="NASDAQ" reportDate="2025-04-25" position="3000" markPrice="19.89" positionValue="59670" openPrice="20.153441225" costBasisPrice="20.153441225" percentOfNAV="1.63" fifoPnlUnrealized="-790.323674" side="Long" openDateTime="" holdingPeriodDateTime="" accruedInt="" commodityType="" />
158 <OpenPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="META" conid="107113386" listingExchange="NASDAQ" reportDate="2025-04-25" position="800" markPrice="547.27" positionValue="437816" openPrice="542.020354354" costBasisPrice="542.020354354" percentOfNAV="11.95" fifoPnlUnrealized="4199.716517" side="Long" openDateTime="" holdingPeriodDateTime="" accruedInt="" commodityType="" />
159 <OpenPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="NFLX" conid="15124833" listingExchange="NASDAQ" reportDate="2025-04-25" position="400" markPrice="1101.53" positionValue="440612" openPrice="1056.32548211" costBasisPrice="1056.32548211" percentOfNAV="12.02" fifoPnlUnrealized="18081.807156" side="Long" openDateTime="" holdingPeriodDateTime="" accruedInt="" commodityType="" />
160 <OpenPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="PLTR" conid="444857009" listingExchange="NASDAQ" reportDate="2025-04-25" position="3100" markPrice="112.78" positionValue="349618" openPrice="104.761973398" costBasisPrice="104.761973398" percentOfNAV="9.54" fifoPnlUnrealized="24855.882465" side="Long" openDateTime="" holdingPeriodDateTime="" accruedInt="" commodityType="" />
161 <OpenPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="TQQQ" conid="72539702" listingExchange="NASDAQ" reportDate="2025-04-25" position="34100" markPrice="53.86" positionValue="1836626" openPrice="53.776784497" costBasisPrice="53.776784497" percentOfNAV="50.12" fifoPnlUnrealized="2837.648645" side="Long" openDateTime="" holdingPeriodDateTime="" accruedInt="" commodityType="" />
162 <OpenPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="TSLA" conid="76792991" listingExchange="NASDAQ" reportDate="2025-04-25" position="1500" markPrice="284.95" positionValue="427425" openPrice="262.984320092" costBasisPrice="262.984320092" percentOfNAV="11.66" fifoPnlUnrealized="32948.519862" side="Long" openDateTime="" holdingPeriodDateTime="" accruedInt="" commodityType="" />
163 <OpenPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="TTWO" conid="6478131" listingExchange="NASDAQ" reportDate="2025-04-25" position="500" markPrice="225.38" positionValue="112690" openPrice="217.200032892" costBasisPrice="217.200032892" percentOfNAV="3.08" fifoPnlUnrealized="4089.983554" side="Long" openDateTime="" holdingPeriodDateTime="" accruedInt="" commodityType="" />
164 </OpenPositions>
165 <NetStockPositionSummary>
166 <NetStockPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="GRPN" conid="426480582" listingExchange="NASDAQ" netShares="3000" />
167 <NetStockPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="META" conid="107113386" listingExchange="NASDAQ" netShares="800" />
168 <NetStockPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="NFLX" conid="15124833" listingExchange="NASDAQ" netShares="400" />
169 <NetStockPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="PLTR" conid="444857009" listingExchange="NASDAQ" netShares="3100" />
170 <NetStockPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="TQQQ" conid="72539702" listingExchange="NASDAQ" netShares="34100" />
171 <NetStockPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="TSLA" conid="76792991" listingExchange="NASDAQ" netShares="1500" />
172 <NetStockPosition accountId="U1234567" currency="USD" assetCategory="STK" symbol="TTWO" conid="6478131" listingExchange="NASDAQ" netShares="500" />
173 </NetStockPositionSummary>
174 <Trades>
175 <Trade accountId="U1234567" currency="USD" symbol="ARGX" conid="276343981" listingExchange="NASDAQ" tradeID="7587063231" reportDate="2025-04-25" dateTime="2025-04-25;10:19:55 EDT" tradeDate="2025-04-25" transactionType="ExchTrade" exchange="BYX" quantity="1" tradePrice="606.57" tradeMoney="606.57" proceeds="-606.57" ibCommission="-1.000035" ibCommissionCurrency="USD" netCash="-607.570035" closePrice="614.76" openCloseIndicator="O" cost="607.570035" fifoPnlRealized="0" mtmPnl="8.19" origTradePrice="0" origTradeDate="" origTradeID="" origOrderID="0" origTransactionID="0" buySell="BUY" ibOrderID="4015030800" transactionID="32580112485" ibExecID="0000edae.680b59d1.01.01" orderTime="2025-04-25;10:19:55 EDT" openDateTime="" holdingPeriodDateTime="" whenRealized="" whenReopened="" orderType="LMT" accruedInt="0" assetCategory="STK" brokerageOrderID="002ce642.00014b44.680b0ed6.0001" orderReference="" isAPIOrder="N" initialInvestment="" />
176 <Trade accountId="U1234567" currency="USD" symbol="GEO" conid="158655765" listingExchange="NYSE" tradeID="7587946875" reportDate="2025-04-25" dateTime="2025-04-25;11:24:28 EDT" tradeDate="2025-04-25" transactionType="ExchTrade" exchange="NYSE" quantity="1000" tradePrice="30.85" tradeMoney="30850" proceeds="-30850" ibCommission="-5.035" ibCommissionCurrency="USD" netCash="-30855.035" closePrice="30.58" openCloseIndicator="O" cost="30855.035" fifoPnlRealized="0" mtmPnl="-270" origTradePrice="0" origTradeDate="" origTradeID="" origOrderID="0" origTransactionID="0" buySell="BUY" ibOrderID="4015577648" transactionID="32582764875" ibExecID="00012e0e.680b7717.01.01" orderTime="2025-04-25;11:24:26 EDT" openDateTime="" holdingPeriodDateTime="" whenRealized="" whenReopened="" orderType="LMT" accruedInt="0" assetCategory="STK" brokerageOrderID="002ce642.00014b44.680b0fbf.0001" orderReference="" isAPIOrder="N" initialInvestment="" />
177 </Trades>
178 </FlexStatement>
179 </FlexStatements>
180 </FlexQueryResponse>
181 "##;
182
183 #[test]
184 fn long_timezone_name_parses() -> Result<()> {
185 let _tz: Tz = "America/New_York".parse().unwrap();
186 Ok(())
187 }
188
189 #[test]
190 fn parsing_succeeds() -> Result<()> {
191 let statements = Parser::new()?.parse_flex_query_response(FULL_STATEMENT_EXAMPLE)?;
192 assert_eq!(statements.len(), 1);
193 let result = &statements[0];
194
195 assert_eq!(result.cash_reports.len(), 3);
197 assert_eq!(result.equity_summaries.len(), 2);
198 assert_eq!(result.fifo_performance_summaries.len(), 2);
199 assert_eq!(result.net_stock_positions.len(), 7);
200 assert_eq!(result.open_positions.len(), 7);
201 assert_eq!(result.trades.len(), 2);
202
203 Ok(())
204 }
205}