Skip to main content

ibkr_flex_statement/
trade.rs

1use crate::{node_utils::NodeWrapper, statement_section::StatementSectionWithTimezone};
2
3use super::currency::Currency;
4use anyhow::Result;
5use chrono::{NaiveDateTime, TimeZone};
6use chrono_tz::Tz;
7use std::collections::HashMap;
8
9#[derive(Debug, PartialEq)]
10pub enum TradeSide {
11    Buy,
12    Sell,
13}
14
15#[derive(Debug, PartialEq)]
16pub enum OpenCloseIndicator {
17    Close,
18    CloseOpen,
19    // TODO figure out what "" (none) means
20    None,
21    Open,
22}
23
24#[derive(Debug, PartialEq)]
25pub enum OrderType {
26    Limit,
27    Market,
28    MidPrice,
29    Stop,
30}
31
32#[derive(Debug, PartialEq)]
33pub struct Trade {
34    pub account_id: String,
35    pub conid: u32,
36    pub currency: Currency,
37    pub execution_exchange: String,
38    pub execution_id: String,
39    pub execution_timestamp_ms: i64,
40    pub commission: f64,
41    pub listing_exchange: String,
42    pub open_close_indicator: OpenCloseIndicator,
43    pub order_id: String,
44    pub order_type: OrderType,
45    pub price: f64,
46    pub quantity: f64,
47    pub side: TradeSide,
48    pub ticker: String,
49}
50
51impl<'a> TryFrom<&'a str> for OpenCloseIndicator {
52    type Error = anyhow::Error;
53    fn try_from(s: &'a str) -> Result<Self> {
54        match s {
55            "C" => Ok(Self::Close),
56            "C;O" => Ok(Self::CloseOpen),
57            "O" => Ok(OpenCloseIndicator::Open),
58            "" => Ok(OpenCloseIndicator::None),
59            _ => Err(anyhow::Error::msg(format!(
60                "unknown openClose indicator \"{}\"",
61                s
62            ))),
63        }
64    }
65}
66
67impl<'a> TryFrom<&'a str> for OrderType {
68    type Error = anyhow::Error;
69    fn try_from(s: &'a str) -> Result<Self> {
70        match s {
71            "LMT" => Ok(Self::Limit),
72            "MIDPX" => Ok(Self::MidPrice),
73            "MKT" => Ok(Self::Market),
74            "STP" => Ok(Self::Stop),
75            _ => Err(anyhow::Error::msg(format!("unknown order type {}", s))),
76        }
77    }
78}
79
80impl<'a> TryFrom<&'a str> for TradeSide {
81    type Error = anyhow::Error;
82
83    fn try_from(s: &'a str) -> Result<Self> {
84        match s {
85            "BUY" => Ok(Self::Buy),
86            "SELL" => Ok(Self::Sell),
87            _ => Err(anyhow::Error::msg(format!("unknown trade side {}", s))),
88        }
89    }
90}
91
92fn try_parse_trade_execution_time_ms(tz_map: &HashMap<String, Tz>, s: &str) -> Result<i64> {
93    let mut dt_parts = s.split(" ");
94    let datetime_str = dt_parts.next().unwrap();
95
96    let short_timezone = dt_parts.next().unwrap();
97    let timezone = tz_map.get(short_timezone).unwrap();
98
99    let naive_dt = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d;%H:%M:%S %Z")?;
100    let tz_aware_dt = timezone.from_local_datetime(&naive_dt).unwrap();
101
102    // println!("tz_aware_dt: {:?}, timestamp: {}", tz_aware_dt, tz_aware_dt.timestamp());
103
104    Ok(tz_aware_dt.timestamp() * 1000)
105}
106
107impl StatementSectionWithTimezone for Trade {
108    fn from_node(node: &NodeWrapper, tz_map: &HashMap<String, Tz>) -> Result<Trade> {
109        Ok(Trade {
110            account_id: node.get_attribute("accountId")?,
111            commission: node.parse_attribute("ibCommission")?,
112            conid: node.parse_attribute("conid")?,
113            currency: Currency::try_from(node.node.attribute("currency").unwrap())?,
114            execution_exchange: node.get_attribute("exchange")?,
115            execution_id: node.get_attribute("ibExecID")?,
116            execution_timestamp_ms: try_parse_trade_execution_time_ms(
117                tz_map,
118                node.node.attribute("dateTime").unwrap(),
119            )?,
120            listing_exchange: node.get_attribute("listingExchange")?,
121            open_close_indicator: OpenCloseIndicator::try_from(
122                node.node.attribute("openCloseIndicator").unwrap(),
123            )?,
124            order_id: node.get_attribute("brokerageOrderID")?,
125            order_type: OrderType::try_from(node.node.attribute("orderType").unwrap())?,
126            price: node.parse_attribute("tradePrice")?,
127            quantity: node.parse_attribute("quantity")?,
128            side: TradeSide::try_from(node.node.attribute("buySell").unwrap())?,
129            ticker: node.get_attribute("symbol")?,
130        })
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::Parser;
138    use anyhow::Result;
139
140    const PARTIAL_STATEMENT_EXAMPLE: &str = r##"
141        <FlexQueryResponse queryName="example-query" type="AF">
142            <FlexStatements count="1">
143                <FlexStatement accountId="U1234567" fromDate="2025-04-25" toDate="2025-04-25" period="LastBusinessDay" whenGenerated="2025-04-26;13:34:28 EDT">
144                    <AccountInformation accountId="U1234567" accountType="Individual" customerType="Individual" accountCapabilities="Portfolio Margin" tradingPermissions="Stocks,Options,Warrants,Forex,Futures,Crypto Currencies,Mutual Funds,Fully Paid Stock Loan" />
145                    <Trades>
146                        <Trade accountId="U1234567" 
147                               currency="USD"
148                               symbol="ARGX"
149                               conid="276343981"
150                               listingExchange="NASDAQ"
151                               tradeID="7587063231"
152                               reportDate="2025-04-25"
153                               dateTime="2025-04-25;10:19:55 EDT"
154                               tradeDate="2025-04-25"
155                               transactionType="ExchTrade"
156                               exchange="BYX"
157                               quantity="1"
158                               tradePrice="606.57"
159                               tradeMoney="606.57"
160                               proceeds="-606.57"
161                               ibCommission="-1.000035"
162                               ibCommissionCurrency="USD"
163                               netCash="-607.570035"
164                               closePrice="614.76"
165                               openCloseIndicator="O"
166                               cost="607.570035"
167                               fifoPnlRealized="0"
168                               mtmPnl="8.19"
169                               origTradePrice="0"
170                               origTradeDate=""
171                               origTradeID=""
172                               origOrderID="0"
173                               origTransactionID="0"
174                               buySell="BUY"
175                               ibOrderID="4015030800"
176                               transactionID="32580112485"
177                               ibExecID="0000edae.680b59d1.01.01"
178                               orderTime="2025-04-25;10:19:55 EDT"
179                               openDateTime=""
180                               holdingPeriodDateTime=""
181                               whenRealized=""
182                               whenReopened=""
183                               orderType="LMT"
184                               accruedInt="0"
185                               assetCategory="STK"
186                               brokerageOrderID="002ce642.00014b44.680b0ed6.0001"
187                               orderReference=""
188                               isAPIOrder="N"
189                               initialInvestment="" />
190                        <Trade accountId="U1234567" 
191                               currency="USD"
192                               symbol="GEO"
193                               conid="158655765"
194                               listingExchange="NYSE"
195                               tradeID="7587946875"
196                               reportDate="2025-04-25"
197                               dateTime="2025-04-25;11:24:28 EDT"
198                               tradeDate="2025-04-25"
199                               transactionType="ExchTrade"
200                               exchange="NYSE"
201                               quantity="1000"
202                               tradePrice="30.85"
203                               tradeMoney="30850"
204                               proceeds="-30850"
205                               ibCommission="-5.035"
206                               ibCommissionCurrency="USD"
207                               netCash="-30855.035"
208                               closePrice="30.58"
209                               openCloseIndicator="O"
210                               cost="30855.035"
211                               fifoPnlRealized="0"
212                               mtmPnl="-270"
213                               origTradePrice="0"
214                               origTradeDate=""
215                               origTradeID=""
216                               origOrderID="0"
217                               origTransactionID="0"
218                               buySell="BUY"
219                               ibOrderID="4015577648"
220                               transactionID="32582764875"
221                               ibExecID="00012e0e.680b7717.01.01"
222                               orderTime="2025-04-25;11:24:26 EDT"
223                               openDateTime=""
224                               holdingPeriodDateTime=""
225                               whenRealized=""
226                               whenReopened=""
227                               orderType="LMT"
228                               accruedInt="0"
229                               assetCategory="STK"
230                               brokerageOrderID="002ce642.00014b44.680b0fbf.0001"
231                               orderReference=""
232                               isAPIOrder="N"
233                               initialInvestment="" />
234                    </Trades>
235                </FlexStatement>
236            </FlexStatements>
237         </FlexQueryResponse>
238        "##;
239
240    #[test]
241    fn trades_parse() -> Result<()> {
242        let statements = Parser::new()?.parse_flex_query_response(PARTIAL_STATEMENT_EXAMPLE)?;
243        assert_eq!(statements.len(), 1);
244        let result = &statements[0];
245
246        // Ensure we got two trades.
247        assert_eq!(result.trades.len(), 2);
248
249        // Ensure the first trade matches.
250        assert_eq!(
251            result.trades[0],
252            Trade {
253                account_id: "U1234567".to_string(),
254                commission: -1.000035,
255                conid: 276343981,
256                currency: Currency::USD,
257                execution_exchange: "BYX".to_string(),
258                execution_id: "0000edae.680b59d1.01.01".to_string(),
259                execution_timestamp_ms: result.trades[0].execution_timestamp_ms,
260                open_close_indicator: OpenCloseIndicator::Open,
261                order_id: "002ce642.00014b44.680b0ed6.0001".to_string(),
262                order_type: OrderType::Limit,
263                price: 606.57,
264                quantity: 1.0,
265                side: TradeSide::Buy,
266                ticker: "ARGX".to_string(),
267                listing_exchange: "NASDAQ".to_string(),
268            }
269        );
270
271        // Ensure the first trade matches.
272        assert_eq!(
273            result.trades[1],
274            Trade {
275                account_id: "U1234567".to_string(),
276                commission: -5.035,
277                conid: 158655765,
278                currency: Currency::USD,
279                execution_exchange: "NYSE".to_string(),
280                execution_id: "00012e0e.680b7717.01.01".to_string(),
281                execution_timestamp_ms: result.trades[1].execution_timestamp_ms,
282                open_close_indicator: OpenCloseIndicator::Open,
283                order_id: "002ce642.00014b44.680b0fbf.0001".to_string(),
284                order_type: OrderType::Limit,
285                price: 30.85,
286                quantity: 1000.0,
287                side: TradeSide::Buy,
288                ticker: "GEO".to_string(),
289                listing_exchange: "NYSE".to_string(),
290            }
291        );
292        Ok(())
293    }
294}