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}