ib_flex/types/activity.rs
1//! Activity FLEX statement types
2
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use super::common::{AssetCategory, BuySell, OpenClose, OrderType, PutCall};
8use crate::parsers::xml_utils::{deserialize_optional_date, deserialize_optional_decimal};
9
10/// Top-level FLEX query response
11///
12/// This is the root XML element in IB FLEX files. It wraps one or more
13/// [`ActivityFlexStatement`]s along with query metadata.
14///
15/// **Note**: When using [`crate::parse_activity_flex`], this wrapper is handled
16/// automatically and you receive the [`ActivityFlexStatement`] directly.
17///
18/// # Example
19/// ```
20/// use ib_flex::types::FlexQueryResponse;
21/// use quick_xml::de::from_str;
22///
23/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
24/// <FlexQueryResponse queryName="Activity" type="AF">
25/// <FlexStatements count="1">
26/// <FlexStatement accountId="U1234567" fromDate="2025-01-01"
27/// toDate="2025-01-31" whenGenerated="2025-01-31;150000">
28/// <Trades />
29/// <OpenPositions />
30/// <CashTransactions />
31/// <CorporateActions />
32/// <SecuritiesInfo />
33/// <ConversionRates />
34/// </FlexStatement>
35/// </FlexStatements>
36/// </FlexQueryResponse>"#;
37///
38/// let response: FlexQueryResponse = from_str(xml).unwrap();
39/// assert_eq!(response.query_name, Some("Activity".to_string()));
40/// assert_eq!(response.statements.statements.len(), 1);
41/// # Ok::<(), Box<dyn std::error::Error>>(())
42/// ```
43#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
44#[serde(rename = "FlexQueryResponse")]
45pub struct FlexQueryResponse {
46 /// Query name
47 #[serde(rename = "@queryName", default)]
48 pub query_name: Option<String>,
49
50 /// Query type
51 #[serde(rename = "@type", default)]
52 pub query_type: Option<String>,
53
54 /// FlexStatements wrapper
55 #[serde(rename = "FlexStatements")]
56 pub statements: FlexStatementsWrapper,
57}
58
59/// Wrapper for FlexStatements
60#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
61pub struct FlexStatementsWrapper {
62 /// Count
63 #[serde(rename = "@count", default)]
64 pub count: Option<String>,
65
66 /// Flex statement(s)
67 #[serde(rename = "FlexStatement")]
68 pub statements: Vec<ActivityFlexStatement>,
69}
70
71/// Top-level Activity FLEX statement
72///
73/// Contains all data from an Activity FLEX query including trades,
74/// positions, cash transactions, and other portfolio data.
75///
76/// This is the main type returned by [`crate::parse_activity_flex`].
77///
78/// # Example
79/// ```no_run
80/// use ib_flex::parse_activity_flex;
81/// use rust_decimal::Decimal;
82///
83/// let xml = std::fs::read_to_string("activity.xml")?;
84/// let statement = parse_activity_flex(&xml)?;
85///
86/// // Access account and date range
87/// println!("Account: {}", statement.account_id);
88/// println!("Period: {} to {}", statement.from_date, statement.to_date);
89///
90/// // Iterate through all trades
91/// for trade in &statement.trades.items {
92/// println!("{}: {} {} @ {}",
93/// trade.symbol,
94/// trade.buy_sell.as_ref().map(|b| format!("{:?}", b)).unwrap_or_default(),
95/// trade.quantity.unwrap_or_default(),
96/// trade.price.unwrap_or_default()
97/// );
98/// }
99///
100/// // Calculate total P&L
101/// let total_pnl: Decimal = statement.trades.items.iter()
102/// .filter_map(|t| t.fifo_pnl_realized)
103/// .sum();
104/// println!("Total realized P&L: {}", total_pnl);
105///
106/// // Access positions
107/// for pos in &statement.positions.items {
108/// println!("{}: {} shares @ {}",
109/// pos.symbol,
110/// pos.quantity,
111/// pos.mark_price
112/// );
113/// }
114/// # Ok::<(), Box<dyn std::error::Error>>(())
115/// ```
116#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
117#[serde(rename = "FlexStatement")]
118pub struct ActivityFlexStatement {
119 /// IB account number
120 #[serde(rename = "@accountId")]
121 pub account_id: String,
122
123 /// Statement date range - start date
124 #[serde(
125 rename = "@fromDate",
126 deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
127 )]
128 pub from_date: NaiveDate,
129
130 /// Statement date range - end date
131 #[serde(
132 rename = "@toDate",
133 deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
134 )]
135 pub to_date: NaiveDate,
136
137 /// When the report was generated
138 #[serde(rename = "@whenGenerated")]
139 pub when_generated: String, // Parse separately due to IB format
140
141 /// All trades in the period
142 #[serde(rename = "Trades", default)]
143 pub trades: TradesWrapper,
144
145 /// Open positions at end of period
146 #[serde(rename = "OpenPositions", default)]
147 pub positions: PositionsWrapper,
148
149 /// Cash transactions (deposits, withdrawals, dividends, interest)
150 #[serde(rename = "CashTransactions", default)]
151 pub cash_transactions: CashTransactionsWrapper,
152
153 /// Corporate actions (splits, mergers, spinoffs)
154 #[serde(rename = "CorporateActions", default)]
155 pub corporate_actions: CorporateActionsWrapper,
156
157 /// Securities information (reference data)
158 #[serde(rename = "SecuritiesInfo", default)]
159 pub securities_info: SecuritiesInfoWrapper,
160
161 /// Currency conversion rates
162 #[serde(rename = "ConversionRates", default)]
163 pub conversion_rates: ConversionRatesWrapper,
164
165 // Extended v0.2.0+ sections
166 /// Account information
167 #[serde(rename = "AccountInformation", default)]
168 pub account_information: Option<super::extended::AccountInformation>,
169
170 /// Change in NAV
171 #[serde(rename = "ChangeInNAV", default)]
172 pub change_in_nav: ChangeInNAVWrapper,
173
174 /// Equity summary by report date in base currency
175 #[serde(rename = "EquitySummaryInBase", default)]
176 pub equity_summary: EquitySummaryWrapper,
177
178 /// Cash report by currency
179 #[serde(rename = "CashReport", default)]
180 pub cash_report: CashReportWrapper,
181
182 /// Trade confirmations
183 #[serde(rename = "TradeConfirms", default)]
184 pub trade_confirms: TradeConfirmsWrapper,
185
186 /// Option exercises, assignments, and expirations
187 #[serde(rename = "OptionEAE", default)]
188 pub option_eae: OptionEAEWrapper,
189
190 /// Foreign exchange transactions
191 #[serde(rename = "FxTransactions", default)]
192 pub fx_transactions: FxTransactionsWrapper,
193
194 /// Change in dividend accruals
195 #[serde(rename = "ChangeInDividendAccruals", default)]
196 pub change_in_dividend_accruals: ChangeInDividendAccrualsWrapper,
197
198 /// Open dividend accruals
199 #[serde(rename = "OpenDividendAccruals", default)]
200 pub open_dividend_accruals: OpenDividendAccrualsWrapper,
201
202 /// Interest accruals by currency
203 #[serde(rename = "InterestAccruals", default)]
204 pub interest_accruals: InterestAccrualsWrapper,
205
206 /// Security transfers
207 #[serde(rename = "Transfers", default)]
208 pub transfers: TransfersWrapper,
209}
210
211/// Wrapper for trades section
212#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
213pub struct TradesWrapper {
214 /// List of trades
215 #[serde(rename = "Trade", default)]
216 pub items: Vec<Trade>,
217}
218
219/// Wrapper for positions section
220#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
221pub struct PositionsWrapper {
222 /// List of positions
223 #[serde(rename = "OpenPosition", default)]
224 pub items: Vec<Position>,
225}
226
227/// Wrapper for cash transactions section
228#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
229pub struct CashTransactionsWrapper {
230 /// List of cash transactions
231 #[serde(rename = "CashTransaction", default)]
232 pub items: Vec<CashTransaction>,
233}
234
235/// Wrapper for corporate actions section
236#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
237pub struct CorporateActionsWrapper {
238 /// List of corporate actions
239 #[serde(rename = "CorporateAction", default)]
240 pub items: Vec<CorporateAction>,
241}
242
243/// A single trade execution
244///
245/// Represents one trade execution from the Activity FLEX statement.
246/// Includes all trade details: security info, quantities, prices, fees, and P&L.
247///
248/// # Example
249/// ```no_run
250/// use ib_flex::parse_activity_flex;
251/// use ib_flex::{AssetCategory, BuySell};
252///
253/// let xml = std::fs::read_to_string("activity.xml")?;
254/// let statement = parse_activity_flex(&xml)?;
255///
256/// for trade in &statement.trades.items {
257/// // Access basic trade info
258/// println!("Symbol: {}", trade.symbol);
259/// println!("Asset: {:?}", trade.asset_category);
260///
261/// // Check trade direction
262/// match trade.buy_sell {
263/// Some(BuySell::Buy) => println!("Bought"),
264/// Some(BuySell::Sell) => println!("Sold"),
265/// _ => {}
266/// }
267///
268/// // Calculate total cost
269/// let quantity = trade.quantity.unwrap_or_default();
270/// let price = trade.price.unwrap_or_default();
271/// let cost = quantity * price;
272/// println!("Cost: {}", cost);
273///
274/// // Access P&L if available
275/// if let Some(pnl) = trade.fifo_pnl_realized {
276/// println!("Realized P&L: {}", pnl);
277/// }
278///
279/// // Check for options
280/// if trade.asset_category == AssetCategory::Option {
281/// println!("Strike: {:?}", trade.strike);
282/// println!("Expiry: {:?}", trade.expiry);
283/// println!("Put/Call: {:?}", trade.put_call);
284/// }
285/// }
286/// # Ok::<(), Box<dyn std::error::Error>>(())
287/// ```
288#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
289pub struct Trade {
290 // IB identifiers
291 /// IB account number
292 #[serde(rename = "@accountId")]
293 pub account_id: String,
294
295 /// IB transaction ID (unique identifier for idempotency)
296 #[serde(rename = "@transactionID", default)]
297 pub transaction_id: Option<String>,
298
299 /// IB order ID (may be shared across multiple executions)
300 #[serde(rename = "@orderID", default)]
301 pub ib_order_id: Option<String>,
302
303 /// Execution ID
304 #[serde(rename = "@execID", default)]
305 pub exec_id: Option<String>,
306
307 /// Trade ID
308 #[serde(rename = "@tradeID", default)]
309 pub trade_id: Option<String>,
310
311 // Security
312 /// IB contract ID (unique per security)
313 #[serde(rename = "@conid")]
314 pub conid: String,
315
316 /// Ticker symbol
317 #[serde(rename = "@symbol")]
318 pub symbol: String,
319
320 /// Security description
321 #[serde(rename = "@description", default)]
322 pub description: Option<String>,
323
324 /// Asset category (stock, option, future, etc.)
325 #[serde(rename = "@assetCategory")]
326 pub asset_category: AssetCategory,
327
328 /// Contract multiplier (for futures/options)
329 #[serde(
330 rename = "@multiplier",
331 default,
332 deserialize_with = "deserialize_optional_decimal"
333 )]
334 pub multiplier: Option<Decimal>,
335
336 // Options/Futures
337 /// Underlying security's contract ID (for derivatives)
338 #[serde(rename = "@underlyingConid", default)]
339 pub underlying_conid: Option<String>,
340
341 /// Underlying symbol
342 #[serde(rename = "@underlyingSymbol", default)]
343 pub underlying_symbol: Option<String>,
344
345 /// Strike price (for options)
346 #[serde(
347 rename = "@strike",
348 default,
349 deserialize_with = "deserialize_optional_decimal"
350 )]
351 pub strike: Option<Decimal>,
352
353 /// Expiry date (for options/futures)
354 #[serde(
355 rename = "@expiry",
356 default,
357 deserialize_with = "deserialize_optional_date"
358 )]
359 pub expiry: Option<NaiveDate>,
360
361 /// Put or Call (for options)
362 #[serde(rename = "@putCall", default)]
363 pub put_call: Option<PutCall>,
364
365 // Trade details
366 /// Trade date
367 #[serde(
368 rename = "@tradeDate",
369 deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
370 )]
371 pub trade_date: NaiveDate,
372
373 /// Trade time (date + time) - parsed from dateTime field
374 #[serde(rename = "@dateTime", default)]
375 pub trade_time: Option<String>, // Will parse manually
376
377 /// Settlement date
378 #[serde(
379 rename = "@settleDateTarget",
380 deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
381 )]
382 pub settle_date: NaiveDate,
383
384 /// Buy or Sell
385 #[serde(rename = "@buySell", default)]
386 pub buy_sell: Option<BuySell>,
387
388 /// Open or Close indicator (for options/futures)
389 #[serde(rename = "@openCloseIndicator", default)]
390 pub open_close: Option<OpenClose>,
391
392 /// Order type (market, limit, stop, etc.)
393 #[serde(rename = "@orderType", default)]
394 pub order_type: Option<OrderType>,
395
396 // Quantities and prices
397 /// Quantity (number of shares/contracts)
398 #[serde(
399 rename = "@quantity",
400 default,
401 deserialize_with = "deserialize_optional_decimal"
402 )]
403 pub quantity: Option<Decimal>,
404
405 /// Trade price per share/contract
406 #[serde(
407 rename = "@price",
408 default,
409 deserialize_with = "deserialize_optional_decimal"
410 )]
411 pub price: Option<Decimal>,
412
413 /// Trade amount
414 #[serde(
415 rename = "@amount",
416 default,
417 deserialize_with = "deserialize_optional_decimal"
418 )]
419 pub amount: Option<Decimal>,
420
421 /// Trade proceeds (negative for buys, positive for sells)
422 #[serde(rename = "@proceeds")]
423 pub proceeds: Decimal,
424
425 /// Commission paid
426 #[serde(rename = "@ibCommission")]
427 pub commission: Decimal,
428
429 /// Commission currency
430 #[serde(rename = "@ibCommissionCurrency", default)]
431 pub commission_currency: Option<String>,
432
433 /// Taxes paid
434 #[serde(
435 rename = "@taxes",
436 default,
437 deserialize_with = "deserialize_optional_decimal"
438 )]
439 pub taxes: Option<Decimal>,
440
441 /// Net cash (proceeds + commission + taxes)
442 #[serde(
443 rename = "@netCash",
444 default,
445 deserialize_with = "deserialize_optional_decimal"
446 )]
447 pub net_cash: Option<Decimal>,
448
449 /// Cost
450 #[serde(
451 rename = "@cost",
452 default,
453 deserialize_with = "deserialize_optional_decimal"
454 )]
455 pub cost: Option<Decimal>,
456
457 // P&L
458 /// FIFO realized P&L (for closing trades)
459 #[serde(
460 rename = "@fifoPnlRealized",
461 default,
462 deserialize_with = "deserialize_optional_decimal"
463 )]
464 pub fifo_pnl_realized: Option<Decimal>,
465
466 /// Mark-to-market P&L
467 #[serde(
468 rename = "@mtmPnl",
469 default,
470 deserialize_with = "deserialize_optional_decimal"
471 )]
472 pub mtm_pnl: Option<Decimal>,
473
474 /// FX P&L (for multi-currency)
475 #[serde(
476 rename = "@fxPnl",
477 default,
478 deserialize_with = "deserialize_optional_decimal"
479 )]
480 pub fx_pnl: Option<Decimal>,
481
482 // Currency
483 /// Trade currency
484 #[serde(rename = "@currency")]
485 pub currency: String,
486
487 /// FX rate to base currency
488 #[serde(
489 rename = "@fxRateToBase",
490 default,
491 deserialize_with = "deserialize_optional_decimal"
492 )]
493 pub fx_rate_to_base: Option<Decimal>,
494
495 // Additional fields
496 /// Listing exchange
497 #[serde(rename = "@listingExchange", default)]
498 pub listing_exchange: Option<String>,
499}
500
501/// An open position snapshot
502///
503/// Represents a single open position at the end of the reporting period.
504/// Includes quantity, current market price, cost basis, and unrealized P&L.
505///
506/// # Example
507/// ```no_run
508/// use ib_flex::parse_activity_flex;
509/// use rust_decimal::Decimal;
510///
511/// let xml = std::fs::read_to_string("activity.xml")?;
512/// let statement = parse_activity_flex(&xml)?;
513///
514/// for position in &statement.positions.items {
515/// println!("{}: {} shares", position.symbol, position.quantity);
516/// println!(" Current price: {}", position.mark_price);
517/// println!(" Position value: {}", position.position_value);
518///
519/// // Calculate gain/loss percentage
520/// if let Some(cost_basis) = position.cost_basis_money {
521/// let current_value = position.position_value;
522/// let gain_pct = ((current_value - cost_basis) / cost_basis) * Decimal::from(100);
523/// println!(" Gain: {:.2}%", gain_pct);
524/// }
525///
526/// // Show unrealized P&L
527/// if let Some(pnl) = position.fifo_pnl_unrealized {
528/// println!(" Unrealized P&L: {}", pnl);
529/// }
530///
531/// // Check if short position
532/// if position.quantity < Decimal::ZERO {
533/// println!(" SHORT POSITION");
534/// }
535/// }
536/// # Ok::<(), Box<dyn std::error::Error>>(())
537/// ```
538#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
539pub struct Position {
540 /// IB account number
541 #[serde(rename = "@accountId")]
542 pub account_id: String,
543
544 /// IB contract ID
545 #[serde(rename = "@conid")]
546 pub conid: String,
547
548 /// Ticker symbol
549 #[serde(rename = "@symbol")]
550 pub symbol: String,
551
552 /// Security description
553 #[serde(rename = "@description", default)]
554 pub description: Option<String>,
555
556 /// Asset category
557 #[serde(rename = "@assetCategory")]
558 pub asset_category: AssetCategory,
559
560 /// Contract multiplier
561 #[serde(
562 rename = "@multiplier",
563 default,
564 deserialize_with = "deserialize_optional_decimal"
565 )]
566 pub multiplier: Option<Decimal>,
567
568 /// Strike (for options)
569 #[serde(
570 rename = "@strike",
571 default,
572 deserialize_with = "deserialize_optional_decimal"
573 )]
574 pub strike: Option<Decimal>,
575
576 /// Expiry (for options/futures)
577 #[serde(
578 rename = "@expiry",
579 default,
580 deserialize_with = "deserialize_optional_date"
581 )]
582 pub expiry: Option<NaiveDate>,
583
584 /// Put or Call
585 #[serde(rename = "@putCall", default)]
586 pub put_call: Option<PutCall>,
587
588 /// Position quantity (negative for short)
589 #[serde(rename = "@position")]
590 pub quantity: Decimal,
591
592 /// Mark price (current market price)
593 #[serde(rename = "@markPrice")]
594 pub mark_price: Decimal,
595
596 /// Position value (quantity * mark_price * multiplier)
597 #[serde(rename = "@positionValue")]
598 pub position_value: Decimal,
599
600 /// Open price
601 #[serde(
602 rename = "@openPrice",
603 default,
604 deserialize_with = "deserialize_optional_decimal"
605 )]
606 pub open_price: Option<Decimal>,
607
608 /// Cost basis price per share/contract
609 #[serde(
610 rename = "@costBasisPrice",
611 default,
612 deserialize_with = "deserialize_optional_decimal"
613 )]
614 pub cost_basis_price: Option<Decimal>,
615
616 /// Total cost basis
617 #[serde(
618 rename = "@costBasisMoney",
619 default,
620 deserialize_with = "deserialize_optional_decimal"
621 )]
622 pub cost_basis_money: Option<Decimal>,
623
624 /// FIFO unrealized P&L
625 #[serde(
626 rename = "@fifoPnlUnrealized",
627 default,
628 deserialize_with = "deserialize_optional_decimal"
629 )]
630 pub fifo_pnl_unrealized: Option<Decimal>,
631
632 /// Percent of NAV
633 #[serde(
634 rename = "@percentOfNAV",
635 default,
636 deserialize_with = "deserialize_optional_decimal"
637 )]
638 pub percent_of_nav: Option<Decimal>,
639
640 /// Side (Long/Short)
641 #[serde(rename = "@side", default)]
642 pub side: Option<String>,
643
644 /// Currency
645 #[serde(rename = "@currency")]
646 pub currency: String,
647
648 /// FX rate to base currency
649 #[serde(
650 rename = "@fxRateToBase",
651 default,
652 deserialize_with = "deserialize_optional_decimal"
653 )]
654 pub fx_rate_to_base: Option<Decimal>,
655
656 /// Date of this position snapshot
657 #[serde(
658 rename = "@reportDate",
659 deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
660 )]
661 pub report_date: NaiveDate,
662}
663
664/// A cash transaction (deposit, withdrawal, dividend, interest, fee)
665///
666/// Represents any cash flow that affects your account balance: deposits,
667/// withdrawals, dividends, interest payments, withholding taxes, and fees.
668///
669/// # Example
670/// ```no_run
671/// use ib_flex::parse_activity_flex;
672/// use rust_decimal::Decimal;
673///
674/// let xml = std::fs::read_to_string("activity.xml")?;
675/// let statement = parse_activity_flex(&xml)?;
676///
677/// // Categorize cash flows
678/// let mut dividends = Decimal::ZERO;
679/// let mut interest = Decimal::ZERO;
680/// let mut fees = Decimal::ZERO;
681///
682/// for cash_txn in &statement.cash_transactions.items {
683/// match cash_txn.transaction_type.as_str() {
684/// "Dividends" => {
685/// dividends += cash_txn.amount;
686/// println!("Dividend from {}: {}",
687/// cash_txn.symbol.as_ref().unwrap_or(&"N/A".to_string()),
688/// cash_txn.amount
689/// );
690/// }
691/// "Broker Interest Paid" | "Broker Interest Received" => {
692/// interest += cash_txn.amount;
693/// }
694/// "Other Fees" | "Commission Adjustments" => {
695/// fees += cash_txn.amount;
696/// }
697/// _ => {
698/// println!("{}: {}", cash_txn.transaction_type, cash_txn.amount);
699/// }
700/// }
701/// }
702///
703/// println!("\nTotals:");
704/// println!(" Dividends: {}", dividends);
705/// println!(" Interest: {}", interest);
706/// println!(" Fees: {}", fees);
707/// # Ok::<(), Box<dyn std::error::Error>>(())
708/// ```
709#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
710pub struct CashTransaction {
711 /// IB account number
712 #[serde(rename = "@accountId")]
713 pub account_id: String,
714
715 /// IB transaction ID
716 #[serde(rename = "@transactionID", default)]
717 pub transaction_id: Option<String>,
718
719 /// Transaction type (Deposits, Dividends, WithholdingTax, BrokerInterest, etc.)
720 #[serde(rename = "@type")]
721 pub transaction_type: String,
722
723 /// Transaction date
724 #[serde(
725 rename = "@date",
726 default,
727 deserialize_with = "deserialize_optional_date"
728 )]
729 pub date: Option<NaiveDate>,
730
731 /// Transaction datetime
732 #[serde(rename = "@dateTime", default)]
733 pub date_time: Option<String>,
734
735 /// Report date
736 #[serde(
737 rename = "@reportDate",
738 default,
739 deserialize_with = "deserialize_optional_date"
740 )]
741 pub report_date: Option<NaiveDate>,
742
743 /// Amount (positive for credits, negative for debits)
744 #[serde(rename = "@amount")]
745 pub amount: Decimal,
746
747 /// Currency
748 #[serde(rename = "@currency")]
749 pub currency: String,
750
751 /// FX rate to base currency
752 #[serde(
753 rename = "@fxRateToBase",
754 default,
755 deserialize_with = "deserialize_optional_decimal"
756 )]
757 pub fx_rate_to_base: Option<Decimal>,
758
759 /// Description of transaction
760 #[serde(rename = "@description", default)]
761 pub description: Option<String>,
762
763 /// Asset category
764 #[serde(rename = "@assetCategory", default)]
765 pub asset_category: Option<AssetCategory>,
766
767 /// Related security's contract ID (for dividends)
768 #[serde(rename = "@conid", default)]
769 pub conid: Option<String>,
770
771 /// Related security's symbol
772 #[serde(rename = "@symbol", default)]
773 pub symbol: Option<String>,
774}
775
776/// A corporate action (split, merger, spinoff, etc.)
777///
778/// Represents corporate events that affect your holdings: stock splits,
779/// reverse splits, mergers, spinoffs, tender offers, bond conversions, etc.
780///
781/// # Example
782/// ```no_run
783/// use ib_flex::parse_activity_flex;
784///
785/// let xml = std::fs::read_to_string("activity.xml")?;
786/// let statement = parse_activity_flex(&xml)?;
787///
788/// for action in &statement.corporate_actions.items {
789/// println!("{}: {}", action.symbol, action.description);
790/// println!(" Type: {}", action.action_type);
791///
792/// // Check action type
793/// match action.action_type.as_str() {
794/// "FS" => println!(" Forward stock split"),
795/// "RS" => println!(" Reverse stock split"),
796/// "SO" => println!(" Spinoff"),
797/// "TO" => println!(" Tender offer"),
798/// "TC" => println!(" Treasury bill/bond maturity"),
799/// "BC" => println!(" Bond conversion"),
800/// _ => println!(" Other: {}", action.action_type),
801/// }
802///
803/// // Show quantities and proceeds
804/// if let Some(qty) = action.quantity {
805/// println!(" Quantity: {}", qty);
806/// }
807/// if let Some(proceeds) = action.proceeds {
808/// println!(" Proceeds: {}", proceeds);
809/// }
810///
811/// // Show realized P&L if applicable
812/// if let Some(pnl) = action.fifo_pnl_realized {
813/// println!(" Realized P&L: {}", pnl);
814/// }
815/// }
816/// # Ok::<(), Box<dyn std::error::Error>>(())
817/// ```
818#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
819pub struct CorporateAction {
820 /// IB account number
821 #[serde(rename = "@accountId")]
822 pub account_id: String,
823
824 /// IB transaction ID
825 #[serde(rename = "@transactionID", default)]
826 pub transaction_id: Option<String>,
827
828 /// Action ID
829 #[serde(rename = "@actionID", default)]
830 pub action_id: Option<String>,
831
832 /// Action type (Split, Merger, Spinoff, etc.)
833 #[serde(rename = "@type")]
834 pub action_type: String,
835
836 /// Action date
837 #[serde(
838 rename = "@date",
839 default,
840 deserialize_with = "deserialize_optional_date"
841 )]
842 pub action_date: Option<NaiveDate>,
843
844 /// Action datetime
845 #[serde(rename = "@dateTime", default)]
846 pub date_time: Option<String>,
847
848 /// Report date
849 #[serde(
850 rename = "@reportDate",
851 deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
852 )]
853 pub report_date: NaiveDate,
854
855 /// IB contract ID
856 #[serde(rename = "@conid")]
857 pub conid: String,
858
859 /// Ticker symbol
860 #[serde(rename = "@symbol")]
861 pub symbol: String,
862
863 /// Description of corporate action
864 #[serde(rename = "@description")]
865 pub description: String,
866
867 /// Asset category
868 #[serde(rename = "@assetCategory", default)]
869 pub asset_category: Option<AssetCategory>,
870
871 /// Currency
872 #[serde(rename = "@currency", default)]
873 pub currency: Option<String>,
874
875 /// FX rate to base
876 #[serde(
877 rename = "@fxRateToBase",
878 default,
879 deserialize_with = "deserialize_optional_decimal"
880 )]
881 pub fx_rate_to_base: Option<Decimal>,
882
883 /// Quantity affected
884 #[serde(
885 rename = "@quantity",
886 default,
887 deserialize_with = "deserialize_optional_decimal"
888 )]
889 pub quantity: Option<Decimal>,
890
891 /// Amount
892 #[serde(
893 rename = "@amount",
894 default,
895 deserialize_with = "deserialize_optional_decimal"
896 )]
897 pub amount: Option<Decimal>,
898
899 /// Proceeds (if any)
900 #[serde(
901 rename = "@proceeds",
902 default,
903 deserialize_with = "deserialize_optional_decimal"
904 )]
905 pub proceeds: Option<Decimal>,
906
907 /// Value (if any)
908 #[serde(
909 rename = "@value",
910 default,
911 deserialize_with = "deserialize_optional_decimal"
912 )]
913 pub value: Option<Decimal>,
914
915 /// FIFO P&L realized
916 #[serde(
917 rename = "@fifoPnlRealized",
918 default,
919 deserialize_with = "deserialize_optional_decimal"
920 )]
921 pub fifo_pnl_realized: Option<Decimal>,
922}
923
924/// Security information (reference data)
925///
926/// Provides detailed reference data for securities in the statement.
927/// Includes identifiers (CUSIP, ISIN, FIGI), exchange info, and derivative details.
928///
929/// # Example
930/// ```no_run
931/// use ib_flex::parse_activity_flex;
932/// use ib_flex::AssetCategory;
933///
934/// let xml = std::fs::read_to_string("activity.xml")?;
935/// let statement = parse_activity_flex(&xml)?;
936///
937/// for security in &statement.securities_info.items {
938/// println!("{} ({})", security.symbol, security.conid);
939///
940/// // Print description
941/// if let Some(desc) = &security.description {
942/// println!(" Description: {}", desc);
943/// }
944///
945/// // Print identifiers
946/// if let Some(cusip) = &security.cusip {
947/// println!(" CUSIP: {}", cusip);
948/// }
949/// if let Some(isin) = &security.isin {
950/// println!(" ISIN: {}", isin);
951/// }
952///
953/// // Show derivative info for options
954/// if security.asset_category == AssetCategory::Option {
955/// println!(" Underlying: {:?}", security.underlying_symbol);
956/// println!(" Strike: {:?}", security.strike);
957/// println!(" Expiry: {:?}", security.expiry);
958/// println!(" Type: {:?}", security.put_call);
959/// println!(" Multiplier: {:?}", security.multiplier);
960/// }
961/// }
962/// # Ok::<(), Box<dyn std::error::Error>>(())
963/// ```
964#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
965pub struct SecurityInfo {
966 /// Asset category
967 #[serde(rename = "@assetCategory")]
968 pub asset_category: AssetCategory,
969
970 /// Ticker symbol
971 #[serde(rename = "@symbol")]
972 pub symbol: String,
973
974 /// Security description
975 #[serde(rename = "@description", default)]
976 pub description: Option<String>,
977
978 /// IB contract ID
979 #[serde(rename = "@conid")]
980 pub conid: String,
981
982 /// Security ID
983 #[serde(rename = "@securityID", default)]
984 pub security_id: Option<String>,
985
986 /// Security ID type
987 #[serde(rename = "@securityIDType", default)]
988 pub security_id_type: Option<String>,
989
990 /// CUSIP
991 #[serde(rename = "@cusip", default)]
992 pub cusip: Option<String>,
993
994 /// ISIN
995 #[serde(rename = "@isin", default)]
996 pub isin: Option<String>,
997
998 /// FIGI
999 #[serde(rename = "@figi", default)]
1000 pub figi: Option<String>,
1001
1002 /// Listing exchange
1003 #[serde(rename = "@listingExchange", default)]
1004 pub listing_exchange: Option<String>,
1005
1006 /// Underlying contract ID
1007 #[serde(rename = "@underlyingConid", default)]
1008 pub underlying_conid: Option<String>,
1009
1010 /// Underlying symbol
1011 #[serde(rename = "@underlyingSymbol", default)]
1012 pub underlying_symbol: Option<String>,
1013
1014 /// Underlying security ID
1015 #[serde(rename = "@underlyingSecurityID", default)]
1016 pub underlying_security_id: Option<String>,
1017
1018 /// Underlying listing exchange
1019 #[serde(rename = "@underlyingListingExchange", default)]
1020 pub underlying_listing_exchange: Option<String>,
1021
1022 /// Issuer
1023 #[serde(rename = "@issuer", default)]
1024 pub issuer: Option<String>,
1025
1026 /// Multiplier
1027 #[serde(
1028 rename = "@multiplier",
1029 default,
1030 deserialize_with = "deserialize_optional_decimal"
1031 )]
1032 pub multiplier: Option<Decimal>,
1033
1034 /// Strike (for options)
1035 #[serde(
1036 rename = "@strike",
1037 default,
1038 deserialize_with = "deserialize_optional_decimal"
1039 )]
1040 pub strike: Option<Decimal>,
1041
1042 /// Expiry (for options/futures)
1043 #[serde(
1044 rename = "@expiry",
1045 default,
1046 deserialize_with = "deserialize_optional_date"
1047 )]
1048 pub expiry: Option<NaiveDate>,
1049
1050 /// Put or Call
1051 #[serde(rename = "@putCall", default)]
1052 pub put_call: Option<PutCall>,
1053
1054 /// Principal adjustment factor
1055 #[serde(
1056 rename = "@principalAdjustFactor",
1057 default,
1058 deserialize_with = "deserialize_optional_decimal"
1059 )]
1060 pub principal_adjust_factor: Option<Decimal>,
1061
1062 /// Currency
1063 #[serde(rename = "@currency", default)]
1064 pub currency: Option<String>,
1065}
1066
1067/// Foreign exchange conversion rate
1068///
1069/// Provides daily FX conversion rates for multi-currency accounts.
1070/// Used to convert foreign currency amounts to your base currency.
1071///
1072/// # Example
1073/// ```no_run
1074/// use ib_flex::parse_activity_flex;
1075/// use rust_decimal::Decimal;
1076///
1077/// let xml = std::fs::read_to_string("activity.xml")?;
1078/// let statement = parse_activity_flex(&xml)?;
1079///
1080/// // Find conversion rate for a specific currency pair
1081/// let eur_to_usd = statement.conversion_rates.items
1082/// .iter()
1083/// .find(|r| r.from_currency == "EUR" && r.to_currency == "USD");
1084///
1085/// if let Some(rate) = eur_to_usd {
1086/// println!("EUR/USD rate on {}: {}", rate.report_date, rate.rate);
1087///
1088/// // Convert 1000 EUR to USD
1089/// let eur_amount = Decimal::from(1000);
1090/// let usd_amount = eur_amount * rate.rate;
1091/// println!("1000 EUR = {} USD", usd_amount);
1092/// }
1093///
1094/// // List all available rates
1095/// for rate in &statement.conversion_rates.items {
1096/// println!("{}/{}: {}", rate.from_currency, rate.to_currency, rate.rate);
1097/// }
1098/// # Ok::<(), Box<dyn std::error::Error>>(())
1099/// ```
1100#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1101pub struct ConversionRate {
1102 /// Report date
1103 #[serde(
1104 rename = "@reportDate",
1105 deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
1106 )]
1107 pub report_date: NaiveDate,
1108
1109 /// From currency (source)
1110 #[serde(rename = "@fromCurrency")]
1111 pub from_currency: String,
1112
1113 /// To currency (target)
1114 #[serde(rename = "@toCurrency")]
1115 pub to_currency: String,
1116
1117 /// Exchange rate
1118 #[serde(rename = "@rate")]
1119 pub rate: Decimal,
1120}
1121
1122/// Wrapper for securities info section
1123#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1124pub struct SecuritiesInfoWrapper {
1125 /// List of securities
1126 #[serde(rename = "SecurityInfo", default)]
1127 pub items: Vec<SecurityInfo>,
1128}
1129
1130/// Wrapper for conversion rates section
1131#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1132pub struct ConversionRatesWrapper {
1133 /// List of conversion rates
1134 #[serde(rename = "ConversionRate", default)]
1135 pub items: Vec<ConversionRate>,
1136}
1137
1138// Extended v0.2.0+ wrappers
1139/// Wrapper for change in NAV section
1140#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1141pub struct ChangeInNAVWrapper {
1142 /// List of NAV changes
1143 #[serde(rename = "ChangeInNAV", default)]
1144 pub items: Vec<super::extended::ChangeInNAV>,
1145}
1146
1147/// Wrapper for equity summary section
1148#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1149pub struct EquitySummaryWrapper {
1150 /// List of equity summaries
1151 #[serde(rename = "EquitySummaryByReportDateInBase", default)]
1152 pub items: Vec<super::extended::EquitySummaryByReportDateInBase>,
1153}
1154
1155/// Wrapper for cash report section
1156#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1157pub struct CashReportWrapper {
1158 /// List of cash reports
1159 #[serde(rename = "CashReportCurrency", default)]
1160 pub items: Vec<super::extended::CashReportCurrency>,
1161}
1162
1163/// Wrapper for trade confirmations section
1164#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1165pub struct TradeConfirmsWrapper {
1166 /// List of trade confirmations
1167 #[serde(rename = "TradeConfirm", default)]
1168 pub items: Vec<super::extended::TradeConfirm>,
1169}
1170
1171/// Wrapper for option EAE section
1172#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1173pub struct OptionEAEWrapper {
1174 /// List of option exercises/assignments/expirations
1175 #[serde(rename = "OptionEAE", default)]
1176 pub items: Vec<super::extended::OptionEAE>,
1177}
1178
1179/// Wrapper for FX transactions section
1180#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1181pub struct FxTransactionsWrapper {
1182 /// List of FX transactions
1183 #[serde(rename = "FxTransaction", default)]
1184 pub items: Vec<super::extended::FxTransaction>,
1185}
1186
1187/// Wrapper for change in dividend accruals section
1188#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1189pub struct ChangeInDividendAccrualsWrapper {
1190 /// List of dividend accrual changes
1191 #[serde(rename = "ChangeInDividendAccrual", default)]
1192 pub items: Vec<super::extended::ChangeInDividendAccrual>,
1193}
1194
1195/// Wrapper for open dividend accruals section
1196#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1197pub struct OpenDividendAccrualsWrapper {
1198 /// List of open dividend accruals
1199 #[serde(rename = "OpenDividendAccrual", default)]
1200 pub items: Vec<super::extended::OpenDividendAccrual>,
1201}
1202
1203/// Wrapper for interest accruals section
1204#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1205pub struct InterestAccrualsWrapper {
1206 /// List of interest accruals
1207 #[serde(rename = "InterestAccrualsCurrency", default)]
1208 pub items: Vec<super::extended::InterestAccrualsCurrency>,
1209}
1210
1211/// Wrapper for transfers section
1212#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
1213pub struct TransfersWrapper {
1214 /// List of transfers
1215 #[serde(rename = "Transfer", default)]
1216 pub items: Vec<super::extended::Transfer>,
1217}