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