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::{
8    AssetCategory, BuySell, DerivativeInfo, LevelOfDetail, OpenClose, OrderType, PutCall,
9    SecurityIdType, SubCategory, TradeType,
10};
11use crate::parsers::xml_utils::{
12    deserialize_optional_bool, deserialize_optional_date, deserialize_optional_decimal,
13    deserialize_optional_string,
14};
15
16/// Top-level FLEX query response
17///
18/// This is the root XML element in IB FLEX files. It wraps one or more
19/// [`ActivityFlexStatement`]s along with query metadata.
20///
21/// **Note**: When using [`crate::parse_activity_flex`], this wrapper is handled
22/// automatically and you receive the [`ActivityFlexStatement`] directly.
23///
24/// # Example
25/// ```
26/// use ib_flex::types::FlexQueryResponse;
27/// use quick_xml::de::from_str;
28///
29/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
30/// <FlexQueryResponse queryName="Activity" type="AF">
31///   <FlexStatements count="1">
32///     <FlexStatement accountId="U1234567" fromDate="2025-01-01"
33///                    toDate="2025-01-31" whenGenerated="2025-01-31;150000">
34///       <Trades />
35///       <OpenPositions />
36///       <CashTransactions />
37///       <CorporateActions />
38///       <SecuritiesInfo />
39///       <ConversionRates />
40///     </FlexStatement>
41///   </FlexStatements>
42/// </FlexQueryResponse>"#;
43///
44/// let response: FlexQueryResponse = from_str(xml).unwrap();
45/// assert_eq!(response.query_name, Some("Activity".to_string()));
46/// assert_eq!(response.statements.statements.len(), 1);
47/// # Ok::<(), Box<dyn std::error::Error>>(())
48/// ```
49#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
50#[serde(rename = "FlexQueryResponse")]
51pub struct FlexQueryResponse {
52    /// Query name
53    #[serde(rename = "@queryName", default)]
54    pub query_name: Option<String>,
55
56    /// Query type
57    #[serde(rename = "@type", default)]
58    pub query_type: Option<String>,
59
60    /// FlexStatements wrapper
61    #[serde(rename = "FlexStatements")]
62    pub statements: FlexStatementsWrapper,
63}
64
65/// Wrapper for FlexStatements
66#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
67pub struct FlexStatementsWrapper {
68    /// Count
69    #[serde(rename = "@count", default)]
70    pub count: Option<String>,
71
72    /// Flex statement(s)
73    #[serde(rename = "FlexStatement")]
74    pub statements: Vec<ActivityFlexStatement>,
75}
76
77/// Top-level Activity FLEX statement
78///
79/// Contains all data from an Activity FLEX query including trades,
80/// positions, cash transactions, and other portfolio data.
81///
82/// This is the main type returned by [`crate::parse_activity_flex`].
83///
84/// # Example
85/// ```no_run
86/// use ib_flex::parse_activity_flex;
87/// use rust_decimal::Decimal;
88///
89/// let xml = std::fs::read_to_string("activity.xml")?;
90/// let statement = parse_activity_flex(&xml)?;
91///
92/// // Access account and date range
93/// println!("Account: {:?}", statement.account_id);
94/// println!("Period: {:?} to {:?}", statement.from_date, statement.to_date);
95///
96/// // Iterate through all trades
97/// for trade in &statement.trades.items {
98///     println!("{}: {} {} @ {}",
99///         trade.symbol.as_deref().unwrap_or(""),
100///         trade.buy_sell.as_ref().map(|b| format!("{:?}", b)).unwrap_or_default(),
101///         trade.quantity.unwrap_or_default(),
102///         trade.price.unwrap_or_default()
103///     );
104/// }
105///
106/// // Calculate total P&L
107/// let total_pnl: Decimal = statement.trades.items.iter()
108///     .filter_map(|t| t.fifo_pnl_realized)
109///     .sum();
110/// println!("Total realized P&L: {}", total_pnl);
111///
112/// // Access positions
113/// for pos in &statement.positions.items {
114///     println!("{}: {} shares @ {}",
115///         pos.symbol.as_deref().unwrap_or(""),
116///         pos.quantity.unwrap_or_default(),
117///         pos.mark_price.unwrap_or_default()
118///     );
119/// }
120/// # Ok::<(), Box<dyn std::error::Error>>(())
121/// ```
122#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
123#[serde(rename = "FlexStatement")]
124pub struct ActivityFlexStatement {
125    /// IB account number
126    #[serde(
127        rename = "@accountId",
128        default,
129        deserialize_with = "deserialize_optional_string"
130    )]
131    pub account_id: Option<String>,
132
133    /// Statement date range - start date
134    #[serde(
135        rename = "@fromDate",
136        default,
137        deserialize_with = "deserialize_optional_date"
138    )]
139    pub from_date: Option<NaiveDate>,
140
141    /// Statement date range - end date
142    #[serde(
143        rename = "@toDate",
144        default,
145        deserialize_with = "deserialize_optional_date"
146    )]
147    pub to_date: Option<NaiveDate>,
148
149    /// When the report was generated
150    #[serde(
151        rename = "@whenGenerated",
152        default,
153        deserialize_with = "deserialize_optional_string"
154    )]
155    pub when_generated: Option<String>, // Parse separately due to IB format
156
157    /// All trades in the period
158    #[serde(rename = "Trades", default)]
159    pub trades: TradesWrapper,
160
161    /// Open positions at end of period
162    #[serde(rename = "OpenPositions", default)]
163    pub positions: PositionsWrapper,
164
165    /// Cash transactions (deposits, withdrawals, dividends, interest)
166    #[serde(rename = "CashTransactions", default)]
167    pub cash_transactions: CashTransactionsWrapper,
168
169    /// Corporate actions (splits, mergers, spinoffs)
170    #[serde(rename = "CorporateActions", default)]
171    pub corporate_actions: CorporateActionsWrapper,
172
173    /// Securities information (reference data)
174    #[serde(rename = "SecuritiesInfo", default)]
175    pub securities_info: SecuritiesInfoWrapper,
176
177    /// Currency conversion rates
178    #[serde(rename = "ConversionRates", default)]
179    pub conversion_rates: ConversionRatesWrapper,
180
181    // Extended v0.2.0+ sections
182    /// Account information
183    #[serde(rename = "AccountInformation", default)]
184    pub account_information: Option<super::extended::AccountInformation>,
185
186    /// Change in NAV - single element (not wrapped like other sections)
187    #[serde(rename = "ChangeInNAV", default)]
188    pub change_in_nav: Option<super::extended::ChangeInNAV>,
189
190    /// Equity summary by report date in base currency
191    #[serde(rename = "EquitySummaryInBase", default)]
192    pub equity_summary: EquitySummaryWrapper,
193
194    /// Cash report by currency
195    #[serde(rename = "CashReport", default)]
196    pub cash_report: CashReportWrapper,
197
198    /// Trade confirmations
199    #[serde(rename = "TradeConfirms", default)]
200    pub trade_confirms: TradeConfirmsWrapper,
201
202    /// Option exercises, assignments, and expirations
203    #[serde(rename = "OptionEAE", default)]
204    pub option_eae: OptionEAEWrapper,
205
206    /// Foreign exchange transactions
207    #[serde(rename = "FxTransactions", default)]
208    pub fx_transactions: FxTransactionsWrapper,
209
210    /// Change in dividend accruals
211    #[serde(rename = "ChangeInDividendAccruals", default)]
212    pub change_in_dividend_accruals: ChangeInDividendAccrualsWrapper,
213
214    /// Open dividend accruals
215    #[serde(rename = "OpenDividendAccruals", default)]
216    pub open_dividend_accruals: OpenDividendAccrualsWrapper,
217
218    /// Interest accruals by currency
219    #[serde(rename = "InterestAccruals", default)]
220    pub interest_accruals: InterestAccrualsWrapper,
221
222    /// Security transfers
223    #[serde(rename = "Transfers", default)]
224    pub transfers: TransfersWrapper,
225
226    // v0.3.0+ sections - Performance and advanced features
227    /// MTM performance summary by underlying
228    #[serde(rename = "MTMPerformanceSummaryInBase", default)]
229    pub mtm_performance_summary: MTMPerformanceSummaryWrapper,
230
231    /// FIFO performance summary by underlying
232    #[serde(rename = "FIFOPerformanceSummaryInBase", default)]
233    pub fifo_performance_summary: FIFOPerformanceSummaryWrapper,
234
235    /// MTD/YTD performance summary
236    #[serde(rename = "MTDYTDPerformanceSummary", default)]
237    pub mtd_ytd_performance_summary: MTDYTDPerformanceSummaryWrapper,
238
239    /// Statement of funds (cash flow tracking)
240    #[serde(rename = "StmtFunds", default)]
241    pub statement_of_funds: StatementOfFundsWrapper,
242
243    /// Change in position value (reconciliation)
244    #[serde(rename = "ChangeInPositionValues", default)]
245    pub change_in_position_values: ChangeInPositionValueWrapper,
246
247    /// Unbundled commission details
248    #[serde(rename = "UnbundledCommissionDetails", default)]
249    pub unbundled_commission_details: UnbundledCommissionDetailWrapper,
250
251    /// Client fees (advisory fees)
252    #[serde(rename = "ClientFees", default)]
253    pub client_fees: ClientFeesWrapper,
254
255    /// Client fees detail
256    #[serde(rename = "ClientFeesDetails", default)]
257    pub client_fees_detail: ClientFeesDetailWrapper,
258
259    /// Securities lending activities
260    #[serde(rename = "SLBActivities", default)]
261    pub slb_activities: SLBActivitiesWrapper,
262
263    /// Securities lending fees
264    #[serde(rename = "SLBFees", default)]
265    pub slb_fees: SLBFeesWrapper,
266
267    /// Hard to borrow details
268    #[serde(rename = "HardToBorrowDetails", default)]
269    pub hard_to_borrow_details: HardToBorrowDetailsWrapper,
270
271    /// FX position lots
272    #[serde(rename = "FxLots", default)]
273    pub fx_lots: FxLotsWrapper,
274
275    /// Unsettled transfers
276    #[serde(rename = "UnsettledTransfers", default)]
277    pub unsettled_transfers: UnsettledTransfersWrapper,
278
279    /// Trade transfers (inter-broker)
280    #[serde(rename = "TradeTransfers", default)]
281    pub trade_transfers: TradeTransfersWrapper,
282
283    /// Prior period positions
284    #[serde(rename = "PriorPeriodPositions", default)]
285    pub prior_period_positions: PriorPeriodPositionsWrapper,
286
287    /// Tier interest details
288    #[serde(rename = "TierInterestDetails", default)]
289    pub tier_interest_details: TierInterestDetailsWrapper,
290
291    /// Debit card activities
292    #[serde(rename = "DebitCardActivities", default)]
293    pub debit_card_activities: DebitCardActivitiesWrapper,
294
295    /// Sales tax
296    #[serde(rename = "SalesTaxes", default)]
297    pub sales_tax: SalesTaxWrapper,
298
299    // Note: SymbolSummary and AssetSummary elements appear INSIDE <Trades>,
300    // not as separate sections. They're handled by TradesWrapper.
301    // Orders also appear inside <Trades> as Order elements.
302    // See TradesWrapper for how these are handled.
303
304    // --- Catch-all fields for sections not yet fully implemented ---
305    // These prevent parse errors when XML contains these sections
306    #[serde(rename = "DepositsOnHold", default, skip_serializing)]
307    deposits_on_hold: IgnoredSection,
308    #[serde(rename = "FxPositions", default, skip_serializing)]
309    fx_positions: IgnoredSection,
310    /// Net stock positions (stock + option-equivalent exposure)
311    #[serde(rename = "NetStockPositions", default)]
312    pub net_stock_positions: NetStockPositionsWrapper,
313    #[serde(rename = "ComplexPositions", default, skip_serializing)]
314    complex_positions: IgnoredSection,
315    /// CFD financing charges
316    #[serde(rename = "CFDCharges", default)]
317    pub cfd_charges: CFDChargesWrapper,
318    #[serde(rename = "CommissionCredits", default, skip_serializing)]
319    commission_credits: IgnoredSection,
320    #[serde(rename = "FdicInsuredDepositsByBank", default, skip_serializing)]
321    fdic_insured_deposits: IgnoredSection,
322    #[serde(rename = "HKIPOOpenSubscriptions", default, skip_serializing)]
323    hk_ipo_open_subscriptions: IgnoredSection,
324    #[serde(rename = "HKIPOSubscriptionActivity", default, skip_serializing)]
325    hk_ipo_subscription_activity: IgnoredSection,
326    #[serde(rename = "IBGNoteTransactions", default, skip_serializing)]
327    ibg_note_transactions: IgnoredSection,
328    #[serde(rename = "IncentiveCouponAccrualDetails", default, skip_serializing)]
329    incentive_coupon_accruals: IgnoredSection,
330    #[serde(rename = "MutualFundDividendDetails", default, skip_serializing)]
331    mutual_fund_dividends: IgnoredSection,
332    #[serde(rename = "NetStockPositionSummary", default, skip_serializing)]
333    net_stock_position_summary: IgnoredSection,
334    #[serde(rename = "PendingExcercises", default, skip_serializing)]
335    pending_exercises: IgnoredSection,
336    #[serde(rename = "RoutingCommissions", default, skip_serializing)]
337    routing_commissions: IgnoredSection,
338    #[serde(rename = "SLBCollaterals", default, skip_serializing)]
339    slb_collaterals: IgnoredSection,
340    /// Open stock-loan/borrow contracts
341    #[serde(rename = "SLBOpenContracts", default)]
342    pub slb_open_contracts: SLBOpenContractsWrapper,
343    #[serde(rename = "SoftDollars", default, skip_serializing)]
344    soft_dollars: IgnoredSection,
345    /// Employee stock grant / vesting activity
346    #[serde(rename = "StockGrantActivities", default)]
347    pub stock_grant_activities: StockGrantActivitiesWrapper,
348    /// Per-transaction taxes (stamp duty, FTT, etc.)
349    #[serde(rename = "TransactionTaxes", default)]
350    pub transaction_taxes: TransactionTaxesWrapper,
351    #[serde(rename = "UnbookedTrades", default, skip_serializing)]
352    unbooked_trades: IgnoredSection,
353    // Note: Catch-all flatten disabled as it causes issues with multi-statement files
354    // All unknown sections should be explicitly listed above with IgnoredSection
355}
356
357/// Helper type for sections we want to ignore during parsing
358#[derive(Debug, Clone, PartialEq, Default)]
359struct IgnoredSection;
360
361impl<'de> serde::Deserialize<'de> for IgnoredSection {
362    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
363    where
364        D: serde::Deserializer<'de>,
365    {
366        // Ignore whatever content is in this section
367        serde::de::IgnoredAny::deserialize(deserializer)?;
368        Ok(IgnoredSection)
369    }
370}
371
372/// Element types that can appear in the `<Trades>` section.
373///
374/// IB FLEX interleaves different element types by symbol, so we parse them all
375/// into an enum and then filter by type for user access.
376#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
377#[serde(rename_all = "PascalCase")]
378enum TradesItem {
379    Trade(Trade),
380    Order(Trade),
381    SymbolSummary(Trade),
382    AssetSummary(Trade),
383    WashSale(Trade),
384    Lot(Trade),
385}
386
387/// Wrapper for trades section
388///
389/// The IB FLEX `<Trades>` section can contain multiple element types based on
390/// the `levelOfDetail` attribute:
391/// - `<Trade>` with levelOfDetail="EXECUTION" - individual trade executions
392/// - `<Order>` with levelOfDetail="ORDER" - order summaries
393/// - `<SymbolSummary>`, `<AssetSummary>`, `<WashSale>`, `<Lot>` - various summary records
394///
395/// These elements can be interleaved (grouped by symbol), not by type.
396#[derive(Debug, Clone, PartialEq, Default, Serialize)]
397pub struct TradesWrapper {
398    /// Trade executions (main trading data)
399    pub items: Vec<Trade>,
400
401    /// Wash sale records
402    pub wash_sales: Vec<Trade>,
403
404    /// Cost-basis lot detail rows (`<Lot>`), emitted as trade-shaped siblings
405    /// inside `<Trades>` when the query requests lot-level detail.
406    pub lots: Vec<Trade>,
407
408    /// Order roll-up rows (`<Order>`, `levelOfDetail="ORDER"`).
409    pub orders: Vec<Trade>,
410
411    /// Per-symbol roll-up rows (`<SymbolSummary>`).
412    pub symbol_summaries: Vec<Trade>,
413
414    /// Per-asset-class roll-up rows (`<AssetSummary>`).
415    pub asset_summaries: Vec<Trade>,
416}
417
418impl<'de> serde::Deserialize<'de> for TradesWrapper {
419    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
420    where
421        D: serde::Deserializer<'de>,
422    {
423        #[derive(Deserialize)]
424        struct Raw {
425            #[serde(rename = "$value", default)]
426            items: Vec<TradesItem>,
427        }
428
429        let raw = Raw::deserialize(deserializer)?;
430
431        let mut trades = Vec::new();
432        let mut wash_sales = Vec::new();
433        let mut lots = Vec::new();
434        let mut orders = Vec::new();
435        let mut symbol_summaries = Vec::new();
436        let mut asset_summaries = Vec::new();
437
438        for item in raw.items {
439            match item {
440                TradesItem::Trade(t) => trades.push(t),
441                TradesItem::WashSale(t) => wash_sales.push(t),
442                TradesItem::Lot(t) => lots.push(t),
443                TradesItem::Order(t) => orders.push(t),
444                TradesItem::SymbolSummary(t) => symbol_summaries.push(t),
445                TradesItem::AssetSummary(t) => asset_summaries.push(t),
446            }
447        }
448
449        Ok(TradesWrapper {
450            items: trades,
451            wash_sales,
452            lots,
453            orders,
454            symbol_summaries,
455            asset_summaries,
456        })
457    }
458}
459
460/// Wrapper for positions section
461#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
462pub struct PositionsWrapper {
463    /// List of positions
464    #[serde(rename = "OpenPosition", default)]
465    pub items: Vec<Position>,
466}
467
468/// Wrapper for cash transactions section
469#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
470pub struct CashTransactionsWrapper {
471    /// List of cash transactions
472    #[serde(rename = "CashTransaction", default)]
473    pub items: Vec<CashTransaction>,
474}
475
476/// Wrapper for corporate actions section
477#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
478pub struct CorporateActionsWrapper {
479    /// List of corporate actions
480    #[serde(rename = "CorporateAction", default)]
481    pub items: Vec<CorporateAction>,
482}
483
484/// Wrapper for the net stock positions section
485#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
486pub struct NetStockPositionsWrapper {
487    /// List of net stock positions
488    #[serde(rename = "NetStockPosition", default)]
489    pub items: Vec<super::extended::NetStockPosition>,
490}
491
492/// Wrapper for the CFD charges section
493#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
494pub struct CFDChargesWrapper {
495    /// List of CFD charges
496    #[serde(rename = "CFDCharge", default)]
497    pub items: Vec<super::extended::CFDCharge>,
498}
499
500/// Wrapper for the SLB open contracts section
501#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
502pub struct SLBOpenContractsWrapper {
503    /// List of open stock-loan/borrow contracts
504    #[serde(rename = "SLBOpenContract", default)]
505    pub items: Vec<super::extended::SLBOpenContract>,
506}
507
508/// Wrapper for the stock grant activities section
509#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
510pub struct StockGrantActivitiesWrapper {
511    /// List of stock grant / vesting activities
512    #[serde(rename = "StockGrantActivity", default)]
513    pub items: Vec<super::extended::StockGrantActivity>,
514}
515
516/// Wrapper for the transaction taxes section
517#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
518pub struct TransactionTaxesWrapper {
519    /// List of per-transaction taxes
520    #[serde(rename = "TransactionTax", default)]
521    pub items: Vec<super::extended::TransactionTax>,
522}
523
524/// A single trade execution
525///
526/// Represents one trade execution from the Activity FLEX statement.
527/// Fields are organized into CORE (essential for tax/portfolio analytics)
528/// and EXTENDED (metadata, execution details) sections.
529///
530/// # Example
531/// ```no_run
532/// use ib_flex::parse_activity_flex;
533/// use ib_flex::{AssetCategory, BuySell};
534///
535/// let xml = std::fs::read_to_string("activity.xml")?;
536/// let statement = parse_activity_flex(&xml)?;
537///
538/// for trade in &statement.trades.items {
539///     // Access basic trade info
540///     println!("Symbol: {:?}", trade.symbol);
541///     println!("Asset: {:?}", trade.asset_category);
542///
543///     // Check trade direction
544///     match trade.buy_sell {
545///         Some(BuySell::Buy) => println!("Bought"),
546///         Some(BuySell::Sell) => println!("Sold"),
547///         _ => {}
548///     }
549///
550///     // Calculate total cost
551///     let quantity = trade.quantity.unwrap_or_default();
552///     let price = trade.price.unwrap_or_default();
553///     let cost = quantity * price;
554///     println!("Cost: {}", cost);
555///
556///     // Access P&L if available
557///     if let Some(pnl) = trade.fifo_pnl_realized {
558///         println!("Realized P&L: {}", pnl);
559///     }
560///
561///     // Check for options
562///     if trade.asset_category == Some(AssetCategory::Option) {
563///         println!("Strike: {:?}", trade.strike);
564///         println!("Expiry: {:?}", trade.expiry);
565///         println!("Put/Call: {:?}", trade.put_call);
566///     }
567/// }
568/// # Ok::<(), Box<dyn std::error::Error>>(())
569/// ```
570#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
571pub struct Trade {
572    // ==================== CORE FIELDS ====================
573    // Essential for tax reporting and portfolio analytics
574
575    // --- Account ---
576    /// IB account number
577    #[serde(
578        rename = "@accountId",
579        default,
580        deserialize_with = "deserialize_optional_string"
581    )]
582    pub account_id: Option<String>,
583
584    /// IB transaction ID (unique identifier for idempotency)
585    #[serde(rename = "@transactionID", default)]
586    pub transaction_id: Option<String>,
587
588    // --- Security Identification ---
589    /// IB contract ID (unique per security)
590    #[serde(
591        rename = "@conid",
592        default,
593        deserialize_with = "deserialize_optional_string"
594    )]
595    pub conid: Option<String>,
596
597    /// Ticker symbol
598    #[serde(
599        rename = "@symbol",
600        default,
601        deserialize_with = "deserialize_optional_string"
602    )]
603    pub symbol: Option<String>,
604
605    /// Security description
606    #[serde(rename = "@description", default)]
607    pub description: Option<String>,
608
609    /// Asset category (stock, option, future, etc.)
610    #[serde(rename = "@assetCategory", default)]
611    pub asset_category: Option<AssetCategory>,
612
613    /// CUSIP
614    #[serde(rename = "@cusip", default)]
615    pub cusip: Option<String>,
616
617    /// ISIN
618    #[serde(rename = "@isin", default)]
619    pub isin: Option<String>,
620
621    /// FIGI
622    #[serde(rename = "@figi", default)]
623    pub figi: Option<String>,
624
625    /// Security ID
626    #[serde(rename = "@securityID", default)]
627    pub security_id: Option<String>,
628
629    /// Security ID type
630    #[serde(rename = "@securityIDType", default)]
631    pub security_id_type: Option<SecurityIdType>,
632
633    // --- Derivatives (Options/Futures) ---
634    /// Contract multiplier (for futures/options)
635    #[serde(
636        rename = "@multiplier",
637        default,
638        deserialize_with = "deserialize_optional_decimal"
639    )]
640    pub multiplier: Option<Decimal>,
641
642    /// Strike price (for options)
643    #[serde(
644        rename = "@strike",
645        default,
646        deserialize_with = "deserialize_optional_decimal"
647    )]
648    pub strike: Option<Decimal>,
649
650    /// Expiry date (for options/futures)
651    #[serde(
652        rename = "@expiry",
653        default,
654        deserialize_with = "deserialize_optional_date"
655    )]
656    pub expiry: Option<NaiveDate>,
657
658    /// Put or Call (for options)
659    #[serde(rename = "@putCall", default)]
660    pub put_call: Option<PutCall>,
661
662    /// Underlying security's contract ID (for derivatives)
663    #[serde(rename = "@underlyingConid", default)]
664    pub underlying_conid: Option<String>,
665
666    /// Underlying symbol
667    #[serde(rename = "@underlyingSymbol", default)]
668    pub underlying_symbol: Option<String>,
669
670    // --- Trade Execution ---
671    /// Trade date (may be empty for summary records)
672    #[serde(
673        rename = "@tradeDate",
674        default,
675        deserialize_with = "deserialize_optional_date"
676    )]
677    pub trade_date: Option<NaiveDate>,
678
679    /// Settlement date (may be empty for summary records)
680    #[serde(
681        rename = "@settleDateTarget",
682        default,
683        deserialize_with = "deserialize_optional_date"
684    )]
685    pub settle_date: Option<NaiveDate>,
686
687    /// Buy or Sell
688    #[serde(rename = "@buySell", default)]
689    pub buy_sell: Option<BuySell>,
690
691    /// Open or Close indicator (for options/futures)
692    #[serde(rename = "@openCloseIndicator", default)]
693    pub open_close: Option<OpenClose>,
694
695    /// Transaction type (ExchTrade, BookTrade, etc.)
696    #[serde(rename = "@transactionType", default)]
697    pub transaction_type: Option<TradeType>,
698
699    // --- Quantities and Prices ---
700    /// Quantity (number of shares/contracts)
701    #[serde(
702        rename = "@quantity",
703        default,
704        deserialize_with = "deserialize_optional_decimal"
705    )]
706    pub quantity: Option<Decimal>,
707
708    /// Trade price per share/contract
709    ///
710    /// Real IB Activity Flex statements emit this as `tradePrice`. Older
711    /// synthetic fixtures use the bare `price` attribute, which is accepted as
712    /// an alias for backward compatibility.
713    #[serde(
714        rename = "@tradePrice",
715        alias = "@price",
716        default,
717        deserialize_with = "deserialize_optional_decimal"
718    )]
719    pub price: Option<Decimal>,
720
721    /// Trade proceeds (negative for buys, positive for sells)
722    #[serde(
723        rename = "@proceeds",
724        default,
725        deserialize_with = "deserialize_optional_decimal"
726    )]
727    pub proceeds: Option<Decimal>,
728
729    /// Cost basis
730    #[serde(
731        rename = "@cost",
732        default,
733        deserialize_with = "deserialize_optional_decimal"
734    )]
735    pub cost: Option<Decimal>,
736
737    // --- Fees and Taxes ---
738    /// Commission paid
739    #[serde(
740        rename = "@ibCommission",
741        default,
742        deserialize_with = "deserialize_optional_decimal"
743    )]
744    pub commission: Option<Decimal>,
745
746    /// Taxes paid
747    #[serde(
748        rename = "@taxes",
749        default,
750        deserialize_with = "deserialize_optional_decimal"
751    )]
752    pub taxes: Option<Decimal>,
753
754    /// Net cash (proceeds + commission + taxes)
755    #[serde(
756        rename = "@netCash",
757        default,
758        deserialize_with = "deserialize_optional_decimal"
759    )]
760    pub net_cash: Option<Decimal>,
761
762    // --- P&L ---
763    /// FIFO realized P&L (for closing trades)
764    #[serde(
765        rename = "@fifoPnlRealized",
766        default,
767        deserialize_with = "deserialize_optional_decimal"
768    )]
769    pub fifo_pnl_realized: Option<Decimal>,
770
771    /// Mark-to-market P&L
772    #[serde(
773        rename = "@mtmPnl",
774        default,
775        deserialize_with = "deserialize_optional_decimal"
776    )]
777    pub mtm_pnl: Option<Decimal>,
778
779    /// FX P&L (for multi-currency)
780    #[serde(
781        rename = "@fxPnl",
782        default,
783        deserialize_with = "deserialize_optional_decimal"
784    )]
785    pub fx_pnl: Option<Decimal>,
786
787    // --- Currency ---
788    /// Trade currency
789    #[serde(
790        rename = "@currency",
791        default,
792        deserialize_with = "deserialize_optional_string"
793    )]
794    pub currency: Option<String>,
795
796    /// FX rate to base currency
797    #[serde(
798        rename = "@fxRateToBase",
799        default,
800        deserialize_with = "deserialize_optional_decimal"
801    )]
802    pub fx_rate_to_base: Option<Decimal>,
803
804    // --- Tax Lot Tracking (Critical for tax reporting) ---
805    /// Original trade date (for lot tracking and holding period)
806    #[serde(
807        rename = "@origTradeDate",
808        default,
809        deserialize_with = "deserialize_optional_date"
810    )]
811    pub orig_trade_date: Option<NaiveDate>,
812
813    /// Original trade price (cost basis of the lot)
814    #[serde(
815        rename = "@origTradePrice",
816        default,
817        deserialize_with = "deserialize_optional_decimal"
818    )]
819    pub orig_trade_price: Option<Decimal>,
820
821    /// Original trade ID (links closing trade to opening trade)
822    #[serde(rename = "@origTradeID", default)]
823    pub orig_trade_id: Option<String>,
824
825    /// Holding period date/time (for long-term vs short-term determination)
826    #[serde(rename = "@holdingPeriodDateTime", default)]
827    pub holding_period_date_time: Option<String>,
828
829    /// When position was opened
830    #[serde(rename = "@openDateTime", default)]
831    pub open_date_time: Option<String>,
832
833    /// When position was reopened (for wash sale tracking)
834    #[serde(rename = "@whenReopened", default)]
835    pub when_reopened: Option<String>,
836
837    /// Trade notes/codes (may contain wash sale indicator "W")
838    #[serde(rename = "@notes", default)]
839    pub notes: Option<String>,
840
841    // ==================== EXTENDED FIELDS ====================
842    // Metadata, execution details, and less commonly used fields
843
844    // --- Order/Execution IDs ---
845    /// IB order ID (may be shared across multiple executions).
846    ///
847    /// Real IB Trade rows emit this as `ibOrderID`; `orderID` (never present on
848    /// the Trade element) is kept as an alias for backward compatibility.
849    #[serde(rename = "@ibOrderID", alias = "@orderID", default)]
850    pub ib_order_id: Option<String>,
851
852    /// Execution ID.
853    ///
854    /// IB does not emit a bare `execID` on the Trade element (the execution id
855    /// is `ibExecID`, captured by [`Trade::ib_exec_id`]); `execID` is retained
856    /// only for backward compatibility and is normally `None` on real data.
857    #[serde(rename = "@execID", default)]
858    pub exec_id: Option<String>,
859
860    /// Trade ID
861    #[serde(rename = "@tradeID", default)]
862    pub trade_id: Option<String>,
863
864    /// Original transaction ID
865    #[serde(rename = "@origTransactionID", default)]
866    pub orig_transaction_id: Option<String>,
867
868    /// Original order ID
869    #[serde(rename = "@origOrderID", default)]
870    pub orig_order_id: Option<String>,
871
872    // --- Timestamps ---
873    /// Trade time (date + time)
874    #[serde(rename = "@dateTime", default)]
875    pub trade_time: Option<String>,
876
877    /// When P&L was realized
878    #[serde(rename = "@whenRealized", default)]
879    pub when_realized: Option<String>,
880
881    /// Order time
882    #[serde(rename = "@orderTime", default)]
883    pub order_time: Option<String>,
884
885    // --- Order Details ---
886    /// Order type (market, limit, stop, etc.)
887    #[serde(rename = "@orderType", default)]
888    pub order_type: Option<OrderType>,
889
890    /// Brokerage order ID
891    #[serde(rename = "@brokerageOrderID", default)]
892    pub brokerage_order_id: Option<String>,
893
894    /// Order reference
895    #[serde(rename = "@orderReference", default)]
896    pub order_reference: Option<String>,
897
898    /// Exchange order ID
899    #[serde(rename = "@exchOrderId", default)]
900    pub exch_order_id: Option<String>,
901
902    /// External execution ID
903    #[serde(rename = "@extExecID", default)]
904    pub ext_exec_id: Option<String>,
905
906    /// IB execution ID
907    #[serde(rename = "@ibExecID", default)]
908    pub ib_exec_id: Option<String>,
909
910    // --- Issuer/Security Metadata ---
911    /// Issuer
912    #[serde(rename = "@issuer", default)]
913    pub issuer: Option<String>,
914
915    /// Issuer country code
916    #[serde(rename = "@issuerCountryCode", default)]
917    pub issuer_country_code: Option<String>,
918
919    /// Sub-category
920    #[serde(rename = "@subCategory", default)]
921    pub sub_category: Option<SubCategory>,
922
923    /// Listing exchange
924    #[serde(rename = "@listingExchange", default)]
925    pub listing_exchange: Option<String>,
926
927    // --- Underlying Extended ---
928    /// Underlying listing exchange
929    #[serde(rename = "@underlyingListingExchange", default)]
930    pub underlying_listing_exchange: Option<String>,
931
932    /// Underlying security ID
933    #[serde(rename = "@underlyingSecurityID", default)]
934    pub underlying_security_id: Option<String>,
935
936    // --- Execution Metadata ---
937    /// Trader ID
938    #[serde(rename = "@traderID", default)]
939    pub trader_id: Option<String>,
940
941    /// Is API order (true if order was placed via API)
942    #[serde(
943        rename = "@isAPIOrder",
944        default,
945        deserialize_with = "deserialize_optional_bool"
946    )]
947    pub is_api_order: Option<bool>,
948
949    /// Volatility order link
950    #[serde(rename = "@volatilityOrderLink", default)]
951    pub volatility_order_link: Option<String>,
952
953    /// Clearing firm ID
954    #[serde(rename = "@clearingFirmID", default)]
955    pub clearing_firm_id: Option<String>,
956
957    /// Level of detail (EXECUTION, ORDER, CLOSED_LOT, etc.)
958    #[serde(rename = "@levelOfDetail", default)]
959    pub level_of_detail: Option<LevelOfDetail>,
960
961    // --- Price/Quantity Changes ---
962    /// Trade amount
963    #[serde(
964        rename = "@amount",
965        default,
966        deserialize_with = "deserialize_optional_decimal"
967    )]
968    pub amount: Option<Decimal>,
969
970    /// Trade money (quantity * price)
971    #[serde(
972        rename = "@tradeMoney",
973        default,
974        deserialize_with = "deserialize_optional_decimal"
975    )]
976    pub trade_money: Option<Decimal>,
977
978    /// Close price
979    #[serde(
980        rename = "@closePrice",
981        default,
982        deserialize_with = "deserialize_optional_decimal"
983    )]
984    pub close_price: Option<Decimal>,
985
986    /// Change in price
987    #[serde(
988        rename = "@changeInPrice",
989        default,
990        deserialize_with = "deserialize_optional_decimal"
991    )]
992    pub change_in_price: Option<Decimal>,
993
994    /// Change in quantity
995    #[serde(
996        rename = "@changeInQuantity",
997        default,
998        deserialize_with = "deserialize_optional_decimal"
999    )]
1000    pub change_in_quantity: Option<Decimal>,
1001
1002    /// Commission currency
1003    #[serde(rename = "@ibCommissionCurrency", default)]
1004    pub commission_currency: Option<String>,
1005
1006    // --- Related Trade Tracking ---
1007    /// Related trade ID
1008    #[serde(rename = "@relatedTradeID", default)]
1009    pub related_trade_id: Option<String>,
1010
1011    /// Related transaction ID
1012    #[serde(rename = "@relatedTransactionID", default)]
1013    pub related_transaction_id: Option<String>,
1014
1015    // --- Bond Fields ---
1016    /// Accrued interest
1017    #[serde(
1018        rename = "@accruedInt",
1019        default,
1020        deserialize_with = "deserialize_optional_decimal"
1021    )]
1022    pub accrued_int: Option<Decimal>,
1023
1024    /// Principal adjust factor
1025    #[serde(
1026        rename = "@principalAdjustFactor",
1027        default,
1028        deserialize_with = "deserialize_optional_decimal"
1029    )]
1030    pub principal_adjust_factor: Option<Decimal>,
1031
1032    // --- Commodity/Physical Delivery ---
1033    /// Serial number (for physical delivery)
1034    #[serde(rename = "@serialNumber", default)]
1035    pub serial_number: Option<String>,
1036
1037    /// Delivery type
1038    #[serde(rename = "@deliveryType", default)]
1039    pub delivery_type: Option<String>,
1040
1041    /// Commodity type
1042    #[serde(rename = "@commodityType", default)]
1043    pub commodity_type: Option<String>,
1044
1045    /// Fineness (for precious metals)
1046    #[serde(
1047        rename = "@fineness",
1048        default,
1049        deserialize_with = "deserialize_optional_decimal"
1050    )]
1051    pub fineness: Option<Decimal>,
1052
1053    /// Weight
1054    #[serde(rename = "@weight", default)]
1055    pub weight: Option<String>,
1056
1057    // --- Other Metadata ---
1058    /// Report date
1059    #[serde(
1060        rename = "@reportDate",
1061        default,
1062        deserialize_with = "deserialize_optional_date"
1063    )]
1064    pub report_date: Option<NaiveDate>,
1065
1066    /// Exchange where trade executed
1067    #[serde(rename = "@exchange", default)]
1068    pub exchange: Option<String>,
1069
1070    /// Model (for model portfolios)
1071    #[serde(rename = "@model", default)]
1072    pub model: Option<String>,
1073
1074    /// Account alias
1075    #[serde(rename = "@acctAlias", default)]
1076    pub acct_alias: Option<String>,
1077
1078    /// RTN
1079    #[serde(rename = "@rtn", default)]
1080    pub rtn: Option<String>,
1081
1082    /// Position action ID
1083    #[serde(rename = "@positionActionID", default)]
1084    pub position_action_id: Option<String>,
1085
1086    /// Initial investment.
1087    ///
1088    /// Kept as a raw string: IB emits non-numeric values here on some asset
1089    /// classes, so parsing it as a decimal would fail the whole statement.
1090    #[serde(
1091        rename = "@initialInvestment",
1092        default,
1093        deserialize_with = "deserialize_optional_string"
1094    )]
1095    pub initial_investment: Option<String>,
1096
1097    /// Net cash in the account's base currency (`netCash` × `fxRateToBase`).
1098    #[serde(
1099        rename = "@netCashInBase",
1100        default,
1101        deserialize_with = "deserialize_optional_decimal"
1102    )]
1103    pub net_cash_in_base: Option<Decimal>,
1104
1105    /// Capital gains P&L component (distinct from `fifoPnlRealized`).
1106    #[serde(
1107        rename = "@capitalGainsPnl",
1108        default,
1109        deserialize_with = "deserialize_optional_decimal"
1110    )]
1111    pub capital_gains_pnl: Option<Decimal>,
1112
1113    /// SEDOL security identifier (LSE/UK markets).
1114    #[serde(rename = "@sedol", default)]
1115    pub sedol: Option<String>,
1116
1117    /// `tradeTime` (added for full ibflex coverage)
1118    #[serde(
1119        rename = "@tradeTime",
1120        default,
1121        deserialize_with = "deserialize_optional_string"
1122    )]
1123    pub trade_time_trad: Option<String>,
1124}
1125
1126impl Trade {
1127    /// Constructs derivative information from flat fields based on asset category
1128    ///
1129    /// This method consolidates derivative-specific fields (strike, expiry, put_call,
1130    /// underlying_symbol, underlying_conid) into a structured `DerivativeInfo` enum
1131    /// based on the trade's asset category.
1132    ///
1133    /// # Returns
1134    /// - `Some(DerivativeInfo)` if the asset is a derivative with complete information
1135    /// - `None` if the asset is not a derivative or lacks required fields
1136    ///
1137    /// # Example
1138    /// ```no_run
1139    /// use ib_flex::parse_activity_flex;
1140    ///
1141    /// let xml = std::fs::read_to_string("activity.xml")?;
1142    /// let statement = parse_activity_flex(&xml)?;
1143    ///
1144    /// for trade in &statement.trades.items {
1145    ///     if let Some(derivative) = trade.derivative() {
1146    ///         match derivative {
1147    ///             ib_flex::types::DerivativeInfo::Option { strike, expiry, put_call, .. } => {
1148    ///                 println!("Option trade: {:?} ${} exp {}", put_call, strike, expiry);
1149    ///             }
1150    ///             ib_flex::types::DerivativeInfo::Future { expiry, .. } => {
1151    ///                 println!("Future trade: exp {}", expiry);
1152    ///             }
1153    ///             _ => {}
1154    ///         }
1155    ///     }
1156    /// }
1157    /// # Ok::<(), Box<dyn std::error::Error>>(())
1158    /// ```
1159    pub fn derivative(&self) -> Option<DerivativeInfo> {
1160        match self.asset_category? {
1161            AssetCategory::Option => {
1162                // For options, we need: strike, expiry, put_call, underlying_symbol
1163                let strike = self.strike?;
1164                let expiry = self.expiry?;
1165                let put_call = self.put_call?;
1166                let underlying_symbol = self.underlying_symbol.clone()?;
1167
1168                Some(DerivativeInfo::Option {
1169                    strike,
1170                    expiry,
1171                    put_call,
1172                    underlying_symbol,
1173                    underlying_conid: self.underlying_conid.clone(),
1174                })
1175            }
1176            AssetCategory::Future => {
1177                // For futures, we need: expiry, underlying_symbol
1178                let expiry = self.expiry?;
1179                let underlying_symbol = self.underlying_symbol.clone()?;
1180
1181                Some(DerivativeInfo::Future {
1182                    expiry,
1183                    underlying_symbol,
1184                    underlying_conid: self.underlying_conid.clone(),
1185                })
1186            }
1187            AssetCategory::FutureOption => {
1188                // For future options, we need: strike, expiry, put_call, underlying_symbol
1189                let strike = self.strike?;
1190                let expiry = self.expiry?;
1191                let put_call = self.put_call?;
1192                let underlying_symbol = self.underlying_symbol.clone()?;
1193
1194                Some(DerivativeInfo::FutureOption {
1195                    strike,
1196                    expiry,
1197                    put_call,
1198                    underlying_symbol,
1199                    underlying_conid: self.underlying_conid.clone(),
1200                })
1201            }
1202            AssetCategory::Warrant => {
1203                // For warrants, all fields are optional but we need at least underlying_symbol
1204                let underlying_symbol = self.underlying_symbol.clone()?;
1205
1206                Some(DerivativeInfo::Warrant {
1207                    strike: self.strike,
1208                    expiry: self.expiry,
1209                    underlying_symbol: Some(underlying_symbol),
1210                })
1211            }
1212            // Not a derivative type
1213            _ => None,
1214        }
1215    }
1216}
1217
1218/// An open position snapshot
1219///
1220/// Represents a single open position at the end of the reporting period.
1221/// Fields are organized into CORE (essential for tax/portfolio analytics)
1222/// and EXTENDED (metadata) sections.
1223///
1224/// # Example
1225/// ```no_run
1226/// use ib_flex::parse_activity_flex;
1227/// use rust_decimal::Decimal;
1228///
1229/// let xml = std::fs::read_to_string("activity.xml")?;
1230/// let statement = parse_activity_flex(&xml)?;
1231///
1232/// for position in &statement.positions.items {
1233///     println!("{:?}: {:?} shares", position.symbol, position.quantity);
1234///     println!("  Current price: {:?}", position.mark_price);
1235///     println!("  Position value: {:?}", position.position_value);
1236///
1237///     // Calculate gain/loss percentage
1238///     if let Some(cost_basis) = position.cost_basis_money {
1239///         let current_value = position.position_value.unwrap_or_default();
1240///         let gain_pct = ((current_value - cost_basis) / cost_basis) * Decimal::from(100);
1241///         println!("  Gain: {:.2}%", gain_pct);
1242///     }
1243///
1244///     // Show unrealized P&L
1245///     if let Some(pnl) = position.fifo_pnl_unrealized {
1246///         println!("  Unrealized P&L: {}", pnl);
1247///     }
1248///
1249///     // Check if short position
1250///     if position.quantity.unwrap_or_default() < Decimal::ZERO {
1251///         println!("  SHORT POSITION");
1252///     }
1253/// }
1254/// # Ok::<(), Box<dyn std::error::Error>>(())
1255/// ```
1256#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1257pub struct Position {
1258    // ==================== CORE FIELDS ====================
1259    // Essential for tax reporting and portfolio analytics
1260
1261    // --- Account ---
1262    /// IB account number
1263    #[serde(
1264        rename = "@accountId",
1265        default,
1266        deserialize_with = "deserialize_optional_string"
1267    )]
1268    pub account_id: Option<String>,
1269
1270    // --- Security Identification ---
1271    /// IB contract ID
1272    #[serde(
1273        rename = "@conid",
1274        default,
1275        deserialize_with = "deserialize_optional_string"
1276    )]
1277    pub conid: Option<String>,
1278
1279    /// Ticker symbol
1280    #[serde(
1281        rename = "@symbol",
1282        default,
1283        deserialize_with = "deserialize_optional_string"
1284    )]
1285    pub symbol: Option<String>,
1286
1287    /// Security description
1288    #[serde(rename = "@description", default)]
1289    pub description: Option<String>,
1290
1291    /// Asset category
1292    #[serde(rename = "@assetCategory", default)]
1293    pub asset_category: Option<AssetCategory>,
1294
1295    /// CUSIP
1296    #[serde(rename = "@cusip", default)]
1297    pub cusip: Option<String>,
1298
1299    /// ISIN
1300    #[serde(rename = "@isin", default)]
1301    pub isin: Option<String>,
1302
1303    /// FIGI
1304    #[serde(rename = "@figi", default)]
1305    pub figi: Option<String>,
1306
1307    /// Security ID
1308    #[serde(rename = "@securityID", default)]
1309    pub security_id: Option<String>,
1310
1311    /// Security ID type
1312    #[serde(rename = "@securityIDType", default)]
1313    pub security_id_type: Option<SecurityIdType>,
1314
1315    // --- Derivatives (Options/Futures) ---
1316    /// Contract multiplier
1317    #[serde(
1318        rename = "@multiplier",
1319        default,
1320        deserialize_with = "deserialize_optional_decimal"
1321    )]
1322    pub multiplier: Option<Decimal>,
1323
1324    /// Strike (for options)
1325    #[serde(
1326        rename = "@strike",
1327        default,
1328        deserialize_with = "deserialize_optional_decimal"
1329    )]
1330    pub strike: Option<Decimal>,
1331
1332    /// Expiry (for options/futures)
1333    #[serde(
1334        rename = "@expiry",
1335        default,
1336        deserialize_with = "deserialize_optional_date"
1337    )]
1338    pub expiry: Option<NaiveDate>,
1339
1340    /// Put or Call
1341    #[serde(rename = "@putCall", default)]
1342    pub put_call: Option<PutCall>,
1343
1344    /// Underlying contract ID
1345    #[serde(rename = "@underlyingConid", default)]
1346    pub underlying_conid: Option<String>,
1347
1348    /// Underlying symbol
1349    #[serde(rename = "@underlyingSymbol", default)]
1350    pub underlying_symbol: Option<String>,
1351
1352    // --- Position and Value ---
1353    /// Position quantity (negative for short)
1354    #[serde(
1355        rename = "@position",
1356        default,
1357        deserialize_with = "deserialize_optional_decimal"
1358    )]
1359    pub quantity: Option<Decimal>,
1360
1361    /// Mark price (current market price)
1362    #[serde(
1363        rename = "@markPrice",
1364        default,
1365        deserialize_with = "deserialize_optional_decimal"
1366    )]
1367    pub mark_price: Option<Decimal>,
1368
1369    /// Position value (quantity * mark_price * multiplier)
1370    #[serde(
1371        rename = "@positionValue",
1372        default,
1373        deserialize_with = "deserialize_optional_decimal"
1374    )]
1375    pub position_value: Option<Decimal>,
1376
1377    /// Side (Long/Short)
1378    #[serde(rename = "@side", default)]
1379    pub side: Option<String>,
1380
1381    // --- Cost Basis and P&L ---
1382    /// Open price
1383    #[serde(
1384        rename = "@openPrice",
1385        default,
1386        deserialize_with = "deserialize_optional_decimal"
1387    )]
1388    pub open_price: Option<Decimal>,
1389
1390    /// Cost basis price per share/contract
1391    #[serde(
1392        rename = "@costBasisPrice",
1393        default,
1394        deserialize_with = "deserialize_optional_decimal"
1395    )]
1396    pub cost_basis_price: Option<Decimal>,
1397
1398    /// Total cost basis
1399    #[serde(
1400        rename = "@costBasisMoney",
1401        default,
1402        deserialize_with = "deserialize_optional_decimal"
1403    )]
1404    pub cost_basis_money: Option<Decimal>,
1405
1406    /// FIFO unrealized P&L
1407    #[serde(
1408        rename = "@fifoPnlUnrealized",
1409        default,
1410        deserialize_with = "deserialize_optional_decimal"
1411    )]
1412    pub fifo_pnl_unrealized: Option<Decimal>,
1413
1414    /// Percent of NAV
1415    #[serde(
1416        rename = "@percentOfNAV",
1417        default,
1418        deserialize_with = "deserialize_optional_decimal"
1419    )]
1420    pub percent_of_nav: Option<Decimal>,
1421
1422    // --- Currency ---
1423    /// Currency
1424    #[serde(
1425        rename = "@currency",
1426        default,
1427        deserialize_with = "deserialize_optional_string"
1428    )]
1429    pub currency: Option<String>,
1430
1431    /// FX rate to base currency
1432    #[serde(
1433        rename = "@fxRateToBase",
1434        default,
1435        deserialize_with = "deserialize_optional_decimal"
1436    )]
1437    pub fx_rate_to_base: Option<Decimal>,
1438
1439    // --- Dates ---
1440    /// Date of this position snapshot
1441    #[serde(
1442        rename = "@reportDate",
1443        default,
1444        deserialize_with = "deserialize_optional_date"
1445    )]
1446    pub report_date: Option<NaiveDate>,
1447
1448    // --- Tax Lot Tracking (Critical for tax reporting) ---
1449    /// Holding period date/time (for long-term vs short-term determination)
1450    #[serde(rename = "@holdingPeriodDateTime", default)]
1451    pub holding_period_date_time: Option<String>,
1452
1453    /// When position was opened
1454    #[serde(rename = "@openDateTime", default)]
1455    pub open_date_time: Option<String>,
1456
1457    /// Originating transaction ID
1458    #[serde(rename = "@originatingTransactionID", default)]
1459    pub originating_transaction_id: Option<String>,
1460
1461    /// Position code (may contain tax-related codes)
1462    #[serde(rename = "@code", default)]
1463    pub code: Option<String>,
1464
1465    // ==================== EXTENDED FIELDS ====================
1466    // Metadata and less commonly used fields
1467
1468    // --- Extended IDs ---
1469    /// Originating order ID (links to opening trade)
1470    #[serde(rename = "@originatingOrderID", default)]
1471    pub originating_order_id: Option<String>,
1472
1473    // --- Issuer/Security Metadata ---
1474    /// Issuer
1475    #[serde(rename = "@issuer", default)]
1476    pub issuer: Option<String>,
1477
1478    /// Issuer country code
1479    #[serde(rename = "@issuerCountryCode", default)]
1480    pub issuer_country_code: Option<String>,
1481
1482    /// Sub-category
1483    #[serde(rename = "@subCategory", default)]
1484    pub sub_category: Option<SubCategory>,
1485
1486    /// Listing exchange
1487    #[serde(rename = "@listingExchange", default)]
1488    pub listing_exchange: Option<String>,
1489
1490    // --- Underlying Extended ---
1491    /// Underlying listing exchange
1492    #[serde(rename = "@underlyingListingExchange", default)]
1493    pub underlying_listing_exchange: Option<String>,
1494
1495    /// Underlying security ID
1496    #[serde(rename = "@underlyingSecurityID", default)]
1497    pub underlying_security_id: Option<String>,
1498
1499    // --- Bond Fields ---
1500    /// Accrued interest
1501    #[serde(
1502        rename = "@accruedInt",
1503        default,
1504        deserialize_with = "deserialize_optional_decimal"
1505    )]
1506    pub accrued_int: Option<Decimal>,
1507
1508    /// Principal adjust factor
1509    #[serde(
1510        rename = "@principalAdjustFactor",
1511        default,
1512        deserialize_with = "deserialize_optional_decimal"
1513    )]
1514    pub principal_adjust_factor: Option<Decimal>,
1515
1516    // --- Commodity/Physical Delivery ---
1517    /// Serial number (for physical delivery)
1518    #[serde(rename = "@serialNumber", default)]
1519    pub serial_number: Option<String>,
1520
1521    /// Delivery type
1522    #[serde(rename = "@deliveryType", default)]
1523    pub delivery_type: Option<String>,
1524
1525    /// Commodity type
1526    #[serde(rename = "@commodityType", default)]
1527    pub commodity_type: Option<String>,
1528
1529    /// Fineness (for precious metals)
1530    #[serde(
1531        rename = "@fineness",
1532        default,
1533        deserialize_with = "deserialize_optional_decimal"
1534    )]
1535    pub fineness: Option<Decimal>,
1536
1537    /// Weight
1538    #[serde(rename = "@weight", default)]
1539    pub weight: Option<String>,
1540
1541    // --- Other Metadata ---
1542    /// Level of detail
1543    #[serde(rename = "@levelOfDetail", default)]
1544    pub level_of_detail: Option<LevelOfDetail>,
1545
1546    /// Model (for model portfolios)
1547    #[serde(rename = "@model", default)]
1548    pub model: Option<String>,
1549
1550    /// Account alias
1551    #[serde(rename = "@acctAlias", default)]
1552    pub acct_alias: Option<String>,
1553
1554    /// Vesting date (for restricted stock)
1555    #[serde(
1556        rename = "@vestingDate",
1557        default,
1558        deserialize_with = "deserialize_optional_date"
1559    )]
1560    pub vesting_date: Option<NaiveDate>,
1561
1562    /// `sedol` (added for full ibflex coverage)
1563    #[serde(
1564        rename = "@sedol",
1565        default,
1566        deserialize_with = "deserialize_optional_string"
1567    )]
1568    pub sedol: Option<String>,
1569
1570    /// `positionValueInBase` (added for full ibflex coverage)
1571    #[serde(
1572        rename = "@positionValueInBase",
1573        default,
1574        deserialize_with = "deserialize_optional_decimal"
1575    )]
1576    pub position_value_in_base: Option<Decimal>,
1577
1578    /// `unrealizedCapitalGainsPnl` (added for full ibflex coverage)
1579    #[serde(
1580        rename = "@unrealizedCapitalGainsPnl",
1581        default,
1582        deserialize_with = "deserialize_optional_decimal"
1583    )]
1584    pub unrealized_capital_gains_pnl: Option<Decimal>,
1585
1586    /// `unrealizedlFxPnl` (added for full ibflex coverage)
1587    #[serde(
1588        rename = "@unrealizedlFxPnl",
1589        default,
1590        deserialize_with = "deserialize_optional_decimal"
1591    )]
1592    pub unrealizedl_fx_pnl: Option<Decimal>,
1593}
1594
1595impl Position {
1596    /// Constructs structured derivative info from flat fields
1597    ///
1598    /// Returns `Some(DerivativeInfo)` if this position is a derivative (option, future,
1599    /// future option, or warrant) and has the required fields populated. Returns `None`
1600    /// for non-derivative positions or if required fields are missing.
1601    ///
1602    /// # Example
1603    /// ```
1604    /// # use ib_flex::types::{Position, AssetCategory, PutCall, DerivativeInfo};
1605    /// # use rust_decimal::Decimal;
1606    /// # use chrono::NaiveDate;
1607    /// # let mut position = Position {
1608    /// #     account_id: Some("U1234567".to_string()),
1609    /// #     conid: Some("12345".to_string()),
1610    /// #     symbol: Some("AAPL".to_string()),
1611    /// #     description: None,
1612    /// #     asset_category: Some(AssetCategory::Option),
1613    /// #     cusip: None,
1614    /// #     isin: None,
1615    /// #     figi: None,
1616    /// #     security_id: None,
1617    /// #     security_id_type: None,
1618    /// #     multiplier: Some(Decimal::new(100, 0)),
1619    /// #     strike: Some(Decimal::new(150, 0)),
1620    /// #     expiry: Some(NaiveDate::from_ymd_opt(2024, 12, 20).unwrap()),
1621    /// #     put_call: Some(PutCall::Call),
1622    /// #     underlying_conid: Some("67890".to_string()),
1623    /// #     underlying_symbol: Some("AAPL".to_string()),
1624    /// #     quantity: Some(Decimal::new(10, 0)),
1625    /// #     mark_price: Some(Decimal::new(5, 0)),
1626    /// #     position_value: Some(Decimal::new(5000, 0)),
1627    /// #     side: Some("Long".to_string()),
1628    /// #     open_price: None,
1629    /// #     cost_basis_price: None,
1630    /// #     cost_basis_money: None,
1631    /// #     fifo_pnl_unrealized: None,
1632    /// #     percent_of_nav: None,
1633    /// #     currency: Some("USD".to_string()),
1634    /// #     fx_rate_to_base: None,
1635    /// #     report_date: Some(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap()),
1636    /// #     holding_period_date_time: None,
1637    /// #     open_date_time: None,
1638    /// #     originating_transaction_id: None,
1639    /// #     code: None,
1640    /// #     originating_order_id: None,
1641    /// #     issuer: None,
1642    /// #     issuer_country_code: None,
1643    /// #     sub_category: None,
1644    /// #     listing_exchange: None,
1645    /// #     underlying_listing_exchange: None,
1646    /// #     underlying_security_id: None,
1647    /// #     accrued_int: None,
1648    /// #     principal_adjust_factor: None,
1649    /// #     serial_number: None,
1650    /// #     delivery_type: None,
1651    /// #     commodity_type: None,
1652    /// #     fineness: None,
1653    /// #     weight: None,
1654    /// #     level_of_detail: None,
1655    /// #     model: None,
1656    /// #     acct_alias: None,
1657    /// #     vesting_date: None,
1658    /// #     sedol: None,
1659    /// #     position_value_in_base: None,
1660    /// #     unrealized_capital_gains_pnl: None,
1661    /// #     unrealizedl_fx_pnl: None,
1662    /// # };
1663    /// if let Some(derivative) = position.derivative() {
1664    ///     match derivative {
1665    ///         DerivativeInfo::Option { strike, expiry, put_call, .. } => {
1666    ///             println!("Option: Strike={}, Expiry={}, Type={:?}", strike, expiry, put_call);
1667    ///         }
1668    ///         _ => {}
1669    ///     }
1670    /// }
1671    /// ```
1672    pub fn derivative(&self) -> Option<DerivativeInfo> {
1673        match self.asset_category? {
1674            AssetCategory::Option => {
1675                // For options, we need: strike, expiry, put_call, underlying_symbol
1676                let strike = self.strike?;
1677                let expiry = self.expiry?;
1678                let put_call = self.put_call?;
1679                let underlying_symbol = self.underlying_symbol.clone()?;
1680
1681                Some(DerivativeInfo::Option {
1682                    strike,
1683                    expiry,
1684                    put_call,
1685                    underlying_symbol,
1686                    underlying_conid: self.underlying_conid.clone(),
1687                })
1688            }
1689            AssetCategory::Future => {
1690                // For futures, we need: expiry, underlying_symbol
1691                let expiry = self.expiry?;
1692                let underlying_symbol = self.underlying_symbol.clone()?;
1693
1694                Some(DerivativeInfo::Future {
1695                    expiry,
1696                    underlying_symbol,
1697                    underlying_conid: self.underlying_conid.clone(),
1698                })
1699            }
1700            AssetCategory::FutureOption => {
1701                // For future options, we need: strike, expiry, put_call, underlying_symbol
1702                let strike = self.strike?;
1703                let expiry = self.expiry?;
1704                let put_call = self.put_call?;
1705                let underlying_symbol = self.underlying_symbol.clone()?;
1706
1707                Some(DerivativeInfo::FutureOption {
1708                    strike,
1709                    expiry,
1710                    put_call,
1711                    underlying_symbol,
1712                    underlying_conid: self.underlying_conid.clone(),
1713                })
1714            }
1715            AssetCategory::Warrant => {
1716                // For warrants, all fields are optional but we need at least underlying_symbol
1717                let underlying_symbol = self.underlying_symbol.clone()?;
1718
1719                Some(DerivativeInfo::Warrant {
1720                    strike: self.strike,
1721                    expiry: self.expiry,
1722                    underlying_symbol: Some(underlying_symbol),
1723                })
1724            }
1725            // Not a derivative type
1726            _ => None,
1727        }
1728    }
1729}
1730
1731/// A cash transaction (deposit, withdrawal, dividend, interest, fee)
1732///
1733/// Represents any cash flow that affects your account balance: deposits,
1734/// withdrawals, dividends, interest payments, withholding taxes, and fees.
1735/// Fields are organized into CORE and EXTENDED sections.
1736///
1737/// # Example
1738/// ```no_run
1739/// use ib_flex::parse_activity_flex;
1740/// use rust_decimal::Decimal;
1741///
1742/// let xml = std::fs::read_to_string("activity.xml")?;
1743/// let statement = parse_activity_flex(&xml)?;
1744///
1745/// // Categorize cash flows
1746/// let mut dividends = Decimal::ZERO;
1747/// let mut interest = Decimal::ZERO;
1748/// let mut fees = Decimal::ZERO;
1749///
1750/// for cash_txn in &statement.cash_transactions.items {
1751///     match cash_txn.transaction_type.as_deref() {
1752///         Some("Dividends") => {
1753///             dividends += cash_txn.amount.unwrap_or_default();
1754///             println!("Dividend from {}: {}",
1755///                 cash_txn.symbol.as_ref().unwrap_or(&"N/A".to_string()),
1756///                 cash_txn.amount.unwrap_or_default()
1757///             );
1758///         }
1759///         Some("Broker Interest Paid") | Some("Broker Interest Received") => {
1760///             interest += cash_txn.amount.unwrap_or_default();
1761///         }
1762///         Some("Other Fees") | Some("Commission Adjustments") => {
1763///             fees += cash_txn.amount.unwrap_or_default();
1764///         }
1765///         _ => {
1766///             println!("{:?}: {}", cash_txn.transaction_type, cash_txn.amount.unwrap_or_default());
1767///         }
1768///     }
1769/// }
1770///
1771/// println!("\nTotals:");
1772/// println!("  Dividends: {}", dividends);
1773/// println!("  Interest: {}", interest);
1774/// println!("  Fees: {}", fees);
1775/// # Ok::<(), Box<dyn std::error::Error>>(())
1776/// ```
1777#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1778pub struct CashTransaction {
1779    // ==================== CORE FIELDS ====================
1780    // Essential for tax reporting and portfolio analytics
1781
1782    // --- Account ---
1783    /// IB account number
1784    #[serde(
1785        rename = "@accountId",
1786        default,
1787        deserialize_with = "deserialize_optional_string"
1788    )]
1789    pub account_id: Option<String>,
1790
1791    /// IB transaction ID
1792    #[serde(rename = "@transactionID", default)]
1793    pub transaction_id: Option<String>,
1794
1795    // --- Transaction Details ---
1796    /// Transaction type (Deposits, Dividends, WithholdingTax, BrokerInterest, etc.)
1797    #[serde(rename = "@type", default)]
1798    pub transaction_type: Option<String>,
1799
1800    /// Description of transaction
1801    #[serde(rename = "@description", default)]
1802    pub description: Option<String>,
1803
1804    /// Amount (positive for credits, negative for debits)
1805    #[serde(
1806        rename = "@amount",
1807        default,
1808        deserialize_with = "deserialize_optional_decimal"
1809    )]
1810    pub amount: Option<Decimal>,
1811
1812    /// Currency
1813    #[serde(
1814        rename = "@currency",
1815        default,
1816        deserialize_with = "deserialize_optional_string"
1817    )]
1818    pub currency: Option<String>,
1819
1820    /// FX rate to base currency
1821    #[serde(
1822        rename = "@fxRateToBase",
1823        default,
1824        deserialize_with = "deserialize_optional_decimal"
1825    )]
1826    pub fx_rate_to_base: Option<Decimal>,
1827
1828    // --- Dates ---
1829    /// Transaction date
1830    #[serde(
1831        rename = "@date",
1832        default,
1833        deserialize_with = "deserialize_optional_date"
1834    )]
1835    pub date: Option<NaiveDate>,
1836
1837    /// Settlement date
1838    #[serde(
1839        rename = "@settleDate",
1840        default,
1841        deserialize_with = "deserialize_optional_date"
1842    )]
1843    pub settle_date: Option<NaiveDate>,
1844
1845    /// Ex-dividend date (tax-critical for dividends)
1846    #[serde(
1847        rename = "@exDate",
1848        default,
1849        deserialize_with = "deserialize_optional_date"
1850    )]
1851    pub ex_date: Option<NaiveDate>,
1852
1853    // --- Security Identification ---
1854    /// Related security's contract ID (for dividends)
1855    #[serde(rename = "@conid", default)]
1856    pub conid: Option<String>,
1857
1858    /// Related security's symbol
1859    #[serde(rename = "@symbol", default)]
1860    pub symbol: Option<String>,
1861
1862    /// Asset category
1863    #[serde(rename = "@assetCategory", default)]
1864    pub asset_category: Option<AssetCategory>,
1865
1866    /// CUSIP
1867    #[serde(rename = "@cusip", default)]
1868    pub cusip: Option<String>,
1869
1870    /// ISIN
1871    #[serde(rename = "@isin", default)]
1872    pub isin: Option<String>,
1873
1874    /// FIGI
1875    #[serde(rename = "@figi", default)]
1876    pub figi: Option<String>,
1877
1878    /// Security ID
1879    #[serde(rename = "@securityID", default)]
1880    pub security_id: Option<String>,
1881
1882    /// Security ID type
1883    #[serde(rename = "@securityIDType", default)]
1884    pub security_id_type: Option<SecurityIdType>,
1885
1886    // --- Derivatives ---
1887    /// Contract multiplier
1888    #[serde(
1889        rename = "@multiplier",
1890        default,
1891        deserialize_with = "deserialize_optional_decimal"
1892    )]
1893    pub multiplier: Option<Decimal>,
1894
1895    /// Strike price
1896    #[serde(
1897        rename = "@strike",
1898        default,
1899        deserialize_with = "deserialize_optional_decimal"
1900    )]
1901    pub strike: Option<Decimal>,
1902
1903    /// Expiry date
1904    #[serde(
1905        rename = "@expiry",
1906        default,
1907        deserialize_with = "deserialize_optional_date"
1908    )]
1909    pub expiry: Option<NaiveDate>,
1910
1911    /// Put or Call
1912    #[serde(rename = "@putCall", default)]
1913    pub put_call: Option<PutCall>,
1914
1915    /// Underlying contract ID
1916    #[serde(rename = "@underlyingConid", default)]
1917    pub underlying_conid: Option<String>,
1918
1919    /// Underlying symbol
1920    #[serde(rename = "@underlyingSymbol", default)]
1921    pub underlying_symbol: Option<String>,
1922
1923    /// Transaction code (tax-relevant codes)
1924    #[serde(rename = "@code", default)]
1925    pub code: Option<String>,
1926
1927    // ==================== EXTENDED FIELDS ====================
1928    // Metadata and less commonly used fields
1929
1930    // --- Timestamps ---
1931    /// Transaction datetime
1932    #[serde(rename = "@dateTime", default)]
1933    pub date_time: Option<String>,
1934
1935    /// Report date
1936    #[serde(
1937        rename = "@reportDate",
1938        default,
1939        deserialize_with = "deserialize_optional_date"
1940    )]
1941    pub report_date: Option<NaiveDate>,
1942
1943    /// Available for trading date
1944    #[serde(
1945        rename = "@availableForTradingDate",
1946        default,
1947        deserialize_with = "deserialize_optional_date"
1948    )]
1949    pub available_for_trading_date: Option<NaiveDate>,
1950
1951    // --- Extended IDs ---
1952    /// Action ID
1953    #[serde(rename = "@actionID", default)]
1954    pub action_id: Option<String>,
1955
1956    /// Trade ID (for dividend/interest related to specific trade)
1957    #[serde(rename = "@tradeID", default)]
1958    pub trade_id: Option<String>,
1959
1960    /// Client reference
1961    #[serde(rename = "@clientReference", default)]
1962    pub client_reference: Option<String>,
1963
1964    // --- Issuer/Security Metadata ---
1965    /// Issuer
1966    #[serde(rename = "@issuer", default)]
1967    pub issuer: Option<String>,
1968
1969    /// Issuer country code
1970    #[serde(rename = "@issuerCountryCode", default)]
1971    pub issuer_country_code: Option<String>,
1972
1973    /// Sub-category
1974    #[serde(rename = "@subCategory", default)]
1975    pub sub_category: Option<SubCategory>,
1976
1977    /// Listing exchange
1978    #[serde(rename = "@listingExchange", default)]
1979    pub listing_exchange: Option<String>,
1980
1981    // --- Underlying Extended ---
1982    /// Underlying listing exchange
1983    #[serde(rename = "@underlyingListingExchange", default)]
1984    pub underlying_listing_exchange: Option<String>,
1985
1986    /// Underlying security ID
1987    #[serde(rename = "@underlyingSecurityID", default)]
1988    pub underlying_security_id: Option<String>,
1989
1990    // --- Bond Fields ---
1991    /// Principal adjust factor
1992    #[serde(
1993        rename = "@principalAdjustFactor",
1994        default,
1995        deserialize_with = "deserialize_optional_decimal"
1996    )]
1997    pub principal_adjust_factor: Option<Decimal>,
1998
1999    // --- Commodity/Physical Delivery ---
2000    /// Serial number
2001    #[serde(rename = "@serialNumber", default)]
2002    pub serial_number: Option<String>,
2003
2004    /// Delivery type
2005    #[serde(rename = "@deliveryType", default)]
2006    pub delivery_type: Option<String>,
2007
2008    /// Commodity type
2009    #[serde(rename = "@commodityType", default)]
2010    pub commodity_type: Option<String>,
2011
2012    /// Fineness
2013    #[serde(
2014        rename = "@fineness",
2015        default,
2016        deserialize_with = "deserialize_optional_decimal"
2017    )]
2018    pub fineness: Option<Decimal>,
2019
2020    /// Weight
2021    #[serde(rename = "@weight", default)]
2022    pub weight: Option<String>,
2023
2024    // --- Other Metadata ---
2025    /// Level of detail
2026    #[serde(rename = "@levelOfDetail", default)]
2027    pub level_of_detail: Option<String>,
2028
2029    /// Model
2030    #[serde(rename = "@model", default)]
2031    pub model: Option<String>,
2032
2033    /// Account alias
2034    #[serde(rename = "@acctAlias", default)]
2035    pub acct_alias: Option<String>,
2036
2037    /// `sedol` (added for full ibflex coverage)
2038    #[serde(
2039        rename = "@sedol",
2040        default,
2041        deserialize_with = "deserialize_optional_string"
2042    )]
2043    pub sedol: Option<String>,
2044}
2045
2046/// A corporate action (split, merger, spinoff, etc.)
2047///
2048/// Represents corporate events that affect your holdings: stock splits,
2049/// reverse splits, mergers, spinoffs, tender offers, bond conversions, etc.
2050/// Fields are organized into CORE and EXTENDED sections.
2051///
2052/// # Example
2053/// ```no_run
2054/// use ib_flex::parse_activity_flex;
2055///
2056/// let xml = std::fs::read_to_string("activity.xml")?;
2057/// let statement = parse_activity_flex(&xml)?;
2058///
2059/// for action in &statement.corporate_actions.items {
2060///     println!("{:?}: {:?}", action.symbol, action.description);
2061///     println!("  Type: {:?}", action.action_type);
2062///
2063///     // Check action type
2064///     match action.action_type.as_deref() {
2065///         Some("FS") => println!("  Forward stock split"),
2066///         Some("RS") => println!("  Reverse stock split"),
2067///         Some("SO") => println!("  Spinoff"),
2068///         Some("TO") => println!("  Tender offer"),
2069///         Some("TC") => println!("  Treasury bill/bond maturity"),
2070///         Some("BC") => println!("  Bond conversion"),
2071///         _ => println!("  Other: {:?}", action.action_type),
2072///     }
2073///
2074///     // Show quantities and proceeds
2075///     if let Some(qty) = action.quantity {
2076///         println!("  Quantity: {}", qty);
2077///     }
2078///     if let Some(proceeds) = action.proceeds {
2079///         println!("  Proceeds: {}", proceeds);
2080///     }
2081///
2082///     // Show realized P&L if applicable
2083///     if let Some(pnl) = action.fifo_pnl_realized {
2084///         println!("  Realized P&L: {}", pnl);
2085///     }
2086/// }
2087/// # Ok::<(), Box<dyn std::error::Error>>(())
2088/// ```
2089#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
2090pub struct CorporateAction {
2091    // ==================== CORE FIELDS ====================
2092    // Essential for tax reporting and portfolio analytics
2093
2094    // --- Account ---
2095    /// IB account number
2096    #[serde(
2097        rename = "@accountId",
2098        default,
2099        deserialize_with = "deserialize_optional_string"
2100    )]
2101    pub account_id: Option<String>,
2102
2103    /// IB transaction ID
2104    #[serde(rename = "@transactionID", default)]
2105    pub transaction_id: Option<String>,
2106
2107    // --- Action Details ---
2108    /// Action type (Split, Merger, Spinoff, etc.)
2109    #[serde(rename = "@type", default)]
2110    pub action_type: Option<String>,
2111
2112    /// Description of corporate action
2113    #[serde(rename = "@description", default)]
2114    pub description: Option<String>,
2115
2116    // --- Dates (Tax-critical) ---
2117    /// Action date
2118    #[serde(
2119        rename = "@date",
2120        default,
2121        deserialize_with = "deserialize_optional_date"
2122    )]
2123    pub action_date: Option<NaiveDate>,
2124
2125    /// Report date
2126    #[serde(
2127        rename = "@reportDate",
2128        default,
2129        deserialize_with = "deserialize_optional_date"
2130    )]
2131    pub report_date: Option<NaiveDate>,
2132
2133    /// Ex-date (ex-dividend date for dividends)
2134    #[serde(
2135        rename = "@exDate",
2136        default,
2137        deserialize_with = "deserialize_optional_date"
2138    )]
2139    pub ex_date: Option<NaiveDate>,
2140
2141    /// Pay date
2142    #[serde(
2143        rename = "@payDate",
2144        default,
2145        deserialize_with = "deserialize_optional_date"
2146    )]
2147    pub pay_date: Option<NaiveDate>,
2148
2149    /// Record date
2150    #[serde(
2151        rename = "@recordDate",
2152        default,
2153        deserialize_with = "deserialize_optional_date"
2154    )]
2155    pub record_date: Option<NaiveDate>,
2156
2157    // --- Security Identification ---
2158    /// IB contract ID
2159    #[serde(
2160        rename = "@conid",
2161        default,
2162        deserialize_with = "deserialize_optional_string"
2163    )]
2164    pub conid: Option<String>,
2165
2166    /// Ticker symbol
2167    #[serde(
2168        rename = "@symbol",
2169        default,
2170        deserialize_with = "deserialize_optional_string"
2171    )]
2172    pub symbol: Option<String>,
2173
2174    /// Asset category
2175    #[serde(rename = "@assetCategory", default)]
2176    pub asset_category: Option<AssetCategory>,
2177
2178    /// CUSIP
2179    #[serde(rename = "@cusip", default)]
2180    pub cusip: Option<String>,
2181
2182    /// ISIN
2183    #[serde(rename = "@isin", default)]
2184    pub isin: Option<String>,
2185
2186    /// FIGI
2187    #[serde(rename = "@figi", default)]
2188    pub figi: Option<String>,
2189
2190    /// Security ID
2191    #[serde(rename = "@securityID", default)]
2192    pub security_id: Option<String>,
2193
2194    /// Security ID type
2195    #[serde(rename = "@securityIDType", default)]
2196    pub security_id_type: Option<SecurityIdType>,
2197
2198    // --- Derivatives ---
2199    /// Contract multiplier
2200    #[serde(
2201        rename = "@multiplier",
2202        default,
2203        deserialize_with = "deserialize_optional_decimal"
2204    )]
2205    pub multiplier: Option<Decimal>,
2206
2207    /// Strike price (for options)
2208    #[serde(
2209        rename = "@strike",
2210        default,
2211        deserialize_with = "deserialize_optional_decimal"
2212    )]
2213    pub strike: Option<Decimal>,
2214
2215    /// Expiry date (for options/futures)
2216    #[serde(
2217        rename = "@expiry",
2218        default,
2219        deserialize_with = "deserialize_optional_date"
2220    )]
2221    pub expiry: Option<NaiveDate>,
2222
2223    /// Put or Call
2224    #[serde(rename = "@putCall", default)]
2225    pub put_call: Option<PutCall>,
2226
2227    /// Underlying contract ID
2228    #[serde(rename = "@underlyingConid", default)]
2229    pub underlying_conid: Option<String>,
2230
2231    /// Underlying symbol
2232    #[serde(rename = "@underlyingSymbol", default)]
2233    pub underlying_symbol: Option<String>,
2234
2235    // --- Quantities and Values ---
2236    /// Quantity affected
2237    #[serde(
2238        rename = "@quantity",
2239        default,
2240        deserialize_with = "deserialize_optional_decimal"
2241    )]
2242    pub quantity: Option<Decimal>,
2243
2244    /// Amount
2245    #[serde(
2246        rename = "@amount",
2247        default,
2248        deserialize_with = "deserialize_optional_decimal"
2249    )]
2250    pub amount: Option<Decimal>,
2251
2252    /// Proceeds (if any)
2253    #[serde(
2254        rename = "@proceeds",
2255        default,
2256        deserialize_with = "deserialize_optional_decimal"
2257    )]
2258    pub proceeds: Option<Decimal>,
2259
2260    /// Value (if any)
2261    #[serde(
2262        rename = "@value",
2263        default,
2264        deserialize_with = "deserialize_optional_decimal"
2265    )]
2266    pub value: Option<Decimal>,
2267
2268    /// Cost
2269    #[serde(
2270        rename = "@cost",
2271        default,
2272        deserialize_with = "deserialize_optional_decimal"
2273    )]
2274    pub cost: Option<Decimal>,
2275
2276    // --- P&L ---
2277    /// FIFO P&L realized
2278    #[serde(
2279        rename = "@fifoPnlRealized",
2280        default,
2281        deserialize_with = "deserialize_optional_decimal"
2282    )]
2283    pub fifo_pnl_realized: Option<Decimal>,
2284
2285    /// Mark-to-market P&L
2286    #[serde(
2287        rename = "@mtmPnl",
2288        default,
2289        deserialize_with = "deserialize_optional_decimal"
2290    )]
2291    pub mtm_pnl: Option<Decimal>,
2292
2293    // --- Currency ---
2294    /// Currency
2295    #[serde(rename = "@currency", default)]
2296    pub currency: Option<String>,
2297
2298    /// FX rate to base
2299    #[serde(
2300        rename = "@fxRateToBase",
2301        default,
2302        deserialize_with = "deserialize_optional_decimal"
2303    )]
2304    pub fx_rate_to_base: Option<Decimal>,
2305
2306    /// Code (may contain tax-relevant info)
2307    #[serde(rename = "@code", default)]
2308    pub code: Option<String>,
2309
2310    // ==================== EXTENDED FIELDS ====================
2311    // Metadata and less commonly used fields
2312
2313    // --- Extended IDs ---
2314    /// Action ID
2315    #[serde(rename = "@actionID", default)]
2316    pub action_id: Option<String>,
2317
2318    // --- Timestamps ---
2319    /// Action datetime
2320    #[serde(rename = "@dateTime", default)]
2321    pub date_time: Option<String>,
2322
2323    // --- Issuer/Security Metadata ---
2324    /// Issuer
2325    #[serde(rename = "@issuer", default)]
2326    pub issuer: Option<String>,
2327
2328    /// Issuer country code
2329    #[serde(rename = "@issuerCountryCode", default)]
2330    pub issuer_country_code: Option<String>,
2331
2332    /// Sub-category
2333    #[serde(rename = "@subCategory", default)]
2334    pub sub_category: Option<SubCategory>,
2335
2336    /// Listing exchange
2337    #[serde(rename = "@listingExchange", default)]
2338    pub listing_exchange: Option<String>,
2339
2340    // --- Underlying Extended ---
2341    /// Underlying listing exchange
2342    #[serde(rename = "@underlyingListingExchange", default)]
2343    pub underlying_listing_exchange: Option<String>,
2344
2345    /// Underlying security ID
2346    #[serde(rename = "@underlyingSecurityID", default)]
2347    pub underlying_security_id: Option<String>,
2348
2349    // --- Bond Fields ---
2350    /// Accrued interest
2351    #[serde(
2352        rename = "@accruedInt",
2353        default,
2354        deserialize_with = "deserialize_optional_decimal"
2355    )]
2356    pub accrued_int: Option<Decimal>,
2357
2358    /// Principal adjust factor
2359    #[serde(
2360        rename = "@principalAdjustFactor",
2361        default,
2362        deserialize_with = "deserialize_optional_decimal"
2363    )]
2364    pub principal_adjust_factor: Option<Decimal>,
2365
2366    // --- Commodity/Physical Delivery ---
2367    /// Serial number
2368    #[serde(rename = "@serialNumber", default)]
2369    pub serial_number: Option<String>,
2370
2371    /// Delivery type
2372    #[serde(rename = "@deliveryType", default)]
2373    pub delivery_type: Option<String>,
2374
2375    /// Commodity type
2376    #[serde(rename = "@commodityType", default)]
2377    pub commodity_type: Option<String>,
2378
2379    /// Fineness (for precious metals)
2380    #[serde(
2381        rename = "@fineness",
2382        default,
2383        deserialize_with = "deserialize_optional_decimal"
2384    )]
2385    pub fineness: Option<Decimal>,
2386
2387    /// Weight
2388    #[serde(rename = "@weight", default)]
2389    pub weight: Option<String>,
2390
2391    // --- Other Metadata ---
2392    /// Level of detail
2393    #[serde(rename = "@levelOfDetail", default)]
2394    pub level_of_detail: Option<String>,
2395
2396    /// Model (for model portfolios)
2397    #[serde(rename = "@model", default)]
2398    pub model: Option<String>,
2399
2400    /// Account alias
2401    #[serde(rename = "@acctAlias", default)]
2402    pub acct_alias: Option<String>,
2403
2404    /// `actionDescription` (added for full ibflex coverage)
2405    #[serde(
2406        rename = "@actionDescription",
2407        default,
2408        deserialize_with = "deserialize_optional_string"
2409    )]
2410    pub action_description: Option<String>,
2411
2412    /// `capitalGainsPnl` (added for full ibflex coverage)
2413    #[serde(
2414        rename = "@capitalGainsPnl",
2415        default,
2416        deserialize_with = "deserialize_optional_decimal"
2417    )]
2418    pub capital_gains_pnl: Option<Decimal>,
2419
2420    /// `fxPnl` (added for full ibflex coverage)
2421    #[serde(
2422        rename = "@fxPnl",
2423        default,
2424        deserialize_with = "deserialize_optional_decimal"
2425    )]
2426    pub fx_pnl: Option<Decimal>,
2427
2428    /// `sedol` (added for full ibflex coverage)
2429    #[serde(
2430        rename = "@sedol",
2431        default,
2432        deserialize_with = "deserialize_optional_string"
2433    )]
2434    pub sedol: Option<String>,
2435
2436    /// `costBasis` (added for full ibflex coverage)
2437    #[serde(
2438        rename = "@costBasis",
2439        default,
2440        deserialize_with = "deserialize_optional_decimal"
2441    )]
2442    pub cost_basis: Option<Decimal>,
2443}
2444
2445/// Security information (reference data)
2446///
2447/// Provides detailed reference data for securities in the statement.
2448/// Includes identifiers (CUSIP, ISIN, FIGI), exchange info, and derivative details.
2449/// Fields are organized into CORE and EXTENDED sections.
2450///
2451/// # Example
2452/// ```no_run
2453/// use ib_flex::parse_activity_flex;
2454/// use ib_flex::AssetCategory;
2455///
2456/// let xml = std::fs::read_to_string("activity.xml")?;
2457/// let statement = parse_activity_flex(&xml)?;
2458///
2459/// for security in &statement.securities_info.items {
2460///     println!("{:?} ({:?})", security.symbol, security.conid);
2461///
2462///     // Print description
2463///     if let Some(desc) = &security.description {
2464///         println!("  Description: {}", desc);
2465///     }
2466///
2467///     // Print identifiers
2468///     if let Some(cusip) = &security.cusip {
2469///         println!("  CUSIP: {}", cusip);
2470///     }
2471///     if let Some(isin) = &security.isin {
2472///         println!("  ISIN: {}", isin);
2473///     }
2474///
2475///     // Show derivative info for options
2476///     if security.asset_category == Some(AssetCategory::Option) {
2477///         println!("  Underlying: {:?}", security.underlying_symbol);
2478///         println!("  Strike: {:?}", security.strike);
2479///         println!("  Expiry: {:?}", security.expiry);
2480///         println!("  Type: {:?}", security.put_call);
2481///         println!("  Multiplier: {:?}", security.multiplier);
2482///     }
2483/// }
2484/// # Ok::<(), Box<dyn std::error::Error>>(())
2485/// ```
2486#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
2487pub struct SecurityInfo {
2488    // ==================== CORE FIELDS ====================
2489    // Essential for tax reporting and portfolio analytics
2490
2491    // --- Security Identification ---
2492    /// Asset category
2493    #[serde(rename = "@assetCategory", default)]
2494    pub asset_category: Option<AssetCategory>,
2495
2496    /// Ticker symbol
2497    #[serde(
2498        rename = "@symbol",
2499        default,
2500        deserialize_with = "deserialize_optional_string"
2501    )]
2502    pub symbol: Option<String>,
2503
2504    /// Security description
2505    #[serde(rename = "@description", default)]
2506    pub description: Option<String>,
2507
2508    /// IB contract ID
2509    #[serde(
2510        rename = "@conid",
2511        default,
2512        deserialize_with = "deserialize_optional_string"
2513    )]
2514    pub conid: Option<String>,
2515
2516    /// Security ID
2517    #[serde(rename = "@securityID", default)]
2518    pub security_id: Option<String>,
2519
2520    /// Security ID type
2521    #[serde(rename = "@securityIDType", default)]
2522    pub security_id_type: Option<SecurityIdType>,
2523
2524    /// CUSIP
2525    #[serde(rename = "@cusip", default)]
2526    pub cusip: Option<String>,
2527
2528    /// ISIN
2529    #[serde(rename = "@isin", default)]
2530    pub isin: Option<String>,
2531
2532    /// FIGI
2533    #[serde(rename = "@figi", default)]
2534    pub figi: Option<String>,
2535
2536    /// SEDOL
2537    #[serde(rename = "@sedol", default)]
2538    pub sedol: Option<String>,
2539
2540    // --- Derivatives (Options/Futures) ---
2541    /// Multiplier
2542    #[serde(
2543        rename = "@multiplier",
2544        default,
2545        deserialize_with = "deserialize_optional_decimal"
2546    )]
2547    pub multiplier: Option<Decimal>,
2548
2549    /// Strike (for options)
2550    #[serde(
2551        rename = "@strike",
2552        default,
2553        deserialize_with = "deserialize_optional_decimal"
2554    )]
2555    pub strike: Option<Decimal>,
2556
2557    /// Expiry (for options/futures)
2558    #[serde(
2559        rename = "@expiry",
2560        default,
2561        deserialize_with = "deserialize_optional_date"
2562    )]
2563    pub expiry: Option<NaiveDate>,
2564
2565    /// Put or Call
2566    #[serde(rename = "@putCall", default)]
2567    pub put_call: Option<PutCall>,
2568
2569    /// Underlying contract ID
2570    #[serde(rename = "@underlyingConid", default)]
2571    pub underlying_conid: Option<String>,
2572
2573    /// Underlying symbol
2574    #[serde(rename = "@underlyingSymbol", default)]
2575    pub underlying_symbol: Option<String>,
2576
2577    // --- Bond/Fixed Income ---
2578    /// Maturity date (for bonds)
2579    #[serde(
2580        rename = "@maturity",
2581        default,
2582        deserialize_with = "deserialize_optional_date"
2583    )]
2584    pub maturity: Option<NaiveDate>,
2585
2586    /// Principal adjustment factor
2587    #[serde(
2588        rename = "@principalAdjustFactor",
2589        default,
2590        deserialize_with = "deserialize_optional_decimal"
2591    )]
2592    pub principal_adjust_factor: Option<Decimal>,
2593
2594    // --- Currency ---
2595    /// Currency
2596    #[serde(rename = "@currency", default)]
2597    pub currency: Option<String>,
2598
2599    // ==================== EXTENDED FIELDS ====================
2600    // Metadata and less commonly used fields
2601
2602    // --- Exchange Info ---
2603    /// Listing exchange
2604    #[serde(rename = "@listingExchange", default)]
2605    pub listing_exchange: Option<String>,
2606
2607    /// Underlying security ID
2608    #[serde(rename = "@underlyingSecurityID", default)]
2609    pub underlying_security_id: Option<String>,
2610
2611    /// Underlying listing exchange
2612    #[serde(rename = "@underlyingListingExchange", default)]
2613    pub underlying_listing_exchange: Option<String>,
2614
2615    // --- Issuer/Security Metadata ---
2616    /// Issuer
2617    #[serde(rename = "@issuer", default)]
2618    pub issuer: Option<String>,
2619
2620    /// Issuer country code
2621    #[serde(rename = "@issuerCountryCode", default)]
2622    pub issuer_country_code: Option<String>,
2623
2624    /// Sub-category
2625    #[serde(rename = "@subCategory", default)]
2626    pub sub_category: Option<SubCategory>,
2627
2628    // --- Futures ---
2629    /// Delivery month (for futures)
2630    #[serde(rename = "@deliveryMonth", default)]
2631    pub delivery_month: Option<String>,
2632
2633    // --- Commodity/Physical Delivery ---
2634    /// Serial number
2635    #[serde(rename = "@serialNumber", default)]
2636    pub serial_number: Option<String>,
2637
2638    /// Delivery type
2639    #[serde(rename = "@deliveryType", default)]
2640    pub delivery_type: Option<String>,
2641
2642    /// Commodity type
2643    #[serde(rename = "@commodityType", default)]
2644    pub commodity_type: Option<String>,
2645
2646    /// Fineness (for precious metals)
2647    #[serde(
2648        rename = "@fineness",
2649        default,
2650        deserialize_with = "deserialize_optional_decimal"
2651    )]
2652    pub fineness: Option<Decimal>,
2653
2654    /// Weight
2655    #[serde(rename = "@weight", default)]
2656    pub weight: Option<String>,
2657
2658    // --- Other ---
2659    /// Code
2660    #[serde(rename = "@code", default)]
2661    pub code: Option<String>,
2662
2663    /// `underlyingCategory` (added for full ibflex coverage)
2664    #[serde(
2665        rename = "@underlyingCategory",
2666        default,
2667        deserialize_with = "deserialize_optional_string"
2668    )]
2669    pub underlying_category: Option<String>,
2670
2671    /// `issueDate` (added for full ibflex coverage)
2672    #[serde(
2673        rename = "@issueDate",
2674        default,
2675        deserialize_with = "deserialize_optional_date"
2676    )]
2677    pub issue_date: Option<NaiveDate>,
2678
2679    /// `type` (added for full ibflex coverage)
2680    #[serde(
2681        rename = "@type",
2682        default,
2683        deserialize_with = "deserialize_optional_string"
2684    )]
2685    pub r#type: Option<String>,
2686
2687    /// `settlementPolicyMethod` (added for full ibflex coverage)
2688    #[serde(
2689        rename = "@settlementPolicyMethod",
2690        default,
2691        deserialize_with = "deserialize_optional_string"
2692    )]
2693    pub settlement_policy_method: Option<String>,
2694
2695    /// `relatedTradeID` (added for full ibflex coverage)
2696    #[serde(
2697        rename = "@relatedTradeID",
2698        default,
2699        deserialize_with = "deserialize_optional_string"
2700    )]
2701    pub related_trade_id: Option<String>,
2702
2703    /// `origTransactionID` (added for full ibflex coverage)
2704    #[serde(
2705        rename = "@origTransactionID",
2706        default,
2707        deserialize_with = "deserialize_optional_string"
2708    )]
2709    pub orig_transaction_id: Option<String>,
2710
2711    /// `relatedTransactionID` (added for full ibflex coverage)
2712    #[serde(
2713        rename = "@relatedTransactionID",
2714        default,
2715        deserialize_with = "deserialize_optional_string"
2716    )]
2717    pub related_transaction_id: Option<String>,
2718
2719    /// `rtn` (added for full ibflex coverage)
2720    #[serde(
2721        rename = "@rtn",
2722        default,
2723        deserialize_with = "deserialize_optional_string"
2724    )]
2725    pub rtn: Option<String>,
2726
2727    /// `initialInvestment` (added for full ibflex coverage)
2728    #[serde(
2729        rename = "@initialInvestment",
2730        default,
2731        deserialize_with = "deserialize_optional_bool"
2732    )]
2733    pub initial_investment: Option<bool>,
2734}
2735
2736/// Foreign exchange conversion rate
2737///
2738/// Provides daily FX conversion rates for multi-currency accounts.
2739/// Used to convert foreign currency amounts to your base currency.
2740///
2741/// # Example
2742/// ```no_run
2743/// use ib_flex::parse_activity_flex;
2744/// use rust_decimal::Decimal;
2745///
2746/// let xml = std::fs::read_to_string("activity.xml")?;
2747/// let statement = parse_activity_flex(&xml)?;
2748///
2749/// // Find conversion rate for a specific currency pair
2750/// let eur_to_usd = statement.conversion_rates.items
2751///     .iter()
2752///     .find(|r| r.from_currency.as_deref() == Some("EUR")
2753///         && r.to_currency.as_deref() == Some("USD"));
2754///
2755/// if let Some(rate) = eur_to_usd {
2756///     println!("EUR/USD rate on {:?}: {:?}", rate.report_date, rate.rate);
2757///
2758///     // Convert 1000 EUR to USD
2759///     let eur_amount = Decimal::from(1000);
2760///     let usd_amount = eur_amount * rate.rate.unwrap_or_default();
2761///     println!("1000 EUR = {} USD", usd_amount);
2762/// }
2763///
2764/// // List all available rates
2765/// for rate in &statement.conversion_rates.items {
2766///     println!("{:?}/{:?}: {:?}", rate.from_currency, rate.to_currency, rate.rate);
2767/// }
2768/// # Ok::<(), Box<dyn std::error::Error>>(())
2769/// ```
2770#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
2771pub struct ConversionRate {
2772    /// Report date
2773    #[serde(
2774        rename = "@reportDate",
2775        default,
2776        deserialize_with = "deserialize_optional_date"
2777    )]
2778    pub report_date: Option<NaiveDate>,
2779
2780    /// From currency (source)
2781    #[serde(
2782        rename = "@fromCurrency",
2783        default,
2784        deserialize_with = "deserialize_optional_string"
2785    )]
2786    pub from_currency: Option<String>,
2787
2788    /// To currency (target)
2789    #[serde(
2790        rename = "@toCurrency",
2791        default,
2792        deserialize_with = "deserialize_optional_string"
2793    )]
2794    pub to_currency: Option<String>,
2795
2796    /// Exchange rate
2797    #[serde(
2798        rename = "@rate",
2799        default,
2800        deserialize_with = "deserialize_optional_decimal"
2801    )]
2802    pub rate: Option<Decimal>,
2803}
2804
2805/// Wrapper for securities info section
2806#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2807pub struct SecuritiesInfoWrapper {
2808    /// List of securities
2809    #[serde(rename = "SecurityInfo", default)]
2810    pub items: Vec<SecurityInfo>,
2811}
2812
2813/// Wrapper for conversion rates section
2814#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2815pub struct ConversionRatesWrapper {
2816    /// List of conversion rates
2817    #[serde(rename = "ConversionRate", default)]
2818    pub items: Vec<ConversionRate>,
2819}
2820
2821// Extended v0.2.0+ wrappers
2822
2823/// Wrapper for equity summary section
2824#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2825pub struct EquitySummaryWrapper {
2826    /// List of equity summaries
2827    #[serde(rename = "EquitySummaryByReportDateInBase", default)]
2828    pub items: Vec<super::extended::EquitySummaryByReportDateInBase>,
2829}
2830
2831/// Wrapper for cash report section
2832#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2833pub struct CashReportWrapper {
2834    /// List of cash reports
2835    #[serde(rename = "CashReportCurrency", default)]
2836    pub items: Vec<super::extended::CashReportCurrency>,
2837}
2838
2839/// Wrapper for trade confirmations section
2840#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2841pub struct TradeConfirmsWrapper {
2842    /// List of trade confirmations
2843    #[serde(rename = "TradeConfirm", default)]
2844    pub items: Vec<super::extended::TradeConfirm>,
2845}
2846
2847/// Wrapper for option EAE section
2848#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2849pub struct OptionEAEWrapper {
2850    /// List of option exercises/assignments/expirations
2851    #[serde(rename = "OptionEAE", default)]
2852    pub items: Vec<super::extended::OptionEAE>,
2853}
2854
2855/// Wrapper for FX transactions section
2856#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2857pub struct FxTransactionsWrapper {
2858    /// List of FX transactions
2859    #[serde(rename = "FxTransaction", default)]
2860    pub items: Vec<super::extended::FxTransaction>,
2861}
2862
2863/// Wrapper for change in dividend accruals section
2864#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2865pub struct ChangeInDividendAccrualsWrapper {
2866    /// List of dividend accrual changes
2867    #[serde(rename = "ChangeInDividendAccrual", default)]
2868    pub items: Vec<super::extended::ChangeInDividendAccrual>,
2869}
2870
2871/// Wrapper for open dividend accruals section
2872#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2873pub struct OpenDividendAccrualsWrapper {
2874    /// List of open dividend accruals
2875    #[serde(rename = "OpenDividendAccrual", default)]
2876    pub items: Vec<super::extended::OpenDividendAccrual>,
2877}
2878
2879/// Wrapper for interest accruals section
2880#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2881pub struct InterestAccrualsWrapper {
2882    /// List of interest accruals
2883    #[serde(rename = "InterestAccrualsCurrency", default)]
2884    pub items: Vec<super::extended::InterestAccrualsCurrency>,
2885}
2886
2887/// Wrapper for transfers section
2888#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2889pub struct TransfersWrapper {
2890    /// List of transfers
2891    #[serde(rename = "Transfer", default)]
2892    pub items: Vec<super::extended::Transfer>,
2893}
2894
2895// v0.3.0+ wrappers for performance and advanced features
2896
2897/// Wrapper for MTM performance summary section
2898#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2899pub struct MTMPerformanceSummaryWrapper {
2900    /// List of MTM performance summaries by underlying
2901    #[serde(rename = "MTMPerformanceSummaryUnderlying", default)]
2902    pub items: Vec<super::extended::MTMPerformanceSummaryUnderlying>,
2903}
2904
2905/// Wrapper for FIFO performance summary section
2906#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2907pub struct FIFOPerformanceSummaryWrapper {
2908    /// List of FIFO performance summaries by underlying
2909    #[serde(rename = "FIFOPerformanceSummaryUnderlying", default)]
2910    pub items: Vec<super::extended::FIFOPerformanceSummaryUnderlying>,
2911}
2912
2913/// Wrapper for MTD/YTD performance summary section
2914#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2915pub struct MTDYTDPerformanceSummaryWrapper {
2916    /// List of MTD/YTD performance summaries
2917    #[serde(rename = "MTDYTDPerformanceSummaryUnderlying", default)]
2918    pub items: Vec<super::extended::MTDYTDPerformanceSummary>,
2919}
2920
2921/// Wrapper for statement of funds section
2922#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2923pub struct StatementOfFundsWrapper {
2924    /// List of statement of funds lines
2925    #[serde(rename = "StatementOfFundsLine", default)]
2926    pub items: Vec<super::extended::StatementOfFundsLine>,
2927}
2928
2929/// Wrapper for change in position value section
2930#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2931pub struct ChangeInPositionValueWrapper {
2932    /// List of position value changes
2933    #[serde(rename = "ChangeInPositionValue", default)]
2934    pub items: Vec<super::extended::ChangeInPositionValue>,
2935}
2936
2937/// Wrapper for unbundled commission details section
2938#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2939pub struct UnbundledCommissionDetailWrapper {
2940    /// List of unbundled commission details
2941    #[serde(rename = "UnbundledCommissionDetail", default)]
2942    pub items: Vec<super::extended::UnbundledCommissionDetail>,
2943}
2944
2945/// Wrapper for client fees section
2946#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2947pub struct ClientFeesWrapper {
2948    /// List of client fees
2949    #[serde(rename = "ClientFee", default)]
2950    pub items: Vec<super::extended::ClientFee>,
2951}
2952
2953/// Wrapper for client fees detail section
2954#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2955pub struct ClientFeesDetailWrapper {
2956    /// List of client fee details
2957    #[serde(rename = "ClientFeesDetail", default)]
2958    pub items: Vec<super::extended::ClientFeesDetail>,
2959}
2960
2961/// Wrapper for SLB activities section
2962#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2963pub struct SLBActivitiesWrapper {
2964    /// List of SLB activities
2965    #[serde(rename = "SLBActivity", default)]
2966    pub items: Vec<super::extended::SLBActivity>,
2967}
2968
2969/// Wrapper for SLB fees section
2970#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2971pub struct SLBFeesWrapper {
2972    /// List of SLB fees
2973    #[serde(rename = "SLBFee", default)]
2974    pub items: Vec<super::extended::SLBFee>,
2975}
2976
2977/// Wrapper for hard to borrow details section
2978#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2979pub struct HardToBorrowDetailsWrapper {
2980    /// List of hard to borrow details
2981    #[serde(rename = "HardToBorrowDetail", default)]
2982    pub items: Vec<super::extended::HardToBorrowDetail>,
2983}
2984
2985/// Wrapper for FX lots section
2986#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2987pub struct FxLotsWrapper {
2988    /// List of FX lots
2989    #[serde(rename = "FxLot", default)]
2990    pub items: Vec<super::extended::FxLot>,
2991}
2992
2993/// Wrapper for unsettled transfers section
2994#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
2995pub struct UnsettledTransfersWrapper {
2996    /// List of unsettled transfers
2997    #[serde(rename = "UnsettledTransfer", default)]
2998    pub items: Vec<super::extended::UnsettledTransfer>,
2999}
3000
3001/// Wrapper for trade transfers section
3002#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
3003pub struct TradeTransfersWrapper {
3004    /// List of trade transfers
3005    #[serde(rename = "TradeTransfer", default)]
3006    pub items: Vec<super::extended::TradeTransfer>,
3007}
3008
3009/// Wrapper for prior period positions section
3010#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
3011pub struct PriorPeriodPositionsWrapper {
3012    /// List of prior period positions
3013    #[serde(rename = "PriorPeriodPosition", default)]
3014    pub items: Vec<super::extended::PriorPeriodPosition>,
3015}
3016
3017/// Wrapper for tier interest details section
3018#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
3019pub struct TierInterestDetailsWrapper {
3020    /// List of tier interest details
3021    #[serde(rename = "TierInterestDetail", default)]
3022    pub items: Vec<super::extended::TierInterestDetail>,
3023}
3024
3025/// Wrapper for debit card activities section
3026#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
3027pub struct DebitCardActivitiesWrapper {
3028    /// List of debit card activities
3029    #[serde(rename = "DebitCardActivity", default)]
3030    pub items: Vec<super::extended::DebitCardActivity>,
3031}
3032
3033/// Wrapper for sales tax section
3034#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
3035pub struct SalesTaxWrapper {
3036    /// List of sales tax entries
3037    #[serde(rename = "SalesTax", default)]
3038    pub items: Vec<super::extended::SalesTax>,
3039}
3040
3041/// Wrapper for symbol summary section
3042#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
3043pub struct SymbolSummaryWrapper {
3044    /// List of symbol summaries
3045    #[serde(rename = "SymbolSummary", default)]
3046    pub items: Vec<super::extended::SymbolSummary>,
3047}
3048
3049/// Wrapper for asset summary section
3050#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
3051pub struct AssetSummaryWrapper {
3052    /// List of asset summaries
3053    #[serde(rename = "AssetSummary", default)]
3054    pub items: Vec<super::extended::AssetSummary>,
3055}
3056
3057/// Wrapper for orders section
3058#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
3059pub struct OrdersWrapper {
3060    /// List of orders
3061    #[serde(rename = "Order", default)]
3062    pub items: Vec<super::extended::Order>,
3063}