Skip to main content

ibkr_flex_statement/
lib.rs

1pub 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
40/// Parser for interpreting the content of an InteractiveBrokers Flex-based XML statement.
41///
42/// # Examples
43///
44/// ```
45/// use anyhow::Result;
46/// use ibkr_flex_statement::{Parser, Statement};
47///
48/// let statement_xml: &str = "<FlexQueryResponse>...</FlexQueryResponse>";
49///
50/// let parser = Parser::new().unwrap();
51/// let statements: Vec<Statement> = parser.parse_flex_query_response(statement_xml).unwrap();
52/// ```
53pub 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        // Ensure we got two equity summaries.
196        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}