ibkr-flex-statement 0.3.6

Parser for InteractiveBrokers (IBKR) Flex-based query response XML broker statements.
Documentation
use crate::currency::Currency;
use crate::node_utils::NodeWrapper;
use crate::statement_section::StatementSection;
use crate::time_utils;
use anyhow::Result;

#[derive(Debug, PartialEq)]
pub struct CashReport {
    pub account_id: String,
    pub currency: Currency,
    pub start_timestamp_ms: i64,
    pub end_timestamp_ms: i64,

    pub starting_cash: f64,
    pub ending_cash: f64,
    pub ending_settled_cash: f64,

    pub net_trade_purchases: f64,
    pub net_trade_sales: f64,

    pub commissions: f64,
    pub commissions_mtd: Option<f64>,
    pub commissions_ytd: Option<f64>,

    pub other_fees: f64,
    pub other_fees_mtd: Option<f64>,
    pub other_fees_ytd: Option<f64>,

    pub dividends: f64,
    pub dividends_mtd: Option<f64>,
    pub dividends_ytd: Option<f64>,

    pub interest: f64,
    pub interest_mtd: Option<f64>,
    pub interest_ytd: Option<f64>,

    pub deposits: f64,
    pub deposits_mtd: Option<f64>,
    pub deposits_ytd: Option<f64>,

    pub withdrawals: f64,
    pub withdrawals_mtd: Option<f64>,
    pub withdrawals_ytd: Option<f64>,
}

impl StatementSection for CashReport {
    fn from_node(node: &NodeWrapper) -> Result<CashReport> {
        let start_date_eod_ms_plus_one = time_utils::trading_eod_after_hours_timestamp_ms(
            node.node.attribute("fromDate").unwrap(),
        )?;
        let start_timestamp_ms = start_date_eod_ms_plus_one - (60 * 60 * 24 * 1000) + 1;

        Ok(CashReport {
            account_id: node.get_attribute("accountId")?,
            currency: Currency::try_from(node.node.attribute("currency").unwrap())?,

            starting_cash: node.parse_attribute("startingCash")?,
            ending_cash: node.parse_attribute("endingCash")?,
            ending_settled_cash: node.parse_attribute("endingSettledCash")?,

            net_trade_purchases: node.parse_attribute("netTradesPurchases")?,
            net_trade_sales: node.parse_attribute("netTradesSales")?,

            commissions: node.parse_attribute("commissions")?,
            commissions_mtd: node.parse_attribute_opt("commissionsMTD")?,
            commissions_ytd: node.parse_attribute_opt("commissionsYTD")?,

            other_fees: node.parse_attribute("otherFees")?,
            other_fees_mtd: node.parse_attribute_opt("otherFeesMTD")?,
            other_fees_ytd: node.parse_attribute_opt("otherFeesYTD")?,

            dividends: node.parse_attribute("dividends")?,
            dividends_mtd: node.parse_attribute_opt("dividendsMTD")?,
            dividends_ytd: node.parse_attribute_opt("dividendsYTD")?,

            interest: node.parse_attribute("brokerInterest")?,
            interest_mtd: node.parse_attribute_opt("brokerInterestMTD")?,
            interest_ytd: node.parse_attribute_opt("brokerInterestYTD")?,

            deposits: node.parse_attribute("deposits")?,
            deposits_mtd: node.parse_attribute_opt("depositsMTD")?,
            deposits_ytd: node.parse_attribute_opt("depositsYTD")?,

            withdrawals: node.parse_attribute("withdrawals")?,
            withdrawals_mtd: node.parse_attribute_opt("withdrawalsMTD")?,
            withdrawals_ytd: node.parse_attribute_opt("withdrawalsYTD")?,

            start_timestamp_ms,
            end_timestamp_ms: time_utils::trading_eod_after_hours_timestamp_ms(
                node.node.attribute("toDate").unwrap(),
            )?,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Parser;
    use crate::currency::Currency;
    use anyhow::Result;

    const PARTIAL_STATEMENT_EXAMPLE: &str = r##"
        <FlexQueryResponse queryName="example-query" type="AF">
            <FlexStatements count="1">
                <FlexStatement accountId="U1234567" fromDate="2025-04-25" toDate="2025-04-25" period="LastBusinessDay" whenGenerated="2025-04-26;13:34:28 EDT">
                    <AccountInformation accountId="U1234567" accountType="Individual" customerType="Individual" accountCapabilities="Portfolio Margin" tradingPermissions="Stocks,Options,Warrants,Forex,Futures,Crypto Currencies,Mutual Funds,Fully Paid Stock Loan" />
                    <CashReport>
                        <CashReportCurrency
                            accountId="U1234567"
                            currency="USD"
                            fromDate="2025-04-25"
                            toDate="2025-04-25"
                            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"
                            netTradesSales="0"
                            netTradesSalesSec="0"
                            netTradesSalesCom="0"
                            netTradesPurchases="0"
                            netTradesPurchasesSec="0"
                            netTradesPurchasesCom="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" />
                    </CashReport>

                </FlexStatement>
            </FlexStatements>
         </FlexQueryResponse>
        "##;

    const PARTIAL_STATEMENT_EXAMPLE_NO_MTD_YTD: &str = r##"
        <FlexQueryResponse queryName="some-query" type="AF">
        <FlexStatements count="1">
        <FlexStatement accountId="U1234567" fromDate="2024-01-01" toDate="2024-01-01" period="" whenGenerated="2025-05-04;01:26:45 EDT">
        <AccountInformation accountId="U1234567" accountType="Individual" customerType="Individual" accountCapabilities="Portfolio Margin" tradingPermissions="Stocks,Options,Warrants,Forex,Futures,Crypto Currencies,Mutual Funds,Fully Paid Stock Loan" />
        <CashReport>
            <CashReportCurrency accountId="U1234567" currency="BASE_SUMMARY" fromDate="2024-01-01" toDate="2024-01-01" startingCash="1308.407281684" startingCashSec="1308.407281684" startingCashCom="0" commissions="0" commissionsSec="0" commissionsCom="0" depositWithdrawals="0" depositWithdrawalsSec="0" depositWithdrawalsCom="0" debitCardActivity="0" debitCardActivitySec="0" debitCardActivityCom="0" dividends="0" dividendsSec="0" dividendsCom="0" otherFees="0" otherFeesSec="0" otherFeesCom="0" otherIncome="0" otherIncomeSec="0" otherIncomeCom="0" endingCash="1308.407282342" endingCashSec="1308.407282342" endingCashCom="0" endingSettledCash="1308.407282342" endingSettledCashSec="1308.407282342" endingSettledCashCom="0" brokerInterest="0" brokerInterestSec="0" brokerInterestCom="0" brokerFees="0" brokerFeesSec="0" brokerFeesCom="0" deposits="0" depositsSec="0" depositsCom="0" withdrawals="0" withdrawalsSec="0" withdrawalsCom="0" netTradesSales="0" netTradesSalesSec="0" netTradesSalesCom="0" netTradesPurchases="0" netTradesPurchasesSec="0" netTradesPurchasesCom="0" transactionTax="0" transactionTaxSec="0" transactionTaxCom="0" withholdingTax="0" withholdingTaxSec="0" withholdingTaxCom="0" withholdingCollectedTax="0" withholdingCollectedTaxSec="0" withholdingCollectedTaxCom="0" slbNetSecuritiesLentActivity="0" slbNetSecuritiesLentActivitySec="0" slbNetSecuritiesLentActivityCom="0" />
            <CashReportCurrency accountId="U1234567" currency="CAD" fromDate="2024-01-01" toDate="2024-01-01" startingCash="0.001153" startingCashSec="0.001153" startingCashCom="0" commissions="0" commissionsSec="0" commissionsCom="0" depositWithdrawals="0" depositWithdrawalsSec="0" depositWithdrawalsCom="0" debitCardActivity="0" debitCardActivitySec="0" debitCardActivityCom="0" dividends="0" dividendsSec="0" dividendsCom="0" otherFees="0" otherFeesSec="0" otherFeesCom="0" otherIncome="0" otherIncomeSec="0" otherIncomeCom="0" endingCash="0.001153" endingCashSec="0.001153" endingCashCom="0" endingSettledCash="0.001153" endingSettledCashSec="0.001153" endingSettledCashCom="0" brokerInterest="0" brokerInterestSec="0" brokerInterestCom="0" brokerFees="0" brokerFeesSec="0" brokerFeesCom="0" deposits="0" depositsSec="0" depositsCom="0" withdrawals="0" withdrawalsSec="0" withdrawalsCom="0" netTradesSales="0" netTradesSalesSec="0" netTradesSalesCom="0" netTradesPurchases="0" netTradesPurchasesSec="0" netTradesPurchasesCom="0" transactionTax="0" transactionTaxSec="0" transactionTaxCom="0" withholdingTax="0" withholdingTaxSec="0" withholdingTaxCom="0" withholdingCollectedTax="0" withholdingCollectedTaxSec="0" withholdingCollectedTaxCom="0" slbNetSecuritiesLentActivity="0" slbNetSecuritiesLentActivitySec="0" slbNetSecuritiesLentActivityCom="0" />
            <CashReportCurrency accountId="U1234567" currency="USD" fromDate="2024-01-01" toDate="2024-01-01" startingCash="1308.406411423" startingCashSec="1308.406411423" startingCashCom="0" commissions="0" commissionsSec="0" commissionsCom="0" depositWithdrawals="0" depositWithdrawalsSec="0" depositWithdrawalsCom="0" debitCardActivity="0" debitCardActivitySec="0" debitCardActivityCom="0" dividends="0" dividendsSec="0" dividendsCom="0" otherFees="0" otherFeesSec="0" otherFeesCom="0" otherIncome="0" otherIncomeSec="0" otherIncomeCom="0" endingCash="1308.406411423" endingCashSec="1308.406411423" endingCashCom="0" endingSettledCash="1308.406411423" endingSettledCashSec="1308.406411423" endingSettledCashCom="0" brokerInterest="0" brokerInterestSec="0" brokerInterestCom="0" brokerFees="0" brokerFeesSec="0" brokerFeesCom="0" deposits="0" depositsSec="0" depositsCom="0" withdrawals="0" withdrawalsSec="0" withdrawalsCom="0" netTradesSales="0" netTradesSalesSec="0" netTradesSalesCom="0" netTradesPurchases="0" netTradesPurchasesSec="0" netTradesPurchasesCom="0" transactionTax="0" transactionTaxSec="0" transactionTaxCom="0" withholdingTax="0" withholdingTaxSec="0" withholdingTaxCom="0" withholdingCollectedTax="0" withholdingCollectedTaxSec="0" withholdingCollectedTaxCom="0" slbNetSecuritiesLentActivity="0" slbNetSecuritiesLentActivitySec="0" slbNetSecuritiesLentActivityCom="0" />
        </CashReport>
        </FlexStatement>
        </FlexStatements>
        </FlexQueryResponse>
        "##;

    #[test]
    fn cash_reports_parse() -> Result<()> {
        let statements = Parser::new()?.parse_flex_query_response(PARTIAL_STATEMENT_EXAMPLE)?;
        assert_eq!(statements.len(), 1);
        let result = &statements[0];

        // Ensure we got two equity summaries.
        assert_eq!(result.cash_reports.len(), 1);

        assert_eq!(
            result.cash_reports[0],
            CashReport {
                account_id: "U1234567".to_string(),
                currency: Currency::USD,

                starting_cash: -1755658.754517244,
                ending_cash: -1856140.999082752,
                ending_settled_cash: -1755734.794082752,

                commissions: -56.26956551,
                commissions_mtd: Some(-11167.4772929),
                commissions_ytd: Some(-25339.56064716),

                dividends: 0.0,
                dividends_mtd: Some(0.0),
                dividends_ytd: Some(110.7),

                other_fees: -19.77,
                other_fees_mtd: Some(-121.27),
                other_fees_ytd: Some(-486.9),

                net_trade_purchases: 0.0,
                net_trade_sales: 0.0,

                interest: 0.0,
                interest_mtd: Some(-545.49),
                interest_ytd: Some(-1341.59),

                deposits: 0.0,
                deposits_mtd: Some(0.0),
                deposits_ytd: Some(1650000.0),

                withdrawals: 0.0,
                withdrawals_mtd: Some(0.0),
                withdrawals_ytd: Some(0.0),

                start_timestamp_ms: result.cash_reports[0].start_timestamp_ms,
                end_timestamp_ms: result.cash_reports[0].end_timestamp_ms,
            }
        );

        Ok(())
    }

    #[test]
    fn cash_reports_parse_no_mtd_ytd() -> Result<()> {
        let statements =
            Parser::new()?.parse_flex_query_response(PARTIAL_STATEMENT_EXAMPLE_NO_MTD_YTD)?;
        assert_eq!(statements.len(), 1);
        let result = &statements[0];

        // Ensure we got two equity summaries.
        assert_eq!(result.cash_reports.len(), 3);

        assert_eq!(
            result.cash_reports[2],
            CashReport {
                account_id: "U1234567".to_string(),
                currency: Currency::USD,

                starting_cash: 1308.406411423,
                ending_cash: 1308.406411423,
                ending_settled_cash: 1308.406411423,

                commissions: 0.0,
                commissions_mtd: None,
                commissions_ytd: None,

                dividends: 0.0,
                dividends_mtd: None,
                dividends_ytd: None,

                other_fees: 0.0,
                other_fees_mtd: None,
                other_fees_ytd: None,

                net_trade_purchases: 0.0,
                net_trade_sales: 0.0,

                interest: 0.0,
                interest_mtd: None,
                interest_ytd: None,

                deposits: 0.0,
                deposits_mtd: None,
                deposits_ytd: None,

                withdrawals: 0.0,
                withdrawals_mtd: None,
                withdrawals_ytd: None,

                start_timestamp_ms: result.cash_reports[2].start_timestamp_ms,
                end_timestamp_ms: result.cash_reports[2].end_timestamp_ms,
            }
        );

        Ok(())
    }
}