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