Skip to main content

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 - single element (not wrapped like other sections)
171    #[serde(rename = "ChangeInNAV", default)]
172    pub change_in_nav: Option<super::extended::ChangeInNAV>,
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    // v0.3.0+ sections - Performance and advanced features
211    /// MTM performance summary by underlying
212    #[serde(rename = "MTMPerformanceSummaryInBase", default)]
213    pub mtm_performance_summary: MTMPerformanceSummaryWrapper,
214
215    /// FIFO performance summary by underlying
216    #[serde(rename = "FIFOPerformanceSummaryInBase", default)]
217    pub fifo_performance_summary: FIFOPerformanceSummaryWrapper,
218
219    /// MTD/YTD performance summary
220    #[serde(rename = "MTDYTDPerformanceSummary", default)]
221    pub mtd_ytd_performance_summary: MTDYTDPerformanceSummaryWrapper,
222
223    /// Statement of funds (cash flow tracking)
224    #[serde(rename = "StmtFunds", default)]
225    pub statement_of_funds: StatementOfFundsWrapper,
226
227    /// Change in position value (reconciliation)
228    #[serde(rename = "ChangeInPositionValues", default)]
229    pub change_in_position_values: ChangeInPositionValueWrapper,
230
231    /// Unbundled commission details
232    #[serde(rename = "UnbundledCommissionDetails", default)]
233    pub unbundled_commission_details: UnbundledCommissionDetailWrapper,
234
235    /// Client fees (advisory fees)
236    #[serde(rename = "ClientFees", default)]
237    pub client_fees: ClientFeesWrapper,
238
239    /// Client fees detail
240    #[serde(rename = "ClientFeesDetails", default)]
241    pub client_fees_detail: ClientFeesDetailWrapper,
242
243    /// Securities lending activities
244    #[serde(rename = "SLBActivities", default)]
245    pub slb_activities: SLBActivitiesWrapper,
246
247    /// Securities lending fees
248    #[serde(rename = "SLBFees", default)]
249    pub slb_fees: SLBFeesWrapper,
250
251    /// Hard to borrow details
252    #[serde(rename = "HardToBorrowDetails", default)]
253    pub hard_to_borrow_details: HardToBorrowDetailsWrapper,
254
255    /// FX position lots
256    #[serde(rename = "FxLots", default)]
257    pub fx_lots: FxLotsWrapper,
258
259    /// Unsettled transfers
260    #[serde(rename = "UnsettledTransfers", default)]
261    pub unsettled_transfers: UnsettledTransfersWrapper,
262
263    /// Trade transfers (inter-broker)
264    #[serde(rename = "TradeTransfers", default)]
265    pub trade_transfers: TradeTransfersWrapper,
266
267    /// Prior period positions
268    #[serde(rename = "PriorPeriodPositions", default)]
269    pub prior_period_positions: PriorPeriodPositionsWrapper,
270
271    /// Tier interest details
272    #[serde(rename = "TierInterestDetails", default)]
273    pub tier_interest_details: TierInterestDetailsWrapper,
274
275    /// Debit card activities
276    #[serde(rename = "DebitCardActivities", default)]
277    pub debit_card_activities: DebitCardActivitiesWrapper,
278
279    /// Sales tax
280    #[serde(rename = "SalesTaxes", default)]
281    pub sales_tax: SalesTaxWrapper,
282
283    // Note: SymbolSummary and AssetSummary elements appear INSIDE <Trades>,
284    // not as separate sections. They're handled by TradesWrapper.
285    // Orders also appear inside <Trades> as Order elements.
286    // See TradesWrapper for how these are handled.
287
288    // --- Catch-all fields for sections not yet fully implemented ---
289    // These prevent parse errors when XML contains these sections
290    #[serde(rename = "DepositsOnHold", default, skip_serializing)]
291    deposits_on_hold: IgnoredSection,
292    #[serde(rename = "FxPositions", default, skip_serializing)]
293    fx_positions: IgnoredSection,
294    #[serde(rename = "NetStockPositions", default, skip_serializing)]
295    net_stock_positions: IgnoredSection,
296    #[serde(rename = "ComplexPositions", default, skip_serializing)]
297    complex_positions: IgnoredSection,
298    #[serde(rename = "CFDCharges", default, skip_serializing)]
299    cfd_charges: IgnoredSection,
300    #[serde(rename = "CommissionCredits", default, skip_serializing)]
301    commission_credits: IgnoredSection,
302    #[serde(rename = "FdicInsuredDepositsByBank", default, skip_serializing)]
303    fdic_insured_deposits: IgnoredSection,
304    #[serde(rename = "HKIPOOpenSubscriptions", default, skip_serializing)]
305    hk_ipo_open_subscriptions: IgnoredSection,
306    #[serde(rename = "HKIPOSubscriptionActivity", default, skip_serializing)]
307    hk_ipo_subscription_activity: IgnoredSection,
308    #[serde(rename = "IBGNoteTransactions", default, skip_serializing)]
309    ibg_note_transactions: IgnoredSection,
310    #[serde(rename = "IncentiveCouponAccrualDetails", default, skip_serializing)]
311    incentive_coupon_accruals: IgnoredSection,
312    #[serde(rename = "MutualFundDividendDetails", default, skip_serializing)]
313    mutual_fund_dividends: IgnoredSection,
314    #[serde(rename = "NetStockPositionSummary", default, skip_serializing)]
315    net_stock_position_summary: IgnoredSection,
316    #[serde(rename = "PendingExcercises", default, skip_serializing)]
317    pending_exercises: IgnoredSection,
318    #[serde(rename = "RoutingCommissions", default, skip_serializing)]
319    routing_commissions: IgnoredSection,
320    #[serde(rename = "SLBCollaterals", default, skip_serializing)]
321    slb_collaterals: IgnoredSection,
322    #[serde(rename = "SLBOpenContracts", default, skip_serializing)]
323    slb_open_contracts: IgnoredSection,
324    #[serde(rename = "SoftDollars", default, skip_serializing)]
325    soft_dollars: IgnoredSection,
326    #[serde(rename = "StockGrantActivities", default, skip_serializing)]
327    stock_grant_activities: IgnoredSection,
328    #[serde(rename = "TransactionTaxes", default, skip_serializing)]
329    transaction_taxes: IgnoredSection,
330    #[serde(rename = "UnbookedTrades", default, skip_serializing)]
331    unbooked_trades: IgnoredSection,
332    // Note: Catch-all flatten disabled as it causes issues with multi-statement files
333    // All unknown sections should be explicitly listed above with IgnoredSection
334}
335
336/// Helper type for sections we want to ignore during parsing
337#[derive(Debug, Clone, PartialEq, Default)]
338struct IgnoredSection;
339
340impl<'de> serde::Deserialize<'de> for IgnoredSection {
341    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
342    where
343        D: serde::Deserializer<'de>,
344    {
345        // Ignore whatever content is in this section
346        serde::de::IgnoredAny::deserialize(deserializer)?;
347        Ok(IgnoredSection)
348    }
349}
350
351/// Element types that can appear in the `<Trades>` section.
352///
353/// IB FLEX interleaves different element types by symbol, so we parse them all
354/// into an enum and then filter by type for user access.
355#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
356#[serde(rename_all = "PascalCase")]
357enum TradesItem {
358    Trade(Trade),
359    Order(Trade),
360    SymbolSummary(Trade),
361    AssetSummary(Trade),
362    WashSale(Trade),
363    Lot(Trade),
364}
365
366/// Wrapper for trades section
367///
368/// The IB FLEX `<Trades>` section can contain multiple element types based on
369/// the `levelOfDetail` attribute:
370/// - `<Trade>` with levelOfDetail="EXECUTION" - individual trade executions
371/// - `<Order>` with levelOfDetail="ORDER" - order summaries
372/// - `<SymbolSummary>`, `<AssetSummary>`, `<WashSale>`, `<Lot>` - various summary records
373///
374/// These elements can be interleaved (grouped by symbol), not by type.
375#[derive(Debug, Clone, PartialEq, Default, Serialize)]
376pub struct TradesWrapper {
377    /// Trade executions (main trading data)
378    pub items: Vec<Trade>,
379
380    /// Wash sale records
381    pub wash_sales: Vec<Trade>,
382}
383
384impl<'de> serde::Deserialize<'de> for TradesWrapper {
385    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
386    where
387        D: serde::Deserializer<'de>,
388    {
389        #[derive(Deserialize)]
390        struct Raw {
391            #[serde(rename = "$value", default)]
392            items: Vec<TradesItem>,
393        }
394
395        let raw = Raw::deserialize(deserializer)?;
396
397        let mut trades = Vec::new();
398        let mut wash_sales = Vec::new();
399
400        for item in raw.items {
401            match item {
402                TradesItem::Trade(t) => trades.push(t),
403                TradesItem::WashSale(t) => wash_sales.push(t),
404                // Ignore Order, SymbolSummary, AssetSummary, Lot for items
405                _ => {}
406            }
407        }
408
409        Ok(TradesWrapper {
410            items: trades,
411            wash_sales,
412        })
413    }
414}
415
416/// Wrapper for positions section
417#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
418pub struct PositionsWrapper {
419    /// List of positions
420    #[serde(rename = "OpenPosition", default)]
421    pub items: Vec<Position>,
422}
423
424/// Wrapper for cash transactions section
425#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
426pub struct CashTransactionsWrapper {
427    /// List of cash transactions
428    #[serde(rename = "CashTransaction", default)]
429    pub items: Vec<CashTransaction>,
430}
431
432/// Wrapper for corporate actions section
433#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
434pub struct CorporateActionsWrapper {
435    /// List of corporate actions
436    #[serde(rename = "CorporateAction", default)]
437    pub items: Vec<CorporateAction>,
438}
439
440/// A single trade execution
441///
442/// Represents one trade execution from the Activity FLEX statement.
443/// Fields are organized into CORE (essential for tax/portfolio analytics)
444/// and EXTENDED (metadata, execution details) sections.
445///
446/// # Example
447/// ```no_run
448/// use ib_flex::parse_activity_flex;
449/// use ib_flex::{AssetCategory, BuySell};
450///
451/// let xml = std::fs::read_to_string("activity.xml")?;
452/// let statement = parse_activity_flex(&xml)?;
453///
454/// for trade in &statement.trades.items {
455///     // Access basic trade info
456///     println!("Symbol: {}", trade.symbol);
457///     println!("Asset: {:?}", trade.asset_category);
458///
459///     // Check trade direction
460///     match trade.buy_sell {
461///         Some(BuySell::Buy) => println!("Bought"),
462///         Some(BuySell::Sell) => println!("Sold"),
463///         _ => {}
464///     }
465///
466///     // Calculate total cost
467///     let quantity = trade.quantity.unwrap_or_default();
468///     let price = trade.price.unwrap_or_default();
469///     let cost = quantity * price;
470///     println!("Cost: {}", cost);
471///
472///     // Access P&L if available
473///     if let Some(pnl) = trade.fifo_pnl_realized {
474///         println!("Realized P&L: {}", pnl);
475///     }
476///
477///     // Check for options
478///     if trade.asset_category == AssetCategory::Option {
479///         println!("Strike: {:?}", trade.strike);
480///         println!("Expiry: {:?}", trade.expiry);
481///         println!("Put/Call: {:?}", trade.put_call);
482///     }
483/// }
484/// # Ok::<(), Box<dyn std::error::Error>>(())
485/// ```
486#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
487pub struct Trade {
488    // ==================== CORE FIELDS ====================
489    // Essential for tax reporting and portfolio analytics
490
491    // --- Account ---
492    /// IB account number
493    #[serde(rename = "@accountId")]
494    pub account_id: String,
495
496    /// IB transaction ID (unique identifier for idempotency)
497    #[serde(rename = "@transactionID", default)]
498    pub transaction_id: Option<String>,
499
500    // --- Security Identification ---
501    /// IB contract ID (unique per security)
502    #[serde(rename = "@conid")]
503    pub conid: String,
504
505    /// Ticker symbol
506    #[serde(rename = "@symbol")]
507    pub symbol: String,
508
509    /// Security description
510    #[serde(rename = "@description", default)]
511    pub description: Option<String>,
512
513    /// Asset category (stock, option, future, etc.)
514    #[serde(rename = "@assetCategory")]
515    pub asset_category: AssetCategory,
516
517    /// CUSIP
518    #[serde(rename = "@cusip", default)]
519    pub cusip: Option<String>,
520
521    /// ISIN
522    #[serde(rename = "@isin", default)]
523    pub isin: Option<String>,
524
525    /// FIGI
526    #[serde(rename = "@figi", default)]
527    pub figi: Option<String>,
528
529    /// Security ID
530    #[serde(rename = "@securityID", default)]
531    pub security_id: Option<String>,
532
533    /// Security ID type
534    #[serde(rename = "@securityIDType", default)]
535    pub security_id_type: Option<String>,
536
537    // --- Derivatives (Options/Futures) ---
538    /// Contract multiplier (for futures/options)
539    #[serde(
540        rename = "@multiplier",
541        default,
542        deserialize_with = "deserialize_optional_decimal"
543    )]
544    pub multiplier: Option<Decimal>,
545
546    /// Strike price (for options)
547    #[serde(
548        rename = "@strike",
549        default,
550        deserialize_with = "deserialize_optional_decimal"
551    )]
552    pub strike: Option<Decimal>,
553
554    /// Expiry date (for options/futures)
555    #[serde(
556        rename = "@expiry",
557        default,
558        deserialize_with = "deserialize_optional_date"
559    )]
560    pub expiry: Option<NaiveDate>,
561
562    /// Put or Call (for options)
563    #[serde(rename = "@putCall", default)]
564    pub put_call: Option<PutCall>,
565
566    /// Underlying security's contract ID (for derivatives)
567    #[serde(rename = "@underlyingConid", default)]
568    pub underlying_conid: Option<String>,
569
570    /// Underlying symbol
571    #[serde(rename = "@underlyingSymbol", default)]
572    pub underlying_symbol: Option<String>,
573
574    // --- Trade Execution ---
575    /// Trade date (may be empty for summary records)
576    #[serde(
577        rename = "@tradeDate",
578        default,
579        deserialize_with = "deserialize_optional_date"
580    )]
581    pub trade_date: Option<NaiveDate>,
582
583    /// Settlement date (may be empty for summary records)
584    #[serde(
585        rename = "@settleDateTarget",
586        default,
587        deserialize_with = "deserialize_optional_date"
588    )]
589    pub settle_date: Option<NaiveDate>,
590
591    /// Buy or Sell
592    #[serde(rename = "@buySell", default)]
593    pub buy_sell: Option<BuySell>,
594
595    /// Open or Close indicator (for options/futures)
596    #[serde(rename = "@openCloseIndicator", default)]
597    pub open_close: Option<OpenClose>,
598
599    /// Transaction type (ExchTrade, BookTrade, etc.)
600    #[serde(rename = "@transactionType", default)]
601    pub transaction_type: Option<String>,
602
603    // --- Quantities and Prices ---
604    /// Quantity (number of shares/contracts)
605    #[serde(
606        rename = "@quantity",
607        default,
608        deserialize_with = "deserialize_optional_decimal"
609    )]
610    pub quantity: Option<Decimal>,
611
612    /// Trade price per share/contract
613    #[serde(
614        rename = "@price",
615        default,
616        deserialize_with = "deserialize_optional_decimal"
617    )]
618    pub price: Option<Decimal>,
619
620    /// Trade proceeds (negative for buys, positive for sells)
621    #[serde(
622        rename = "@proceeds",
623        default,
624        deserialize_with = "deserialize_optional_decimal"
625    )]
626    pub proceeds: Option<Decimal>,
627
628    /// Cost basis
629    #[serde(
630        rename = "@cost",
631        default,
632        deserialize_with = "deserialize_optional_decimal"
633    )]
634    pub cost: Option<Decimal>,
635
636    // --- Fees and Taxes ---
637    /// Commission paid
638    #[serde(
639        rename = "@ibCommission",
640        default,
641        deserialize_with = "deserialize_optional_decimal"
642    )]
643    pub commission: Option<Decimal>,
644
645    /// Taxes paid
646    #[serde(
647        rename = "@taxes",
648        default,
649        deserialize_with = "deserialize_optional_decimal"
650    )]
651    pub taxes: Option<Decimal>,
652
653    /// Net cash (proceeds + commission + taxes)
654    #[serde(
655        rename = "@netCash",
656        default,
657        deserialize_with = "deserialize_optional_decimal"
658    )]
659    pub net_cash: Option<Decimal>,
660
661    // --- P&L ---
662    /// FIFO realized P&L (for closing trades)
663    #[serde(
664        rename = "@fifoPnlRealized",
665        default,
666        deserialize_with = "deserialize_optional_decimal"
667    )]
668    pub fifo_pnl_realized: Option<Decimal>,
669
670    /// Mark-to-market P&L
671    #[serde(
672        rename = "@mtmPnl",
673        default,
674        deserialize_with = "deserialize_optional_decimal"
675    )]
676    pub mtm_pnl: Option<Decimal>,
677
678    /// FX P&L (for multi-currency)
679    #[serde(
680        rename = "@fxPnl",
681        default,
682        deserialize_with = "deserialize_optional_decimal"
683    )]
684    pub fx_pnl: Option<Decimal>,
685
686    // --- Currency ---
687    /// Trade currency
688    #[serde(rename = "@currency")]
689    pub currency: String,
690
691    /// FX rate to base currency
692    #[serde(
693        rename = "@fxRateToBase",
694        default,
695        deserialize_with = "deserialize_optional_decimal"
696    )]
697    pub fx_rate_to_base: Option<Decimal>,
698
699    // --- Tax Lot Tracking (Critical for tax reporting) ---
700    /// Original trade date (for lot tracking and holding period)
701    #[serde(
702        rename = "@origTradeDate",
703        default,
704        deserialize_with = "deserialize_optional_date"
705    )]
706    pub orig_trade_date: Option<NaiveDate>,
707
708    /// Original trade price (cost basis of the lot)
709    #[serde(
710        rename = "@origTradePrice",
711        default,
712        deserialize_with = "deserialize_optional_decimal"
713    )]
714    pub orig_trade_price: Option<Decimal>,
715
716    /// Original trade ID (links closing trade to opening trade)
717    #[serde(rename = "@origTradeID", default)]
718    pub orig_trade_id: Option<String>,
719
720    /// Holding period date/time (for long-term vs short-term determination)
721    #[serde(rename = "@holdingPeriodDateTime", default)]
722    pub holding_period_date_time: Option<String>,
723
724    /// When position was opened
725    #[serde(rename = "@openDateTime", default)]
726    pub open_date_time: Option<String>,
727
728    /// When position was reopened (for wash sale tracking)
729    #[serde(rename = "@whenReopened", default)]
730    pub when_reopened: Option<String>,
731
732    /// Trade notes/codes (may contain wash sale indicator "W")
733    #[serde(rename = "@notes", default)]
734    pub notes: Option<String>,
735
736    // ==================== EXTENDED FIELDS ====================
737    // Metadata, execution details, and less commonly used fields
738
739    // --- Order/Execution IDs ---
740    /// IB order ID (may be shared across multiple executions)
741    #[serde(rename = "@orderID", default)]
742    pub ib_order_id: Option<String>,
743
744    /// Execution ID
745    #[serde(rename = "@execID", default)]
746    pub exec_id: Option<String>,
747
748    /// Trade ID
749    #[serde(rename = "@tradeID", default)]
750    pub trade_id: Option<String>,
751
752    /// Original transaction ID
753    #[serde(rename = "@origTransactionID", default)]
754    pub orig_transaction_id: Option<String>,
755
756    /// Original order ID
757    #[serde(rename = "@origOrderID", default)]
758    pub orig_order_id: Option<String>,
759
760    // --- Timestamps ---
761    /// Trade time (date + time)
762    #[serde(rename = "@dateTime", default)]
763    pub trade_time: Option<String>,
764
765    /// When P&L was realized
766    #[serde(rename = "@whenRealized", default)]
767    pub when_realized: Option<String>,
768
769    /// Order time
770    #[serde(rename = "@orderTime", default)]
771    pub order_time: Option<String>,
772
773    // --- Order Details ---
774    /// Order type (market, limit, stop, etc.)
775    #[serde(rename = "@orderType", default)]
776    pub order_type: Option<OrderType>,
777
778    /// Brokerage order ID
779    #[serde(rename = "@brokerageOrderID", default)]
780    pub brokerage_order_id: Option<String>,
781
782    /// Order reference
783    #[serde(rename = "@orderReference", default)]
784    pub order_reference: Option<String>,
785
786    /// Exchange order ID
787    #[serde(rename = "@exchOrderId", default)]
788    pub exch_order_id: Option<String>,
789
790    /// External execution ID
791    #[serde(rename = "@extExecID", default)]
792    pub ext_exec_id: Option<String>,
793
794    /// IB execution ID
795    #[serde(rename = "@ibExecID", default)]
796    pub ib_exec_id: Option<String>,
797
798    // --- Issuer/Security Metadata ---
799    /// Issuer
800    #[serde(rename = "@issuer", default)]
801    pub issuer: Option<String>,
802
803    /// Issuer country code
804    #[serde(rename = "@issuerCountryCode", default)]
805    pub issuer_country_code: Option<String>,
806
807    /// Sub-category
808    #[serde(rename = "@subCategory", default)]
809    pub sub_category: Option<String>,
810
811    /// Listing exchange
812    #[serde(rename = "@listingExchange", default)]
813    pub listing_exchange: Option<String>,
814
815    // --- Underlying Extended ---
816    /// Underlying listing exchange
817    #[serde(rename = "@underlyingListingExchange", default)]
818    pub underlying_listing_exchange: Option<String>,
819
820    /// Underlying security ID
821    #[serde(rename = "@underlyingSecurityID", default)]
822    pub underlying_security_id: Option<String>,
823
824    // --- Execution Metadata ---
825    /// Trader ID
826    #[serde(rename = "@traderID", default)]
827    pub trader_id: Option<String>,
828
829    /// Is API order
830    #[serde(rename = "@isAPIOrder", default)]
831    pub is_api_order: Option<String>,
832
833    /// Volatility order link
834    #[serde(rename = "@volatilityOrderLink", default)]
835    pub volatility_order_link: Option<String>,
836
837    /// Clearing firm ID
838    #[serde(rename = "@clearingFirmID", default)]
839    pub clearing_firm_id: Option<String>,
840
841    /// Level of detail (EXECUTION, ORDER, CLOSED_LOT, etc.)
842    #[serde(rename = "@levelOfDetail", default)]
843    pub level_of_detail: Option<String>,
844
845    // --- Price/Quantity Changes ---
846    /// Trade amount
847    #[serde(
848        rename = "@amount",
849        default,
850        deserialize_with = "deserialize_optional_decimal"
851    )]
852    pub amount: Option<Decimal>,
853
854    /// Trade money (quantity * price)
855    #[serde(
856        rename = "@tradeMoney",
857        default,
858        deserialize_with = "deserialize_optional_decimal"
859    )]
860    pub trade_money: Option<Decimal>,
861
862    /// Close price
863    #[serde(
864        rename = "@closePrice",
865        default,
866        deserialize_with = "deserialize_optional_decimal"
867    )]
868    pub close_price: Option<Decimal>,
869
870    /// Change in price
871    #[serde(
872        rename = "@changeInPrice",
873        default,
874        deserialize_with = "deserialize_optional_decimal"
875    )]
876    pub change_in_price: Option<Decimal>,
877
878    /// Change in quantity
879    #[serde(
880        rename = "@changeInQuantity",
881        default,
882        deserialize_with = "deserialize_optional_decimal"
883    )]
884    pub change_in_quantity: Option<Decimal>,
885
886    /// Commission currency
887    #[serde(rename = "@ibCommissionCurrency", default)]
888    pub commission_currency: Option<String>,
889
890    // --- Related Trade Tracking ---
891    /// Related trade ID
892    #[serde(rename = "@relatedTradeID", default)]
893    pub related_trade_id: Option<String>,
894
895    /// Related transaction ID
896    #[serde(rename = "@relatedTransactionID", default)]
897    pub related_transaction_id: Option<String>,
898
899    // --- Bond Fields ---
900    /// Accrued interest
901    #[serde(
902        rename = "@accruedInt",
903        default,
904        deserialize_with = "deserialize_optional_decimal"
905    )]
906    pub accrued_int: Option<Decimal>,
907
908    /// Principal adjust factor
909    #[serde(
910        rename = "@principalAdjustFactor",
911        default,
912        deserialize_with = "deserialize_optional_decimal"
913    )]
914    pub principal_adjust_factor: Option<Decimal>,
915
916    // --- Commodity/Physical Delivery ---
917    /// Serial number (for physical delivery)
918    #[serde(rename = "@serialNumber", default)]
919    pub serial_number: Option<String>,
920
921    /// Delivery type
922    #[serde(rename = "@deliveryType", default)]
923    pub delivery_type: Option<String>,
924
925    /// Commodity type
926    #[serde(rename = "@commodityType", default)]
927    pub commodity_type: Option<String>,
928
929    /// Fineness (for precious metals)
930    #[serde(
931        rename = "@fineness",
932        default,
933        deserialize_with = "deserialize_optional_decimal"
934    )]
935    pub fineness: Option<Decimal>,
936
937    /// Weight
938    #[serde(rename = "@weight", default)]
939    pub weight: Option<String>,
940
941    // --- Other Metadata ---
942    /// Report date
943    #[serde(
944        rename = "@reportDate",
945        default,
946        deserialize_with = "deserialize_optional_date"
947    )]
948    pub report_date: Option<NaiveDate>,
949
950    /// Exchange where trade executed
951    #[serde(rename = "@exchange", default)]
952    pub exchange: Option<String>,
953
954    /// Model (for model portfolios)
955    #[serde(rename = "@model", default)]
956    pub model: Option<String>,
957
958    /// Account alias
959    #[serde(rename = "@acctAlias", default)]
960    pub acct_alias: Option<String>,
961
962    /// RTN
963    #[serde(rename = "@rtn", default)]
964    pub rtn: Option<String>,
965
966    /// Position action ID
967    #[serde(rename = "@positionActionID", default)]
968    pub position_action_id: Option<String>,
969
970    /// Initial investment
971    #[serde(
972        rename = "@initialInvestment",
973        default,
974        deserialize_with = "deserialize_optional_decimal"
975    )]
976    pub initial_investment: Option<Decimal>,
977}
978
979/// An open position snapshot
980///
981/// Represents a single open position at the end of the reporting period.
982/// Fields are organized into CORE (essential for tax/portfolio analytics)
983/// and EXTENDED (metadata) sections.
984///
985/// # Example
986/// ```no_run
987/// use ib_flex::parse_activity_flex;
988/// use rust_decimal::Decimal;
989///
990/// let xml = std::fs::read_to_string("activity.xml")?;
991/// let statement = parse_activity_flex(&xml)?;
992///
993/// for position in &statement.positions.items {
994///     println!("{}: {} shares", position.symbol, position.quantity);
995///     println!("  Current price: {}", position.mark_price);
996///     println!("  Position value: {}", position.position_value);
997///
998///     // Calculate gain/loss percentage
999///     if let Some(cost_basis) = position.cost_basis_money {
1000///         let current_value = position.position_value;
1001///         let gain_pct = ((current_value - cost_basis) / cost_basis) * Decimal::from(100);
1002///         println!("  Gain: {:.2}%", gain_pct);
1003///     }
1004///
1005///     // Show unrealized P&L
1006///     if let Some(pnl) = position.fifo_pnl_unrealized {
1007///         println!("  Unrealized P&L: {}", pnl);
1008///     }
1009///
1010///     // Check if short position
1011///     if position.quantity < Decimal::ZERO {
1012///         println!("  SHORT POSITION");
1013///     }
1014/// }
1015/// # Ok::<(), Box<dyn std::error::Error>>(())
1016/// ```
1017#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1018pub struct Position {
1019    // ==================== CORE FIELDS ====================
1020    // Essential for tax reporting and portfolio analytics
1021
1022    // --- Account ---
1023    /// IB account number
1024    #[serde(rename = "@accountId")]
1025    pub account_id: String,
1026
1027    // --- Security Identification ---
1028    /// IB contract ID
1029    #[serde(rename = "@conid")]
1030    pub conid: String,
1031
1032    /// Ticker symbol
1033    #[serde(rename = "@symbol")]
1034    pub symbol: String,
1035
1036    /// Security description
1037    #[serde(rename = "@description", default)]
1038    pub description: Option<String>,
1039
1040    /// Asset category
1041    #[serde(rename = "@assetCategory")]
1042    pub asset_category: AssetCategory,
1043
1044    /// CUSIP
1045    #[serde(rename = "@cusip", default)]
1046    pub cusip: Option<String>,
1047
1048    /// ISIN
1049    #[serde(rename = "@isin", default)]
1050    pub isin: Option<String>,
1051
1052    /// FIGI
1053    #[serde(rename = "@figi", default)]
1054    pub figi: Option<String>,
1055
1056    /// Security ID
1057    #[serde(rename = "@securityID", default)]
1058    pub security_id: Option<String>,
1059
1060    /// Security ID type
1061    #[serde(rename = "@securityIDType", default)]
1062    pub security_id_type: Option<String>,
1063
1064    // --- Derivatives (Options/Futures) ---
1065    /// Contract multiplier
1066    #[serde(
1067        rename = "@multiplier",
1068        default,
1069        deserialize_with = "deserialize_optional_decimal"
1070    )]
1071    pub multiplier: Option<Decimal>,
1072
1073    /// Strike (for options)
1074    #[serde(
1075        rename = "@strike",
1076        default,
1077        deserialize_with = "deserialize_optional_decimal"
1078    )]
1079    pub strike: Option<Decimal>,
1080
1081    /// Expiry (for options/futures)
1082    #[serde(
1083        rename = "@expiry",
1084        default,
1085        deserialize_with = "deserialize_optional_date"
1086    )]
1087    pub expiry: Option<NaiveDate>,
1088
1089    /// Put or Call
1090    #[serde(rename = "@putCall", default)]
1091    pub put_call: Option<PutCall>,
1092
1093    /// Underlying contract ID
1094    #[serde(rename = "@underlyingConid", default)]
1095    pub underlying_conid: Option<String>,
1096
1097    /// Underlying symbol
1098    #[serde(rename = "@underlyingSymbol", default)]
1099    pub underlying_symbol: Option<String>,
1100
1101    // --- Position and Value ---
1102    /// Position quantity (negative for short)
1103    #[serde(rename = "@position")]
1104    pub quantity: Decimal,
1105
1106    /// Mark price (current market price)
1107    #[serde(rename = "@markPrice")]
1108    pub mark_price: Decimal,
1109
1110    /// Position value (quantity * mark_price * multiplier)
1111    #[serde(rename = "@positionValue")]
1112    pub position_value: Decimal,
1113
1114    /// Side (Long/Short)
1115    #[serde(rename = "@side", default)]
1116    pub side: Option<String>,
1117
1118    // --- Cost Basis and P&L ---
1119    /// Open price
1120    #[serde(
1121        rename = "@openPrice",
1122        default,
1123        deserialize_with = "deserialize_optional_decimal"
1124    )]
1125    pub open_price: Option<Decimal>,
1126
1127    /// Cost basis price per share/contract
1128    #[serde(
1129        rename = "@costBasisPrice",
1130        default,
1131        deserialize_with = "deserialize_optional_decimal"
1132    )]
1133    pub cost_basis_price: Option<Decimal>,
1134
1135    /// Total cost basis
1136    #[serde(
1137        rename = "@costBasisMoney",
1138        default,
1139        deserialize_with = "deserialize_optional_decimal"
1140    )]
1141    pub cost_basis_money: Option<Decimal>,
1142
1143    /// FIFO unrealized P&L
1144    #[serde(
1145        rename = "@fifoPnlUnrealized",
1146        default,
1147        deserialize_with = "deserialize_optional_decimal"
1148    )]
1149    pub fifo_pnl_unrealized: Option<Decimal>,
1150
1151    /// Percent of NAV
1152    #[serde(
1153        rename = "@percentOfNAV",
1154        default,
1155        deserialize_with = "deserialize_optional_decimal"
1156    )]
1157    pub percent_of_nav: Option<Decimal>,
1158
1159    // --- Currency ---
1160    /// Currency
1161    #[serde(rename = "@currency")]
1162    pub currency: String,
1163
1164    /// FX rate to base currency
1165    #[serde(
1166        rename = "@fxRateToBase",
1167        default,
1168        deserialize_with = "deserialize_optional_decimal"
1169    )]
1170    pub fx_rate_to_base: Option<Decimal>,
1171
1172    // --- Dates ---
1173    /// Date of this position snapshot
1174    #[serde(
1175        rename = "@reportDate",
1176        deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
1177    )]
1178    pub report_date: NaiveDate,
1179
1180    // --- Tax Lot Tracking (Critical for tax reporting) ---
1181    /// Holding period date/time (for long-term vs short-term determination)
1182    #[serde(rename = "@holdingPeriodDateTime", default)]
1183    pub holding_period_date_time: Option<String>,
1184
1185    /// When position was opened
1186    #[serde(rename = "@openDateTime", default)]
1187    pub open_date_time: Option<String>,
1188
1189    /// Originating transaction ID
1190    #[serde(rename = "@originatingTransactionID", default)]
1191    pub originating_transaction_id: Option<String>,
1192
1193    /// Position code (may contain tax-related codes)
1194    #[serde(rename = "@code", default)]
1195    pub code: Option<String>,
1196
1197    // ==================== EXTENDED FIELDS ====================
1198    // Metadata and less commonly used fields
1199
1200    // --- Extended IDs ---
1201    /// Originating order ID (links to opening trade)
1202    #[serde(rename = "@originatingOrderID", default)]
1203    pub originating_order_id: Option<String>,
1204
1205    // --- Issuer/Security Metadata ---
1206    /// Issuer
1207    #[serde(rename = "@issuer", default)]
1208    pub issuer: Option<String>,
1209
1210    /// Issuer country code
1211    #[serde(rename = "@issuerCountryCode", default)]
1212    pub issuer_country_code: Option<String>,
1213
1214    /// Sub-category
1215    #[serde(rename = "@subCategory", default)]
1216    pub sub_category: Option<String>,
1217
1218    /// Listing exchange
1219    #[serde(rename = "@listingExchange", default)]
1220    pub listing_exchange: Option<String>,
1221
1222    // --- Underlying Extended ---
1223    /// Underlying listing exchange
1224    #[serde(rename = "@underlyingListingExchange", default)]
1225    pub underlying_listing_exchange: Option<String>,
1226
1227    /// Underlying security ID
1228    #[serde(rename = "@underlyingSecurityID", default)]
1229    pub underlying_security_id: Option<String>,
1230
1231    // --- Bond Fields ---
1232    /// Accrued interest
1233    #[serde(
1234        rename = "@accruedInt",
1235        default,
1236        deserialize_with = "deserialize_optional_decimal"
1237    )]
1238    pub accrued_int: Option<Decimal>,
1239
1240    /// Principal adjust factor
1241    #[serde(
1242        rename = "@principalAdjustFactor",
1243        default,
1244        deserialize_with = "deserialize_optional_decimal"
1245    )]
1246    pub principal_adjust_factor: Option<Decimal>,
1247
1248    // --- Commodity/Physical Delivery ---
1249    /// Serial number (for physical delivery)
1250    #[serde(rename = "@serialNumber", default)]
1251    pub serial_number: Option<String>,
1252
1253    /// Delivery type
1254    #[serde(rename = "@deliveryType", default)]
1255    pub delivery_type: Option<String>,
1256
1257    /// Commodity type
1258    #[serde(rename = "@commodityType", default)]
1259    pub commodity_type: Option<String>,
1260
1261    /// Fineness (for precious metals)
1262    #[serde(
1263        rename = "@fineness",
1264        default,
1265        deserialize_with = "deserialize_optional_decimal"
1266    )]
1267    pub fineness: Option<Decimal>,
1268
1269    /// Weight
1270    #[serde(rename = "@weight", default)]
1271    pub weight: Option<String>,
1272
1273    // --- Other Metadata ---
1274    /// Level of detail
1275    #[serde(rename = "@levelOfDetail", default)]
1276    pub level_of_detail: Option<String>,
1277
1278    /// Model (for model portfolios)
1279    #[serde(rename = "@model", default)]
1280    pub model: Option<String>,
1281
1282    /// Account alias
1283    #[serde(rename = "@acctAlias", default)]
1284    pub acct_alias: Option<String>,
1285
1286    /// Vesting date (for restricted stock)
1287    #[serde(
1288        rename = "@vestingDate",
1289        default,
1290        deserialize_with = "deserialize_optional_date"
1291    )]
1292    pub vesting_date: Option<NaiveDate>,
1293}
1294
1295/// A cash transaction (deposit, withdrawal, dividend, interest, fee)
1296///
1297/// Represents any cash flow that affects your account balance: deposits,
1298/// withdrawals, dividends, interest payments, withholding taxes, and fees.
1299/// Fields are organized into CORE and EXTENDED sections.
1300///
1301/// # Example
1302/// ```no_run
1303/// use ib_flex::parse_activity_flex;
1304/// use rust_decimal::Decimal;
1305///
1306/// let xml = std::fs::read_to_string("activity.xml")?;
1307/// let statement = parse_activity_flex(&xml)?;
1308///
1309/// // Categorize cash flows
1310/// let mut dividends = Decimal::ZERO;
1311/// let mut interest = Decimal::ZERO;
1312/// let mut fees = Decimal::ZERO;
1313///
1314/// for cash_txn in &statement.cash_transactions.items {
1315///     match cash_txn.transaction_type.as_deref() {
1316///         Some("Dividends") => {
1317///             dividends += cash_txn.amount;
1318///             println!("Dividend from {}: {}",
1319///                 cash_txn.symbol.as_ref().unwrap_or(&"N/A".to_string()),
1320///                 cash_txn.amount
1321///             );
1322///         }
1323///         Some("Broker Interest Paid") | Some("Broker Interest Received") => {
1324///             interest += cash_txn.amount;
1325///         }
1326///         Some("Other Fees") | Some("Commission Adjustments") => {
1327///             fees += cash_txn.amount;
1328///         }
1329///         _ => {
1330///             println!("{:?}: {}", cash_txn.transaction_type, cash_txn.amount);
1331///         }
1332///     }
1333/// }
1334///
1335/// println!("\nTotals:");
1336/// println!("  Dividends: {}", dividends);
1337/// println!("  Interest: {}", interest);
1338/// println!("  Fees: {}", fees);
1339/// # Ok::<(), Box<dyn std::error::Error>>(())
1340/// ```
1341#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1342pub struct CashTransaction {
1343    // ==================== CORE FIELDS ====================
1344    // Essential for tax reporting and portfolio analytics
1345
1346    // --- Account ---
1347    /// IB account number
1348    #[serde(rename = "@accountId")]
1349    pub account_id: String,
1350
1351    /// IB transaction ID
1352    #[serde(rename = "@transactionID", default)]
1353    pub transaction_id: Option<String>,
1354
1355    // --- Transaction Details ---
1356    /// Transaction type (Deposits, Dividends, WithholdingTax, BrokerInterest, etc.)
1357    #[serde(rename = "@type", default)]
1358    pub transaction_type: Option<String>,
1359
1360    /// Description of transaction
1361    #[serde(rename = "@description", default)]
1362    pub description: Option<String>,
1363
1364    /// Amount (positive for credits, negative for debits)
1365    #[serde(rename = "@amount")]
1366    pub amount: Decimal,
1367
1368    /// Currency
1369    #[serde(rename = "@currency")]
1370    pub currency: String,
1371
1372    /// FX rate to base currency
1373    #[serde(
1374        rename = "@fxRateToBase",
1375        default,
1376        deserialize_with = "deserialize_optional_decimal"
1377    )]
1378    pub fx_rate_to_base: Option<Decimal>,
1379
1380    // --- Dates ---
1381    /// Transaction date
1382    #[serde(
1383        rename = "@date",
1384        default,
1385        deserialize_with = "deserialize_optional_date"
1386    )]
1387    pub date: Option<NaiveDate>,
1388
1389    /// Settlement date
1390    #[serde(
1391        rename = "@settleDate",
1392        default,
1393        deserialize_with = "deserialize_optional_date"
1394    )]
1395    pub settle_date: Option<NaiveDate>,
1396
1397    /// Ex-dividend date (tax-critical for dividends)
1398    #[serde(
1399        rename = "@exDate",
1400        default,
1401        deserialize_with = "deserialize_optional_date"
1402    )]
1403    pub ex_date: Option<NaiveDate>,
1404
1405    // --- Security Identification ---
1406    /// Related security's contract ID (for dividends)
1407    #[serde(rename = "@conid", default)]
1408    pub conid: Option<String>,
1409
1410    /// Related security's symbol
1411    #[serde(rename = "@symbol", default)]
1412    pub symbol: Option<String>,
1413
1414    /// Asset category
1415    #[serde(rename = "@assetCategory", default)]
1416    pub asset_category: Option<AssetCategory>,
1417
1418    /// CUSIP
1419    #[serde(rename = "@cusip", default)]
1420    pub cusip: Option<String>,
1421
1422    /// ISIN
1423    #[serde(rename = "@isin", default)]
1424    pub isin: Option<String>,
1425
1426    /// FIGI
1427    #[serde(rename = "@figi", default)]
1428    pub figi: Option<String>,
1429
1430    /// Security ID
1431    #[serde(rename = "@securityID", default)]
1432    pub security_id: Option<String>,
1433
1434    /// Security ID type
1435    #[serde(rename = "@securityIDType", default)]
1436    pub security_id_type: Option<String>,
1437
1438    // --- Derivatives ---
1439    /// Contract multiplier
1440    #[serde(
1441        rename = "@multiplier",
1442        default,
1443        deserialize_with = "deserialize_optional_decimal"
1444    )]
1445    pub multiplier: Option<Decimal>,
1446
1447    /// Strike price
1448    #[serde(
1449        rename = "@strike",
1450        default,
1451        deserialize_with = "deserialize_optional_decimal"
1452    )]
1453    pub strike: Option<Decimal>,
1454
1455    /// Expiry date
1456    #[serde(
1457        rename = "@expiry",
1458        default,
1459        deserialize_with = "deserialize_optional_date"
1460    )]
1461    pub expiry: Option<NaiveDate>,
1462
1463    /// Put or Call
1464    #[serde(rename = "@putCall", default)]
1465    pub put_call: Option<PutCall>,
1466
1467    /// Underlying contract ID
1468    #[serde(rename = "@underlyingConid", default)]
1469    pub underlying_conid: Option<String>,
1470
1471    /// Underlying symbol
1472    #[serde(rename = "@underlyingSymbol", default)]
1473    pub underlying_symbol: Option<String>,
1474
1475    /// Transaction code (tax-relevant codes)
1476    #[serde(rename = "@code", default)]
1477    pub code: Option<String>,
1478
1479    // ==================== EXTENDED FIELDS ====================
1480    // Metadata and less commonly used fields
1481
1482    // --- Timestamps ---
1483    /// Transaction datetime
1484    #[serde(rename = "@dateTime", default)]
1485    pub date_time: Option<String>,
1486
1487    /// Report date
1488    #[serde(
1489        rename = "@reportDate",
1490        default,
1491        deserialize_with = "deserialize_optional_date"
1492    )]
1493    pub report_date: Option<NaiveDate>,
1494
1495    /// Available for trading date
1496    #[serde(
1497        rename = "@availableForTradingDate",
1498        default,
1499        deserialize_with = "deserialize_optional_date"
1500    )]
1501    pub available_for_trading_date: Option<NaiveDate>,
1502
1503    // --- Extended IDs ---
1504    /// Action ID
1505    #[serde(rename = "@actionID", default)]
1506    pub action_id: Option<String>,
1507
1508    /// Trade ID (for dividend/interest related to specific trade)
1509    #[serde(rename = "@tradeID", default)]
1510    pub trade_id: Option<String>,
1511
1512    /// Client reference
1513    #[serde(rename = "@clientReference", default)]
1514    pub client_reference: Option<String>,
1515
1516    // --- Issuer/Security Metadata ---
1517    /// Issuer
1518    #[serde(rename = "@issuer", default)]
1519    pub issuer: Option<String>,
1520
1521    /// Issuer country code
1522    #[serde(rename = "@issuerCountryCode", default)]
1523    pub issuer_country_code: Option<String>,
1524
1525    /// Sub-category
1526    #[serde(rename = "@subCategory", default)]
1527    pub sub_category: Option<String>,
1528
1529    /// Listing exchange
1530    #[serde(rename = "@listingExchange", default)]
1531    pub listing_exchange: Option<String>,
1532
1533    // --- Underlying Extended ---
1534    /// Underlying listing exchange
1535    #[serde(rename = "@underlyingListingExchange", default)]
1536    pub underlying_listing_exchange: Option<String>,
1537
1538    /// Underlying security ID
1539    #[serde(rename = "@underlyingSecurityID", default)]
1540    pub underlying_security_id: Option<String>,
1541
1542    // --- Bond Fields ---
1543    /// Principal adjust factor
1544    #[serde(
1545        rename = "@principalAdjustFactor",
1546        default,
1547        deserialize_with = "deserialize_optional_decimal"
1548    )]
1549    pub principal_adjust_factor: Option<Decimal>,
1550
1551    // --- Commodity/Physical Delivery ---
1552    /// Serial number
1553    #[serde(rename = "@serialNumber", default)]
1554    pub serial_number: Option<String>,
1555
1556    /// Delivery type
1557    #[serde(rename = "@deliveryType", default)]
1558    pub delivery_type: Option<String>,
1559
1560    /// Commodity type
1561    #[serde(rename = "@commodityType", default)]
1562    pub commodity_type: Option<String>,
1563
1564    /// Fineness
1565    #[serde(
1566        rename = "@fineness",
1567        default,
1568        deserialize_with = "deserialize_optional_decimal"
1569    )]
1570    pub fineness: Option<Decimal>,
1571
1572    /// Weight
1573    #[serde(rename = "@weight", default)]
1574    pub weight: Option<String>,
1575
1576    // --- Other Metadata ---
1577    /// Level of detail
1578    #[serde(rename = "@levelOfDetail", default)]
1579    pub level_of_detail: Option<String>,
1580
1581    /// Model
1582    #[serde(rename = "@model", default)]
1583    pub model: Option<String>,
1584
1585    /// Account alias
1586    #[serde(rename = "@acctAlias", default)]
1587    pub acct_alias: Option<String>,
1588}
1589
1590/// A corporate action (split, merger, spinoff, etc.)
1591///
1592/// Represents corporate events that affect your holdings: stock splits,
1593/// reverse splits, mergers, spinoffs, tender offers, bond conversions, etc.
1594/// Fields are organized into CORE and EXTENDED sections.
1595///
1596/// # Example
1597/// ```no_run
1598/// use ib_flex::parse_activity_flex;
1599///
1600/// let xml = std::fs::read_to_string("activity.xml")?;
1601/// let statement = parse_activity_flex(&xml)?;
1602///
1603/// for action in &statement.corporate_actions.items {
1604///     println!("{}: {:?}", action.symbol, action.description);
1605///     println!("  Type: {:?}", action.action_type);
1606///
1607///     // Check action type
1608///     match action.action_type.as_deref() {
1609///         Some("FS") => println!("  Forward stock split"),
1610///         Some("RS") => println!("  Reverse stock split"),
1611///         Some("SO") => println!("  Spinoff"),
1612///         Some("TO") => println!("  Tender offer"),
1613///         Some("TC") => println!("  Treasury bill/bond maturity"),
1614///         Some("BC") => println!("  Bond conversion"),
1615///         _ => println!("  Other: {:?}", action.action_type),
1616///     }
1617///
1618///     // Show quantities and proceeds
1619///     if let Some(qty) = action.quantity {
1620///         println!("  Quantity: {}", qty);
1621///     }
1622///     if let Some(proceeds) = action.proceeds {
1623///         println!("  Proceeds: {}", proceeds);
1624///     }
1625///
1626///     // Show realized P&L if applicable
1627///     if let Some(pnl) = action.fifo_pnl_realized {
1628///         println!("  Realized P&L: {}", pnl);
1629///     }
1630/// }
1631/// # Ok::<(), Box<dyn std::error::Error>>(())
1632/// ```
1633#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1634pub struct CorporateAction {
1635    // ==================== CORE FIELDS ====================
1636    // Essential for tax reporting and portfolio analytics
1637
1638    // --- Account ---
1639    /// IB account number
1640    #[serde(rename = "@accountId")]
1641    pub account_id: String,
1642
1643    /// IB transaction ID
1644    #[serde(rename = "@transactionID", default)]
1645    pub transaction_id: Option<String>,
1646
1647    // --- Action Details ---
1648    /// Action type (Split, Merger, Spinoff, etc.)
1649    #[serde(rename = "@type", default)]
1650    pub action_type: Option<String>,
1651
1652    /// Description of corporate action
1653    #[serde(rename = "@description", default)]
1654    pub description: Option<String>,
1655
1656    // --- Dates (Tax-critical) ---
1657    /// Action date
1658    #[serde(
1659        rename = "@date",
1660        default,
1661        deserialize_with = "deserialize_optional_date"
1662    )]
1663    pub action_date: Option<NaiveDate>,
1664
1665    /// Report date
1666    #[serde(
1667        rename = "@reportDate",
1668        deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
1669    )]
1670    pub report_date: NaiveDate,
1671
1672    /// Ex-date (ex-dividend date for dividends)
1673    #[serde(
1674        rename = "@exDate",
1675        default,
1676        deserialize_with = "deserialize_optional_date"
1677    )]
1678    pub ex_date: Option<NaiveDate>,
1679
1680    /// Pay date
1681    #[serde(
1682        rename = "@payDate",
1683        default,
1684        deserialize_with = "deserialize_optional_date"
1685    )]
1686    pub pay_date: Option<NaiveDate>,
1687
1688    /// Record date
1689    #[serde(
1690        rename = "@recordDate",
1691        default,
1692        deserialize_with = "deserialize_optional_date"
1693    )]
1694    pub record_date: Option<NaiveDate>,
1695
1696    // --- Security Identification ---
1697    /// IB contract ID
1698    #[serde(rename = "@conid")]
1699    pub conid: String,
1700
1701    /// Ticker symbol
1702    #[serde(rename = "@symbol")]
1703    pub symbol: String,
1704
1705    /// Asset category
1706    #[serde(rename = "@assetCategory", default)]
1707    pub asset_category: Option<AssetCategory>,
1708
1709    /// CUSIP
1710    #[serde(rename = "@cusip", default)]
1711    pub cusip: Option<String>,
1712
1713    /// ISIN
1714    #[serde(rename = "@isin", default)]
1715    pub isin: Option<String>,
1716
1717    /// FIGI
1718    #[serde(rename = "@figi", default)]
1719    pub figi: Option<String>,
1720
1721    /// Security ID
1722    #[serde(rename = "@securityID", default)]
1723    pub security_id: Option<String>,
1724
1725    /// Security ID type
1726    #[serde(rename = "@securityIDType", default)]
1727    pub security_id_type: Option<String>,
1728
1729    // --- Derivatives ---
1730    /// Contract multiplier
1731    #[serde(
1732        rename = "@multiplier",
1733        default,
1734        deserialize_with = "deserialize_optional_decimal"
1735    )]
1736    pub multiplier: Option<Decimal>,
1737
1738    /// Strike price (for options)
1739    #[serde(
1740        rename = "@strike",
1741        default,
1742        deserialize_with = "deserialize_optional_decimal"
1743    )]
1744    pub strike: Option<Decimal>,
1745
1746    /// Expiry date (for options/futures)
1747    #[serde(
1748        rename = "@expiry",
1749        default,
1750        deserialize_with = "deserialize_optional_date"
1751    )]
1752    pub expiry: Option<NaiveDate>,
1753
1754    /// Put or Call
1755    #[serde(rename = "@putCall", default)]
1756    pub put_call: Option<PutCall>,
1757
1758    /// Underlying contract ID
1759    #[serde(rename = "@underlyingConid", default)]
1760    pub underlying_conid: Option<String>,
1761
1762    /// Underlying symbol
1763    #[serde(rename = "@underlyingSymbol", default)]
1764    pub underlying_symbol: Option<String>,
1765
1766    // --- Quantities and Values ---
1767    /// Quantity affected
1768    #[serde(
1769        rename = "@quantity",
1770        default,
1771        deserialize_with = "deserialize_optional_decimal"
1772    )]
1773    pub quantity: Option<Decimal>,
1774
1775    /// Amount
1776    #[serde(
1777        rename = "@amount",
1778        default,
1779        deserialize_with = "deserialize_optional_decimal"
1780    )]
1781    pub amount: Option<Decimal>,
1782
1783    /// Proceeds (if any)
1784    #[serde(
1785        rename = "@proceeds",
1786        default,
1787        deserialize_with = "deserialize_optional_decimal"
1788    )]
1789    pub proceeds: Option<Decimal>,
1790
1791    /// Value (if any)
1792    #[serde(
1793        rename = "@value",
1794        default,
1795        deserialize_with = "deserialize_optional_decimal"
1796    )]
1797    pub value: Option<Decimal>,
1798
1799    /// Cost
1800    #[serde(
1801        rename = "@cost",
1802        default,
1803        deserialize_with = "deserialize_optional_decimal"
1804    )]
1805    pub cost: Option<Decimal>,
1806
1807    // --- P&L ---
1808    /// FIFO P&L realized
1809    #[serde(
1810        rename = "@fifoPnlRealized",
1811        default,
1812        deserialize_with = "deserialize_optional_decimal"
1813    )]
1814    pub fifo_pnl_realized: Option<Decimal>,
1815
1816    /// Mark-to-market P&L
1817    #[serde(
1818        rename = "@mtmPnl",
1819        default,
1820        deserialize_with = "deserialize_optional_decimal"
1821    )]
1822    pub mtm_pnl: Option<Decimal>,
1823
1824    // --- Currency ---
1825    /// Currency
1826    #[serde(rename = "@currency", default)]
1827    pub currency: Option<String>,
1828
1829    /// FX rate to base
1830    #[serde(
1831        rename = "@fxRateToBase",
1832        default,
1833        deserialize_with = "deserialize_optional_decimal"
1834    )]
1835    pub fx_rate_to_base: Option<Decimal>,
1836
1837    /// Code (may contain tax-relevant info)
1838    #[serde(rename = "@code", default)]
1839    pub code: Option<String>,
1840
1841    // ==================== EXTENDED FIELDS ====================
1842    // Metadata and less commonly used fields
1843
1844    // --- Extended IDs ---
1845    /// Action ID
1846    #[serde(rename = "@actionID", default)]
1847    pub action_id: Option<String>,
1848
1849    // --- Timestamps ---
1850    /// Action datetime
1851    #[serde(rename = "@dateTime", default)]
1852    pub date_time: Option<String>,
1853
1854    // --- Issuer/Security Metadata ---
1855    /// Issuer
1856    #[serde(rename = "@issuer", default)]
1857    pub issuer: Option<String>,
1858
1859    /// Issuer country code
1860    #[serde(rename = "@issuerCountryCode", default)]
1861    pub issuer_country_code: Option<String>,
1862
1863    /// Sub-category
1864    #[serde(rename = "@subCategory", default)]
1865    pub sub_category: Option<String>,
1866
1867    /// Listing exchange
1868    #[serde(rename = "@listingExchange", default)]
1869    pub listing_exchange: Option<String>,
1870
1871    // --- Underlying Extended ---
1872    /// Underlying listing exchange
1873    #[serde(rename = "@underlyingListingExchange", default)]
1874    pub underlying_listing_exchange: Option<String>,
1875
1876    /// Underlying security ID
1877    #[serde(rename = "@underlyingSecurityID", default)]
1878    pub underlying_security_id: Option<String>,
1879
1880    // --- Bond Fields ---
1881    /// Accrued interest
1882    #[serde(
1883        rename = "@accruedInt",
1884        default,
1885        deserialize_with = "deserialize_optional_decimal"
1886    )]
1887    pub accrued_int: Option<Decimal>,
1888
1889    /// Principal adjust factor
1890    #[serde(
1891        rename = "@principalAdjustFactor",
1892        default,
1893        deserialize_with = "deserialize_optional_decimal"
1894    )]
1895    pub principal_adjust_factor: Option<Decimal>,
1896
1897    // --- Commodity/Physical Delivery ---
1898    /// Serial number
1899    #[serde(rename = "@serialNumber", default)]
1900    pub serial_number: Option<String>,
1901
1902    /// Delivery type
1903    #[serde(rename = "@deliveryType", default)]
1904    pub delivery_type: Option<String>,
1905
1906    /// Commodity type
1907    #[serde(rename = "@commodityType", default)]
1908    pub commodity_type: Option<String>,
1909
1910    /// Fineness (for precious metals)
1911    #[serde(
1912        rename = "@fineness",
1913        default,
1914        deserialize_with = "deserialize_optional_decimal"
1915    )]
1916    pub fineness: Option<Decimal>,
1917
1918    /// Weight
1919    #[serde(rename = "@weight", default)]
1920    pub weight: Option<String>,
1921
1922    // --- Other Metadata ---
1923    /// Level of detail
1924    #[serde(rename = "@levelOfDetail", default)]
1925    pub level_of_detail: Option<String>,
1926
1927    /// Model (for model portfolios)
1928    #[serde(rename = "@model", default)]
1929    pub model: Option<String>,
1930
1931    /// Account alias
1932    #[serde(rename = "@acctAlias", default)]
1933    pub acct_alias: Option<String>,
1934}
1935
1936/// Security information (reference data)
1937///
1938/// Provides detailed reference data for securities in the statement.
1939/// Includes identifiers (CUSIP, ISIN, FIGI), exchange info, and derivative details.
1940/// Fields are organized into CORE and EXTENDED sections.
1941///
1942/// # Example
1943/// ```no_run
1944/// use ib_flex::parse_activity_flex;
1945/// use ib_flex::AssetCategory;
1946///
1947/// let xml = std::fs::read_to_string("activity.xml")?;
1948/// let statement = parse_activity_flex(&xml)?;
1949///
1950/// for security in &statement.securities_info.items {
1951///     println!("{} ({})", security.symbol, security.conid);
1952///
1953///     // Print description
1954///     if let Some(desc) = &security.description {
1955///         println!("  Description: {}", desc);
1956///     }
1957///
1958///     // Print identifiers
1959///     if let Some(cusip) = &security.cusip {
1960///         println!("  CUSIP: {}", cusip);
1961///     }
1962///     if let Some(isin) = &security.isin {
1963///         println!("  ISIN: {}", isin);
1964///     }
1965///
1966///     // Show derivative info for options
1967///     if security.asset_category == AssetCategory::Option {
1968///         println!("  Underlying: {:?}", security.underlying_symbol);
1969///         println!("  Strike: {:?}", security.strike);
1970///         println!("  Expiry: {:?}", security.expiry);
1971///         println!("  Type: {:?}", security.put_call);
1972///         println!("  Multiplier: {:?}", security.multiplier);
1973///     }
1974/// }
1975/// # Ok::<(), Box<dyn std::error::Error>>(())
1976/// ```
1977#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1978pub struct SecurityInfo {
1979    // ==================== CORE FIELDS ====================
1980    // Essential for tax reporting and portfolio analytics
1981
1982    // --- Security Identification ---
1983    /// Asset category
1984    #[serde(rename = "@assetCategory")]
1985    pub asset_category: AssetCategory,
1986
1987    /// Ticker symbol
1988    #[serde(rename = "@symbol")]
1989    pub symbol: String,
1990
1991    /// Security description
1992    #[serde(rename = "@description", default)]
1993    pub description: Option<String>,
1994
1995    /// IB contract ID
1996    #[serde(rename = "@conid")]
1997    pub conid: String,
1998
1999    /// Security ID
2000    #[serde(rename = "@securityID", default)]
2001    pub security_id: Option<String>,
2002
2003    /// Security ID type
2004    #[serde(rename = "@securityIDType", default)]
2005    pub security_id_type: Option<String>,
2006
2007    /// CUSIP
2008    #[serde(rename = "@cusip", default)]
2009    pub cusip: Option<String>,
2010
2011    /// ISIN
2012    #[serde(rename = "@isin", default)]
2013    pub isin: Option<String>,
2014
2015    /// FIGI
2016    #[serde(rename = "@figi", default)]
2017    pub figi: Option<String>,
2018
2019    /// SEDOL
2020    #[serde(rename = "@sedol", default)]
2021    pub sedol: Option<String>,
2022
2023    // --- Derivatives (Options/Futures) ---
2024    /// Multiplier
2025    #[serde(
2026        rename = "@multiplier",
2027        default,
2028        deserialize_with = "deserialize_optional_decimal"
2029    )]
2030    pub multiplier: Option<Decimal>,
2031
2032    /// Strike (for options)
2033    #[serde(
2034        rename = "@strike",
2035        default,
2036        deserialize_with = "deserialize_optional_decimal"
2037    )]
2038    pub strike: Option<Decimal>,
2039
2040    /// Expiry (for options/futures)
2041    #[serde(
2042        rename = "@expiry",
2043        default,
2044        deserialize_with = "deserialize_optional_date"
2045    )]
2046    pub expiry: Option<NaiveDate>,
2047
2048    /// Put or Call
2049    #[serde(rename = "@putCall", default)]
2050    pub put_call: Option<PutCall>,
2051
2052    /// Underlying contract ID
2053    #[serde(rename = "@underlyingConid", default)]
2054    pub underlying_conid: Option<String>,
2055
2056    /// Underlying symbol
2057    #[serde(rename = "@underlyingSymbol", default)]
2058    pub underlying_symbol: Option<String>,
2059
2060    // --- Bond/Fixed Income ---
2061    /// Maturity date (for bonds)
2062    #[serde(
2063        rename = "@maturity",
2064        default,
2065        deserialize_with = "deserialize_optional_date"
2066    )]
2067    pub maturity: Option<NaiveDate>,
2068
2069    /// Principal adjustment factor
2070    #[serde(
2071        rename = "@principalAdjustFactor",
2072        default,
2073        deserialize_with = "deserialize_optional_decimal"
2074    )]
2075    pub principal_adjust_factor: Option<Decimal>,
2076
2077    // --- Currency ---
2078    /// Currency
2079    #[serde(rename = "@currency", default)]
2080    pub currency: Option<String>,
2081
2082    // ==================== EXTENDED FIELDS ====================
2083    // Metadata and less commonly used fields
2084
2085    // --- Exchange Info ---
2086    /// Listing exchange
2087    #[serde(rename = "@listingExchange", default)]
2088    pub listing_exchange: Option<String>,
2089
2090    /// Underlying security ID
2091    #[serde(rename = "@underlyingSecurityID", default)]
2092    pub underlying_security_id: Option<String>,
2093
2094    /// Underlying listing exchange
2095    #[serde(rename = "@underlyingListingExchange", default)]
2096    pub underlying_listing_exchange: Option<String>,
2097
2098    // --- Issuer/Security Metadata ---
2099    /// Issuer
2100    #[serde(rename = "@issuer", default)]
2101    pub issuer: Option<String>,
2102
2103    /// Issuer country code
2104    #[serde(rename = "@issuerCountryCode", default)]
2105    pub issuer_country_code: Option<String>,
2106
2107    /// Sub-category
2108    #[serde(rename = "@subCategory", default)]
2109    pub sub_category: Option<String>,
2110
2111    // --- Futures ---
2112    /// Delivery month (for futures)
2113    #[serde(rename = "@deliveryMonth", default)]
2114    pub delivery_month: Option<String>,
2115
2116    // --- Commodity/Physical Delivery ---
2117    /// Serial number
2118    #[serde(rename = "@serialNumber", default)]
2119    pub serial_number: Option<String>,
2120
2121    /// Delivery type
2122    #[serde(rename = "@deliveryType", default)]
2123    pub delivery_type: Option<String>,
2124
2125    /// Commodity type
2126    #[serde(rename = "@commodityType", default)]
2127    pub commodity_type: Option<String>,
2128
2129    /// Fineness (for precious metals)
2130    #[serde(
2131        rename = "@fineness",
2132        default,
2133        deserialize_with = "deserialize_optional_decimal"
2134    )]
2135    pub fineness: Option<Decimal>,
2136
2137    /// Weight
2138    #[serde(rename = "@weight", default)]
2139    pub weight: Option<String>,
2140
2141    // --- Other ---
2142    /// Code
2143    #[serde(rename = "@code", default)]
2144    pub code: Option<String>,
2145}
2146
2147/// Foreign exchange conversion rate
2148///
2149/// Provides daily FX conversion rates for multi-currency accounts.
2150/// Used to convert foreign currency amounts to your base currency.
2151///
2152/// # Example
2153/// ```no_run
2154/// use ib_flex::parse_activity_flex;
2155/// use rust_decimal::Decimal;
2156///
2157/// let xml = std::fs::read_to_string("activity.xml")?;
2158/// let statement = parse_activity_flex(&xml)?;
2159///
2160/// // Find conversion rate for a specific currency pair
2161/// let eur_to_usd = statement.conversion_rates.items
2162///     .iter()
2163///     .find(|r| r.from_currency == "EUR" && r.to_currency == "USD");
2164///
2165/// if let Some(rate) = eur_to_usd {
2166///     println!("EUR/USD rate on {}: {}", rate.report_date, rate.rate);
2167///
2168///     // Convert 1000 EUR to USD
2169///     let eur_amount = Decimal::from(1000);
2170///     let usd_amount = eur_amount * rate.rate;
2171///     println!("1000 EUR = {} USD", usd_amount);
2172/// }
2173///
2174/// // List all available rates
2175/// for rate in &statement.conversion_rates.items {
2176///     println!("{}/{}: {}", rate.from_currency, rate.to_currency, rate.rate);
2177/// }
2178/// # Ok::<(), Box<dyn std::error::Error>>(())
2179/// ```
2180#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
2181pub struct ConversionRate {
2182    /// Report date
2183    #[serde(
2184        rename = "@reportDate",
2185        deserialize_with = "crate::parsers::xml_utils::deserialize_flex_date"
2186    )]
2187    pub report_date: NaiveDate,
2188
2189    /// From currency (source)
2190    #[serde(rename = "@fromCurrency")]
2191    pub from_currency: String,
2192
2193    /// To currency (target)
2194    #[serde(rename = "@toCurrency")]
2195    pub to_currency: String,
2196
2197    /// Exchange rate
2198    #[serde(rename = "@rate")]
2199    pub rate: Decimal,
2200}
2201
2202/// Wrapper for securities info section
2203#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2204pub struct SecuritiesInfoWrapper {
2205    /// List of securities
2206    #[serde(rename = "SecurityInfo", default)]
2207    pub items: Vec<SecurityInfo>,
2208}
2209
2210/// Wrapper for conversion rates section
2211#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2212pub struct ConversionRatesWrapper {
2213    /// List of conversion rates
2214    #[serde(rename = "ConversionRate", default)]
2215    pub items: Vec<ConversionRate>,
2216}
2217
2218// Extended v0.2.0+ wrappers
2219
2220/// Wrapper for equity summary section
2221#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2222pub struct EquitySummaryWrapper {
2223    /// List of equity summaries
2224    #[serde(rename = "EquitySummaryByReportDateInBase", default)]
2225    pub items: Vec<super::extended::EquitySummaryByReportDateInBase>,
2226}
2227
2228/// Wrapper for cash report section
2229#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2230pub struct CashReportWrapper {
2231    /// List of cash reports
2232    #[serde(rename = "CashReportCurrency", default)]
2233    pub items: Vec<super::extended::CashReportCurrency>,
2234}
2235
2236/// Wrapper for trade confirmations section
2237#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2238pub struct TradeConfirmsWrapper {
2239    /// List of trade confirmations
2240    #[serde(rename = "TradeConfirm", default)]
2241    pub items: Vec<super::extended::TradeConfirm>,
2242}
2243
2244/// Wrapper for option EAE section
2245#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2246pub struct OptionEAEWrapper {
2247    /// List of option exercises/assignments/expirations
2248    #[serde(rename = "OptionEAE", default)]
2249    pub items: Vec<super::extended::OptionEAE>,
2250}
2251
2252/// Wrapper for FX transactions section
2253#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2254pub struct FxTransactionsWrapper {
2255    /// List of FX transactions
2256    #[serde(rename = "FxTransaction", default)]
2257    pub items: Vec<super::extended::FxTransaction>,
2258}
2259
2260/// Wrapper for change in dividend accruals section
2261#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2262pub struct ChangeInDividendAccrualsWrapper {
2263    /// List of dividend accrual changes
2264    #[serde(rename = "ChangeInDividendAccrual", default)]
2265    pub items: Vec<super::extended::ChangeInDividendAccrual>,
2266}
2267
2268/// Wrapper for open dividend accruals section
2269#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2270pub struct OpenDividendAccrualsWrapper {
2271    /// List of open dividend accruals
2272    #[serde(rename = "OpenDividendAccrual", default)]
2273    pub items: Vec<super::extended::OpenDividendAccrual>,
2274}
2275
2276/// Wrapper for interest accruals section
2277#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2278pub struct InterestAccrualsWrapper {
2279    /// List of interest accruals
2280    #[serde(rename = "InterestAccrualsCurrency", default)]
2281    pub items: Vec<super::extended::InterestAccrualsCurrency>,
2282}
2283
2284/// Wrapper for transfers section
2285#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2286pub struct TransfersWrapper {
2287    /// List of transfers
2288    #[serde(rename = "Transfer", default)]
2289    pub items: Vec<super::extended::Transfer>,
2290}
2291
2292// v0.3.0+ wrappers for performance and advanced features
2293
2294/// Wrapper for MTM performance summary section
2295#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2296pub struct MTMPerformanceSummaryWrapper {
2297    /// List of MTM performance summaries by underlying
2298    #[serde(rename = "MTMPerformanceSummaryUnderlying", default)]
2299    pub items: Vec<super::extended::MTMPerformanceSummaryUnderlying>,
2300}
2301
2302/// Wrapper for FIFO performance summary section
2303#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2304pub struct FIFOPerformanceSummaryWrapper {
2305    /// List of FIFO performance summaries by underlying
2306    #[serde(rename = "FIFOPerformanceSummaryUnderlying", default)]
2307    pub items: Vec<super::extended::FIFOPerformanceSummaryUnderlying>,
2308}
2309
2310/// Wrapper for MTD/YTD performance summary section
2311#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2312pub struct MTDYTDPerformanceSummaryWrapper {
2313    /// List of MTD/YTD performance summaries
2314    #[serde(rename = "MTDYTDPerformanceSummaryUnderlying", default)]
2315    pub items: Vec<super::extended::MTDYTDPerformanceSummary>,
2316}
2317
2318/// Wrapper for statement of funds section
2319#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2320pub struct StatementOfFundsWrapper {
2321    /// List of statement of funds lines
2322    #[serde(rename = "StatementOfFundsLine", default)]
2323    pub items: Vec<super::extended::StatementOfFundsLine>,
2324}
2325
2326/// Wrapper for change in position value section
2327#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2328pub struct ChangeInPositionValueWrapper {
2329    /// List of position value changes
2330    #[serde(rename = "ChangeInPositionValue", default)]
2331    pub items: Vec<super::extended::ChangeInPositionValue>,
2332}
2333
2334/// Wrapper for unbundled commission details section
2335#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2336pub struct UnbundledCommissionDetailWrapper {
2337    /// List of unbundled commission details
2338    #[serde(rename = "UnbundledCommissionDetail", default)]
2339    pub items: Vec<super::extended::UnbundledCommissionDetail>,
2340}
2341
2342/// Wrapper for client fees section
2343#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2344pub struct ClientFeesWrapper {
2345    /// List of client fees
2346    #[serde(rename = "ClientFee", default)]
2347    pub items: Vec<super::extended::ClientFee>,
2348}
2349
2350/// Wrapper for client fees detail section
2351#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2352pub struct ClientFeesDetailWrapper {
2353    /// List of client fee details
2354    #[serde(rename = "ClientFeesDetail", default)]
2355    pub items: Vec<super::extended::ClientFeesDetail>,
2356}
2357
2358/// Wrapper for SLB activities section
2359#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2360pub struct SLBActivitiesWrapper {
2361    /// List of SLB activities
2362    #[serde(rename = "SLBActivity", default)]
2363    pub items: Vec<super::extended::SLBActivity>,
2364}
2365
2366/// Wrapper for SLB fees section
2367#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2368pub struct SLBFeesWrapper {
2369    /// List of SLB fees
2370    #[serde(rename = "SLBFee", default)]
2371    pub items: Vec<super::extended::SLBFee>,
2372}
2373
2374/// Wrapper for hard to borrow details section
2375#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2376pub struct HardToBorrowDetailsWrapper {
2377    /// List of hard to borrow details
2378    #[serde(rename = "HardToBorrowDetail", default)]
2379    pub items: Vec<super::extended::HardToBorrowDetail>,
2380}
2381
2382/// Wrapper for FX lots section
2383#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2384pub struct FxLotsWrapper {
2385    /// List of FX lots
2386    #[serde(rename = "FxLot", default)]
2387    pub items: Vec<super::extended::FxLot>,
2388}
2389
2390/// Wrapper for unsettled transfers section
2391#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2392pub struct UnsettledTransfersWrapper {
2393    /// List of unsettled transfers
2394    #[serde(rename = "UnsettledTransfer", default)]
2395    pub items: Vec<super::extended::UnsettledTransfer>,
2396}
2397
2398/// Wrapper for trade transfers section
2399#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2400pub struct TradeTransfersWrapper {
2401    /// List of trade transfers
2402    #[serde(rename = "TradeTransfer", default)]
2403    pub items: Vec<super::extended::TradeTransfer>,
2404}
2405
2406/// Wrapper for prior period positions section
2407#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2408pub struct PriorPeriodPositionsWrapper {
2409    /// List of prior period positions
2410    #[serde(rename = "PriorPeriodPosition", default)]
2411    pub items: Vec<super::extended::PriorPeriodPosition>,
2412}
2413
2414/// Wrapper for tier interest details section
2415#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2416pub struct TierInterestDetailsWrapper {
2417    /// List of tier interest details
2418    #[serde(rename = "TierInterestDetail", default)]
2419    pub items: Vec<super::extended::TierInterestDetail>,
2420}
2421
2422/// Wrapper for debit card activities section
2423#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2424pub struct DebitCardActivitiesWrapper {
2425    /// List of debit card activities
2426    #[serde(rename = "DebitCardActivity", default)]
2427    pub items: Vec<super::extended::DebitCardActivity>,
2428}
2429
2430/// Wrapper for sales tax section
2431#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2432pub struct SalesTaxWrapper {
2433    /// List of sales tax entries
2434    #[serde(rename = "SalesTax", default)]
2435    pub items: Vec<super::extended::SalesTax>,
2436}
2437
2438/// Wrapper for symbol summary section
2439#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2440pub struct SymbolSummaryWrapper {
2441    /// List of symbol summaries
2442    #[serde(rename = "SymbolSummary", default)]
2443    pub items: Vec<super::extended::SymbolSummary>,
2444}
2445
2446/// Wrapper for asset summary section
2447#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2448pub struct AssetSummaryWrapper {
2449    /// List of asset summaries
2450    #[serde(rename = "AssetSummary", default)]
2451    pub items: Vec<super::extended::AssetSummary>,
2452}
2453
2454/// Wrapper for orders section
2455#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2456pub struct OrdersWrapper {
2457    /// List of orders
2458    #[serde(rename = "Order", default)]
2459    pub items: Vec<super::extended::Order>,
2460}