use ib_flex::types::TradeType;
use ib_flex::{parse_activity_flex, AssetCategory, BuySell, OpenClose, PutCall};
use rust_decimal::Decimal;
use std::str::FromStr;
#[test]
fn test_parse_minimal_statement() {
let xml = include_str!("fixtures/activity_minimal.xml");
let result = parse_activity_flex(xml);
assert!(result.is_ok(), "Failed to parse XML: {:?}", result.err());
let statement = result.unwrap();
assert_eq!(statement.account_id.as_deref(), Some("U1234567"));
assert_eq!(statement.from_date.unwrap().to_string(), "2025-01-15");
assert_eq!(statement.to_date.unwrap().to_string(), "2025-01-15");
}
#[test]
fn test_parse_symbol_summary_with_multi_settlement_date() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<FlexQueryResponse queryName="Activity Query" type="AF">
<FlexStatements count="1">
<FlexStatement accountId="U1234567" fromDate="2025-01-15" toDate="2025-01-15"
period="SingleDay" whenGenerated="2025-01-15;235959">
<Trades>
<Trade accountId="U1234567" currency="USD" fxRateToBase="1"
assetCategory="STK" symbol="AAPL" description="APPLE INC" conid="265598"
transactionID="567890123" tradeDate="2025-01-15" dateTime="2025-01-15;093015"
settleDateTarget="2025-01-17" buySell="BUY" quantity="100" price="185.50"
amount="-18550.00" proceeds="-18550.00" ibCommission="-1.00"
ibCommissionCurrency="USD" netCash="-18551.00" multiplier="1"
tradeID="123456789" execID="test123" />
<SymbolSummary accountId="U1234567" currency="USD" fxRateToBase="1"
assetCategory="CASH" symbol="USD.TWD" description="USD.TWD" conid="12087792"
levelOfDetail="SYMBOL_SUMMARY" tradeDate="20260527"
settleDateTarget="MULTI" quantity="0" price="0" proceeds="0"
ibCommission="0" netCash="0" />
</Trades>
</FlexStatement>
</FlexStatements>
</FlexQueryResponse>"#;
let statement = parse_activity_flex(xml).unwrap();
let trade = &statement.trades.items[0];
assert_eq!(statement.trades.items.len(), 1);
assert_eq!(trade.symbol.as_deref(), Some("AAPL"));
}
#[test]
fn test_trades_container_captures_lots_and_summaries() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<FlexQueryResponse queryName="Activity Query" type="AF">
<FlexStatements count="1">
<FlexStatement accountId="U1234567" fromDate="20250115" toDate="20250115"
period="SingleDay" whenGenerated="20250115;235959">
<Trades>
<Trade accountId="U1234567" currency="USD" assetCategory="STK"
symbol="AAPL" conid="265598" tradeDate="20250115"
buySell="BUY" quantity="100" tradePrice="185.50"
ibCommission="-1.00" netCash="-18551.00"
levelOfDetail="EXECUTION" />
<Lot accountId="U1234567" currency="USD" assetCategory="STK"
symbol="AAPL" conid="265598" tradeDate="20250115"
buySell="BUY" quantity="100" tradePrice="185.50"
openDateTime="20240101;100000" levelOfDetail="LOT"
fifoPnlRealized="12.34" />
<SymbolSummary accountId="U1234567" currency="USD"
assetCategory="STK" symbol="AAPL" conid="265598"
quantity="100" levelOfDetail="SYMBOL_SUMMARY" />
<Order accountId="U1234567" currency="USD" assetCategory="STK"
symbol="AAPL" conid="265598" buySell="BUY" quantity="100"
tradePrice="185.50" levelOfDetail="ORDER" />
</Trades>
</FlexStatement>
</FlexStatements>
</FlexQueryResponse>"#;
let statement = parse_activity_flex(xml).unwrap();
let t = &statement.trades;
assert_eq!(t.items.len(), 1, "one execution-level trade");
assert_eq!(t.lots.len(), 1, "one lot detail row");
assert_eq!(t.symbol_summaries.len(), 1, "one symbol summary");
assert_eq!(t.orders.len(), 1, "one order roll-up");
assert_eq!(
t.lots[0].fifo_pnl_realized,
Some(Decimal::from_str("12.34").unwrap())
);
assert_eq!(t.lots[0].open_date_time.as_deref(), Some("20240101;100000"));
assert_eq!(
t.orders[0].price,
Some(Decimal::from_str("185.50").unwrap())
);
}
#[test]
fn test_parse_newly_modeled_sections() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<FlexQueryResponse queryName="Activity Query" type="AF">
<FlexStatements count="1">
<FlexStatement accountId="U1234567" fromDate="20250115" toDate="20250115"
period="SingleDay" whenGenerated="20250115;235959">
<TransactionTaxes>
<TransactionTax accountId="U1234567" currency="GBP" symbol="VOD"
conid="111" date="20250115" taxableDescription="Stamp Duty"
taxAmount="-12.50" country="UK" />
</TransactionTaxes>
<NetStockPositions>
<NetStockPosition accountId="U1234567" assetCategory="STK" symbol="AAPL"
conid="265598" netShares="100" />
</NetStockPositions>
<CFDCharges>
<CFDCharge accountId="U1234567" currency="USD" total="-3.21" />
</CFDCharges>
</FlexStatement>
</FlexStatements>
</FlexQueryResponse>"#;
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.transaction_taxes.items.len(), 1);
assert_eq!(
statement.transaction_taxes.items[0].tax_amount,
Some(Decimal::from_str("-12.50").unwrap())
);
assert_eq!(statement.net_stock_positions.items.len(), 1);
assert_eq!(statement.cfd_charges.items.len(), 1);
}
#[test]
fn test_parse_trades() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 1);
let trade = &statement.trades.items[0];
assert_eq!(trade.symbol.as_deref(), Some("AAPL"));
assert_eq!(trade.asset_category, Some(AssetCategory::Stock));
assert_eq!(trade.buy_sell, Some(BuySell::Buy));
assert_eq!(trade.quantity.unwrap().to_string(), "100");
assert_eq!(trade.price.unwrap().to_string(), "185.50");
}
#[test]
fn test_empty_sections() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 0);
assert_eq!(statement.cash_transactions.items.len(), 0);
assert_eq!(statement.corporate_actions.items.len(), 0);
}
#[test]
fn test_securities_info_parsing() {
let xml = include_str!("fixtures/activity_simple.xml");
let result = parse_activity_flex(xml);
match result {
Ok(statement) => {
println!(
"Successfully parsed with {} securities",
statement.securities_info.items.len()
);
}
Err(e) => {
println!("Expected error with complex XML: {}", e);
}
}
}
#[test]
fn test_commission_calculation() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
let total_commission: rust_decimal::Decimal = statement
.trades
.items
.iter()
.filter_map(|t| t.commission)
.sum();
assert_eq!(total_commission.to_string(), "-1.00");
}
#[test]
fn test_trade_proceeds() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
let trade = &statement.trades.items[0];
assert_eq!(trade.proceeds.unwrap().to_string(), "-18550.00");
assert_eq!(
trade.net_cash.map(|d| d.to_string()),
Some("-18551.00".to_string())
);
}
#[test]
fn test_account_id_extraction() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
assert!(!statement.account_id.as_deref().unwrap_or("").is_empty());
assert_eq!(statement.account_id.as_deref(), Some("U1234567"));
}
#[test]
fn test_date_range_parsing() {
let xml = include_str!("fixtures/activity_minimal.xml");
let statement = parse_activity_flex(xml).unwrap();
assert!(statement.from_date <= statement.to_date);
}
#[test]
fn test_parse_options_trades() {
let xml = include_str!("fixtures/activity_options.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let call_trade = &statement.trades.items[0];
assert_eq!(call_trade.symbol.as_deref(), Some("AAPL 250221C00180000"));
assert_eq!(call_trade.asset_category, Some(AssetCategory::Option));
assert_eq!(call_trade.put_call, Some(PutCall::Call));
assert_eq!(call_trade.strike.unwrap().to_string(), "180");
assert_eq!(call_trade.multiplier.unwrap().to_string(), "100");
assert_eq!(call_trade.underlying_symbol, Some("AAPL".to_string()));
assert_eq!(call_trade.open_close, Some(OpenClose::Open));
}
#[test]
fn test_parse_options_short_put() {
let xml = include_str!("fixtures/activity_options.xml");
let statement = parse_activity_flex(xml).unwrap();
let put_trade = &statement.trades.items[1];
assert_eq!(put_trade.symbol.as_deref(), Some("TSLA 250221P00200000"));
assert_eq!(put_trade.put_call, Some(PutCall::Put));
assert_eq!(put_trade.strike.unwrap().to_string(), "200");
assert_eq!(put_trade.quantity.unwrap().to_string(), "-2");
assert_eq!(put_trade.underlying_symbol, Some("TSLA".to_string()));
}
#[test]
fn test_parse_options_assignment() {
let xml = include_str!("fixtures/activity_options.xml");
let statement = parse_activity_flex(xml).unwrap();
let assignment = &statement.trades.items[2];
assert_eq!(assignment.symbol.as_deref(), Some("MSFT 250115C00400000"));
assert_eq!(assignment.open_close, Some(OpenClose::Close));
assert!(assignment.fifo_pnl_realized.is_some());
assert_eq!(assignment.fifo_pnl_realized.unwrap().to_string(), "1050");
}
#[test]
fn test_parse_options_positions() {
let xml = include_str!("fixtures/activity_options.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 2);
let long_pos = &statement.positions.items[0];
assert_eq!(long_pos.quantity.unwrap().to_string(), "1");
assert_eq!(long_pos.asset_category, Some(AssetCategory::Option));
let short_pos = &statement.positions.items[1];
assert_eq!(short_pos.quantity.unwrap().to_string(), "-2");
}
#[test]
fn test_parse_futures_trades() {
let xml = include_str!("fixtures/activity_futures.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let es_trade = &statement.trades.items[0];
assert_eq!(es_trade.symbol.as_deref(), Some("ESH5"));
assert_eq!(es_trade.asset_category, Some(AssetCategory::Future));
assert_eq!(es_trade.multiplier.unwrap().to_string(), "50");
assert_eq!(es_trade.underlying_symbol, Some("ES".to_string()));
}
#[test]
fn test_parse_futures_commodity() {
let xml = include_str!("fixtures/activity_futures.xml");
let statement = parse_activity_flex(xml).unwrap();
let gc_trade = &statement.trades.items[3];
assert_eq!(gc_trade.symbol.as_deref(), Some("GCG5"));
}
#[test]
fn test_parse_futures_positions() {
let xml = include_str!("fixtures/activity_futures.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 3);
let es_pos = &statement.positions.items[0];
assert_eq!(es_pos.quantity.unwrap().to_string(), "2");
assert_eq!(es_pos.asset_category, Some(AssetCategory::Future));
}
#[test]
fn test_parse_forex_trades() {
let xml = include_str!("fixtures/activity_forex.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let eur_trade = &statement.trades.items[0];
assert_eq!(eur_trade.symbol.as_deref(), Some("EUR.USD"));
assert_eq!(eur_trade.asset_category, Some(AssetCategory::Cash));
assert_eq!(eur_trade.quantity.unwrap().to_string(), "50000");
}
#[test]
fn test_parse_forex_positions() {
let xml = include_str!("fixtures/activity_forex.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 3);
let eur_pos = &statement.positions.items[0];
assert_eq!(eur_pos.quantity.unwrap().to_string(), "50000");
assert_eq!(eur_pos.currency.as_deref(), Some("EUR"));
}
#[test]
fn test_parse_forex_conversion_rates() {
let xml = include_str!("fixtures/activity_forex.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.conversion_rates.items.len(), 5);
let eur_rate = statement
.conversion_rates
.items
.iter()
.find(|r| r.from_currency.as_deref() == Some("EUR"))
.unwrap();
assert_eq!(eur_rate.rate.unwrap().to_string(), "1.0855");
}
#[test]
fn test_parse_bonds_trades() {
let xml = include_str!("fixtures/activity_bonds.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let treasury = &statement.trades.items[0];
assert_eq!(treasury.symbol.as_deref(), Some("T 4.5 11/15/33"));
assert_eq!(treasury.asset_category, Some(AssetCategory::Bond));
}
#[test]
fn test_parse_bonds_corporate() {
let xml = include_str!("fixtures/activity_bonds.xml");
let statement = parse_activity_flex(xml).unwrap();
let corp_bond = &statement.trades.items[1];
assert_eq!(corp_bond.symbol.as_deref(), Some("AAPL 3.85 08/04/46"));
}
#[test]
fn test_parse_bonds_positions() {
let xml = include_str!("fixtures/activity_bonds.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 3);
for pos in &statement.positions.items {
assert_eq!(pos.asset_category, Some(AssetCategory::Bond));
assert!(pos.quantity.unwrap() > rust_decimal::Decimal::ZERO);
}
}
#[test]
fn test_parse_corporate_actions() {
let xml = include_str!("fixtures/activity_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.corporate_actions.items.len(), 8);
}
#[test]
fn test_parse_dividend_action() {
let xml = include_str!("fixtures/activity_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let dividend = &statement.corporate_actions.items[0];
assert_eq!(dividend.symbol.as_deref(), Some("AAPL"));
assert_eq!(dividend.action_type, Some("DI".to_string()));
assert_eq!(dividend.quantity.unwrap().to_string(), "400");
assert_eq!(dividend.amount.unwrap().to_string(), "100.00");
}
#[test]
fn test_parse_stock_split() {
let xml = include_str!("fixtures/activity_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let split = &statement.corporate_actions.items[1];
assert_eq!(split.symbol.as_deref(), Some("TSLA"));
assert_eq!(split.action_type, Some("FS".to_string()));
}
#[test]
fn test_parse_merger() {
let xml = include_str!("fixtures/activity_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let merger = &statement.corporate_actions.items[3];
assert_eq!(merger.symbol.as_deref(), Some("ACQUIRED"));
assert_eq!(merger.action_type, Some("TC".to_string()));
assert_eq!(merger.fifo_pnl_realized.unwrap().to_string(), "1500.00");
}
#[test]
fn test_parse_cash_transactions() {
let xml = include_str!("fixtures/activity_cash.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.cash_transactions.items.len(), 15);
}
#[test]
fn test_parse_deposit_withdrawal() {
let xml = include_str!("fixtures/activity_cash.xml");
let statement = parse_activity_flex(xml).unwrap();
let deposit = &statement.cash_transactions.items[0];
assert_eq!(deposit.amount.unwrap().to_string(), "10000.00");
assert_eq!(
deposit.transaction_type,
Some("Deposits & Withdrawals".to_string())
);
let withdrawal = &statement.cash_transactions.items[1];
assert_eq!(withdrawal.amount.unwrap().to_string(), "-5000.00");
}
#[test]
fn test_parse_interest_transactions() {
let xml = include_str!("fixtures/activity_cash.xml");
let statement = parse_activity_flex(xml).unwrap();
let credit = &statement.cash_transactions.items[2];
assert_eq!(
credit.transaction_type,
Some("Broker Interest Paid".to_string())
);
assert!(credit.amount.unwrap() > rust_decimal::Decimal::ZERO);
}
#[test]
fn test_parse_dividend_cash() {
let xml = include_str!("fixtures/activity_cash.xml");
let statement = parse_activity_flex(xml).unwrap();
let dividend = &statement.cash_transactions.items[4];
assert_eq!(dividend.symbol.clone().unwrap(), "AAPL");
assert_eq!(dividend.transaction_type, Some("Dividends".to_string()));
assert_eq!(dividend.amount.unwrap().to_string(), "150.00");
}
#[test]
fn test_parse_warrants() {
let xml = include_str!("fixtures/activity_warrants.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 2);
let buy = &statement.trades.items[0];
assert_eq!(buy.asset_category, Some(AssetCategory::Warrant));
assert_eq!(buy.symbol.as_deref(), Some("ABC WS"));
assert_eq!(buy.buy_sell, Some(BuySell::Buy));
assert_eq!(buy.quantity.unwrap().to_string(), "100");
let sell = &statement.trades.items[1];
assert_eq!(sell.asset_category, Some(AssetCategory::Warrant));
assert_eq!(sell.buy_sell, Some(BuySell::Sell));
assert!(sell.fifo_pnl_realized.is_some());
}
#[test]
fn test_parse_warrant_position() {
let xml = include_str!("fixtures/activity_warrants.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 1);
let position = &statement.positions.items[0];
assert_eq!(position.asset_category, Some(AssetCategory::Warrant));
assert_eq!(position.symbol.as_deref(), Some("ABC WS"));
assert_eq!(position.quantity.unwrap().to_string(), "50");
}
#[test]
fn test_parse_tbills() {
let xml = include_str!("fixtures/activity_tbills.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 1);
let trade = &statement.trades.items[0];
assert_eq!(trade.asset_category, Some(AssetCategory::Bill));
assert_eq!(trade.symbol.as_deref(), Some("912796ZX9"));
assert_eq!(trade.buy_sell, Some(BuySell::Buy));
assert_eq!(trade.quantity.unwrap().to_string(), "10000");
}
#[test]
fn test_parse_tbill_position() {
let xml = include_str!("fixtures/activity_tbills.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 1);
let position = &statement.positions.items[0];
assert_eq!(position.asset_category, Some(AssetCategory::Bill));
assert_eq!(position.quantity.unwrap().to_string(), "10000");
}
#[test]
fn test_parse_tbill_maturity() {
let xml = include_str!("fixtures/activity_tbills.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.corporate_actions.items.len(), 1);
let action = &statement.corporate_actions.items[0];
assert_eq!(action.asset_category, Some(AssetCategory::Bill));
assert_eq!(action.action_type, Some("TC".to_string()));
}
#[test]
fn test_parse_cfds() {
let xml = include_str!("fixtures/activity_cfds.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 2);
let buy = &statement.trades.items[0];
assert_eq!(buy.asset_category, Some(AssetCategory::Cfd));
assert_eq!(buy.symbol.as_deref(), Some("AAPL"));
assert_eq!(buy.buy_sell, Some(BuySell::Buy));
let sell = &statement.trades.items[1];
assert_eq!(sell.asset_category, Some(AssetCategory::Cfd));
assert_eq!(sell.buy_sell, Some(BuySell::Sell));
assert!(sell.fifo_pnl_realized.is_some());
}
#[test]
fn test_parse_cfd_financing() {
let xml = include_str!("fixtures/activity_cfds.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.cash_transactions.items.len(), 1);
let financing = &statement.cash_transactions.items[0];
assert_eq!(financing.asset_category, Some(AssetCategory::Cfd));
assert_eq!(financing.transaction_type, Some("Other Fees".to_string()));
}
#[test]
fn test_parse_cancelled_trades() {
let xml = include_str!("fixtures/activity_cancelled_trades.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let buy = &statement.trades.items[0];
assert_eq!(buy.buy_sell, Some(BuySell::Buy));
assert_eq!(buy.symbol.as_deref(), Some("MSFT"));
let cancel_buy = &statement.trades.items[1];
assert_eq!(cancel_buy.buy_sell, Some(BuySell::CancelBuy));
assert_eq!(cancel_buy.symbol.as_deref(), Some("MSFT"));
let sell = &statement.trades.items[2];
assert_eq!(sell.buy_sell, Some(BuySell::Sell));
assert_eq!(sell.symbol.as_deref(), Some("GOOGL"));
let cancel_sell = &statement.trades.items[3];
assert_eq!(cancel_sell.buy_sell, Some(BuySell::CancelSell));
assert_eq!(cancel_sell.symbol.as_deref(), Some("GOOGL"));
}
#[test]
fn test_parse_friday_fx_trade_cancel_with_trade_price() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<FlexQueryResponse queryName="Activity Query" type="AF">
<FlexStatements count="1">
<FlexStatement accountId="U1234567" fromDate="20260529" toDate="20260529"
period="SingleDay" whenGenerated="20260601;000000">
<Trades>
<Trade accountId="U1234567" acctAlias="Fund" model="" currency="TWD"
fxRateToBase="0.031812" assetCategory="CASH" symbol="USD.TWD"
description="USD.TWD" conid="37928794" tradeID="" multiplier="1"
reportDate="20260529" dateTime="20260528;170000"
tradeDate="20260528" settleDateTarget="20260528"
transactionType="TradeCancel" exchange="--" quantity="0"
tradePrice="31.41202078" tradeMoney="-0.000000063"
proceeds="0.000000063" taxes="0" ibCommission="0"
ibCommissionCurrency="USD" netCash="0" closePrice="0"
openCloseIndicator="" notes="Ca" cost="0" fifoPnlRealized="0"
mtmPnl="0" origTradePrice="31.41202078" origTradeDate="20260528"
origTradeID="9590116305" origOrderID="5240898668"
origTransactionID="40261375730" buySell="SELL (Ca.)"
ibOrderID="5240898668" transactionID="40271461617"
ibExecID="" exchOrderId="N/A" extExecID="N/A"
levelOfDetail="EXECUTION" changeInPrice="0" changeInQuantity="0"
isAPIOrder="N" accruedInt="0" fineness="0.0" weight="0.0" />
<Trade accountId="U1234567" acctAlias="Fund" model="" currency="TWD"
fxRateToBase="0.031812" assetCategory="CASH" symbol="USD.TWD"
description="USD.TWD" conid="37928794" tradeID="9590116305"
multiplier="1" reportDate="20260529" dateTime="20260528;170000"
tradeDate="20260528" settleDateTarget="20260528"
transactionType="ExchTrade" exchange="IDEALFX" quantity="0"
tradePrice="31.3774104" tradeMoney="0.000000063"
proceeds="-0.000000063" taxes="0" ibCommission="0"
ibCommissionCurrency="USD" netCash="0" closePrice="0"
openCloseIndicator="" notes="" cost="0" fifoPnlRealized="0"
mtmPnl="0" origTradePrice="0" origOrderID="0"
origTransactionID="0" buySell="BUY" ibOrderID="5240898668"
transactionID="40271461619" ibExecID="CUSTHSFX.021206da.01.02"
exchOrderId="N/A" extExecID="N/A" orderTime="20260528;170000"
levelOfDetail="EXECUTION" changeInPrice="0" changeInQuantity="0"
isAPIOrder="N" accruedInt="0" fineness="0.0" weight="0.0" />
</Trades>
</FlexStatement>
</FlexStatements>
</FlexQueryResponse>"#;
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 2);
let cancel = &statement.trades.items[0];
assert_eq!(cancel.transaction_type, Some(TradeType::TradeCancel));
assert_eq!(cancel.buy_sell, Some(BuySell::CancelSell));
assert_eq!(cancel.quantity, Some(Decimal::ZERO));
assert_eq!(
cancel.price,
Some(Decimal::from_str("31.41202078").unwrap())
);
assert_eq!(
cancel.trade_money,
Some(Decimal::from_str("-0.000000063").unwrap())
);
assert_eq!(cancel.orig_transaction_id.as_deref(), Some("40261375730"));
let correction = &statement.trades.items[1];
assert_eq!(correction.transaction_type, Some(TradeType::ExchTrade));
assert_eq!(correction.buy_sell, Some(BuySell::Buy));
assert_eq!(correction.quantity, Some(Decimal::ZERO));
assert_eq!(
correction.price,
Some(Decimal::from_str("31.3774104").unwrap())
);
assert_eq!(
correction.trade_money,
Some(Decimal::from_str("0.000000063").unwrap())
);
}
#[test]
fn test_parse_fractional_shares() {
let xml = include_str!("fixtures/activity_fractional_shares.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.trades.items.len(), 4);
let buy = &statement.trades.items[0];
assert_eq!(buy.symbol.as_deref(), Some("AMZN"));
assert_eq!(buy.quantity.unwrap().to_string(), "2.5");
let sell = &statement.trades.items[1];
assert_eq!(sell.quantity.unwrap().to_string(), "-1.25");
}
#[test]
fn test_parse_fractional_cancellation() {
let xml = include_str!("fixtures/activity_fractional_shares.xml");
let statement = parse_activity_flex(xml).unwrap();
let cancel = &statement.trades.items[3];
assert_eq!(cancel.buy_sell, Some(BuySell::CancelBuy));
}
#[test]
fn test_parse_fractional_position() {
let xml = include_str!("fixtures/activity_fractional_shares.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.positions.items.len(), 1);
let position = &statement.positions.items[0];
assert_eq!(position.symbol.as_deref(), Some("AMZN"));
assert_eq!(position.quantity.unwrap().to_string(), "1.25");
}
#[test]
fn test_parse_complex_corporate_actions() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
assert_eq!(statement.corporate_actions.items.len(), 10);
}
#[test]
fn test_parse_choice_dividend() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let choice = &statement.corporate_actions.items[0];
assert_eq!(choice.action_type, Some("CD".to_string()));
let _delivery = &statement.corporate_actions.items[1];
}
#[test]
fn test_parse_tender_offer() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let tender = &statement.corporate_actions.items[2];
assert_eq!(tender.action_type, Some("TO".to_string()));
assert_eq!(tender.quantity.unwrap().to_string(), "-50");
let issue = &statement.corporate_actions.items[3];
assert_eq!(issue.proceeds.unwrap().to_string(), "3500.00");
}
#[test]
fn test_parse_bond_conversion() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let conversion = &statement.corporate_actions.items[4];
assert_eq!(conversion.action_type, Some("BC".to_string()));
assert_eq!(conversion.asset_category, Some(AssetCategory::Bond));
let issue = &statement.corporate_actions.items[5];
assert_eq!(issue.asset_category, Some(AssetCategory::Stock));
assert_eq!(issue.quantity.unwrap().to_string(), "250");
}
#[test]
fn test_parse_bond_maturity() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let maturity = &statement.corporate_actions.items[6];
assert_eq!(maturity.action_type, Some("BM".to_string()));
assert_eq!(maturity.asset_category, Some(AssetCategory::Bond));
assert_eq!(maturity.proceeds.unwrap().to_string(), "5000.00");
}
#[test]
fn test_parse_coupon_payment() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let coupon = &statement.corporate_actions.items[7];
assert_eq!(coupon.action_type, Some("CP".to_string()));
assert_eq!(coupon.proceeds.unwrap().to_string(), "200.00");
}
#[test]
fn test_parse_rights_issue() {
let xml = include_str!("fixtures/activity_complex_corporate_actions.xml");
let statement = parse_activity_flex(xml).unwrap();
let rights = &statement.corporate_actions.items[8];
assert_eq!(rights.action_type, Some("RI".to_string()));
let subscribe = &statement.corporate_actions.items[9];
assert_eq!(subscribe.action_type, Some("SR".to_string()));
}