Skip to main content

ibkr_flex_statement/
open_position.rs

1use super::currency::Currency;
2use super::time_utils;
3use crate::asset_category::AssetCategory;
4use crate::node_utils::NodeWrapper;
5use crate::statement_section::StatementSection;
6use anyhow::Result;
7
8#[derive(Debug, PartialEq)]
9pub enum PositionSide {
10    Long,
11    Short,
12}
13
14#[derive(Debug, PartialEq)]
15pub struct OpenPosition {
16    pub account_id: String,
17    pub asset_category: AssetCategory,
18    pub conid: u32,
19    pub cost_basis_price: f64,
20    pub fifo_pnl_unrealized: f64,
21    pub currency: Currency,
22    pub listing_exchange: String,
23    pub mark_price: f64,
24    pub open_quantity: f64,
25    pub position_value: f64,
26    pub timestamp_eod_ms: i64,
27    pub ticker: String,
28    pub side: PositionSide,
29}
30
31impl<'a> TryFrom<&'a str> for PositionSide {
32    type Error = anyhow::Error;
33
34    fn try_from(s: &'a str) -> Result<Self> {
35        match s {
36            "Long" => Ok(Self::Long),
37            "Short" => Ok(Self::Short),
38            _ => Err(anyhow::Error::msg(format!("unknown position side {}", s))),
39        }
40    }
41}
42
43impl StatementSection for OpenPosition {
44    fn from_node(node: &NodeWrapper) -> Result<OpenPosition> {
45        Ok(OpenPosition {
46            account_id: node.get_attribute("accountId")?,
47            asset_category: AssetCategory::try_from(node.node.attribute("assetCategory").unwrap())?,
48            conid: node.parse_attribute("conid")?,
49            cost_basis_price: node.parse_attribute("costBasisPrice")?,
50            currency: Currency::try_from(node.node.attribute("currency").unwrap())?,
51            fifo_pnl_unrealized: node.parse_attribute("fifoPnlUnrealized")?,
52            listing_exchange: node.get_attribute("listingExchange")?,
53            mark_price: node.parse_attribute("markPrice")?,
54            open_quantity: node.parse_attribute("position")?,
55            position_value: node.parse_attribute("positionValue")?,
56            side: PositionSide::try_from(node.node.attribute("side").unwrap())?,
57            ticker: node.get_attribute("symbol")?,
58            timestamp_eod_ms: time_utils::trading_eod_after_hours_timestamp_ms(
59                node.node.attribute("reportDate").unwrap(),
60            )?,
61        })
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::Parser;
69    use crate::currency::Currency;
70    use anyhow::Result;
71
72    const PARTIAL_STATEMENT_EXAMPLE: &str = r##"
73        <FlexQueryResponse queryName="example-query" type="AF">
74            <FlexStatements count="1">
75                <FlexStatement accountId="U1234567" fromDate="2025-04-25" toDate="2025-04-25" period="LastBusinessDay" whenGenerated="2025-04-26;13:34:28 EDT">
76                    <AccountInformation accountId="U1234567" accountType="Individual" customerType="Individual" accountCapabilities="Portfolio Margin" tradingPermissions="Stocks,Options,Warrants,Forex,Futures,Crypto Currencies,Mutual Funds,Fully Paid Stock Loan" />
77                    <OpenPositions>
78                        <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="" />
79                        <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="" />
80                        <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="" />
81                        <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="" />
82                        <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="" />
83                        <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="" />
84                        <OpenPosition 
85                            accountId="U1234567"
86                            currency="USD"
87                            assetCategory="STK"
88                            symbol="TTWO"
89                            conid="6478131"
90                            listingExchange="NASDAQ"
91                            reportDate="2025-04-25"
92                            position="500"
93                            markPrice="225.38"
94                            positionValue="112690"
95                            openPrice="217.200032892"
96                            costBasisPrice="217.200032892"
97                            percentOfNAV="3.08"
98                            fifoPnlUnrealized="4089.983554"
99                            side="Long"
100                            openDateTime=""
101                            holdingPeriodDateTime=""
102                            accruedInt=""
103                            commodityType="" />
104                    </OpenPositions>
105                </FlexStatement>
106            </FlexStatements>
107         </FlexQueryResponse>
108        "##;
109
110    #[test]
111    fn open_positions_parse() -> Result<()> {
112        let statements = Parser::new()?.parse_flex_query_response(PARTIAL_STATEMENT_EXAMPLE)?;
113        assert_eq!(statements.len(), 1);
114        let result = &statements[0];
115
116        // Ensure we got two equity summaries.
117        assert_eq!(result.open_positions.len(), 7);
118
119        assert_eq!(
120            result.open_positions[6],
121            OpenPosition {
122                account_id: "U1234567".to_string(),
123                asset_category: AssetCategory::Stock,
124                conid: 6478131,
125                cost_basis_price: 217.200032892,
126                fifo_pnl_unrealized: 4089.983554,
127                currency: Currency::USD,
128                listing_exchange: "NASDAQ".to_string(),
129                mark_price: 225.38,
130                open_quantity: 500.0,
131                position_value: 112690.0,
132                timestamp_eod_ms: result.open_positions[6].timestamp_eod_ms,
133                ticker: "TTWO".to_string(),
134                side: PositionSide::Long
135            }
136        );
137        Ok(())
138    }
139}