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 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 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 assert_eq!(result.trades.len(), 2);
248
249 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 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}