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