From b41f90d1a152560ced885e932d08d0f8c63eb3ba Mon Sep 17 00:00:00 2001 From: Jan Vroonhof <38109466+vroonhof@users.noreply.github.com> Date: Tue, 27 May 2025 00:30:20 +0200 Subject: [PATCH 01/20] Add a bunch of fields that appear in my flex queries on new types IBKR seems to be in migrarion from AssetSummary to SymbolSummary Currency conversion rates sometimes appear for 'RUS' (though always --- ibflex/Types.py | 11 ++++++++++- ibflex/parser.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ibflex/Types.py b/ibflex/Types.py index 6ab7555..1f21d3a 100644 --- a/ibflex/Types.py +++ b/ibflex/Types.py @@ -709,6 +709,8 @@ class CashReportCurrency(FlexElement): salesTaxYTD: Optional[decimal.Decimal] = None salesTaxPaxos: Optional[decimal.Decimal] = None otherIncome: Optional[decimal.Decimal] = None + otherIncomeMTD: Optional[decimal.Decimal] = None + otherIncomeYTD: Optional[decimal.Decimal] = None otherIncomeSec: Optional[decimal.Decimal] = None otherIncomeCom: Optional[decimal.Decimal] = None otherFeesMTD: Optional[decimal.Decimal] = None @@ -1397,7 +1399,9 @@ class SymbolSummary(FlexElement): dateTime: Optional[datetime.datetime] = None reportDate: Optional[datetime.date] = None settleDate: Optional[datetime.date] = None + taxes: Optional[decimal.Decimal] = None tradeDate: Optional[datetime.date] = None + tradePrice: Optional[decimal.Decimal] = None exchange: Optional[str] = None buySell: Optional[enums.BuySell] = None quantity: Optional[decimal.Decimal] = None @@ -1427,6 +1431,10 @@ class SymbolSummary(FlexElement): relatedTradeID: Optional[str] = None origTransactionID: Optional[str] = None relatedTransactionID: Optional[str] = None + ibCommission: Optional[decimal.Decimal] = None + ibCommissionCurrency: Optional[str] = None + netCash: Optional[decimal.Decimal] = None + netCashInBase: Optional[decimal.Decimal] = None @dataclass(frozen=True) @@ -2122,7 +2130,8 @@ class Transfer(FlexElement): commodityType: Optional[str] = None fineness: Optional[decimal.Decimal] = None weight: Optional[str] = None - + figi: Optional[str] = None + settleDate: Optional[datetime.date] = None @dataclass(frozen=True) class UnsettledTransfer(FlexElement): diff --git a/ibflex/parser.py b/ibflex/parser.py index af6b270..67a43d6 100755 --- a/ibflex/parser.py +++ b/ibflex/parser.py @@ -463,6 +463,7 @@ def convert_enum(Type, value): "CNH", # RMB traded in HK "BASE_SUMMARY", # Fake currency code used in IB NAV/Performance reports "", # Lot element allows blank currency ?! + "RUS", # Appears with placeholder rate -1 in some list of rates. ) From e4c49130acf090474585e36d8cf6a0c0d5809180 Mon Sep 17 00:00:00 2001 From: Jan Vroonhof <38109466+vroonhof@users.noreply.github.com> Date: Tue, 27 May 2025 01:18:42 +0200 Subject: [PATCH 02/20] All types from enabling everything Most of these are empty but present on my reports, it just seems to inherit from Trade --- ibflex/Types.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ibflex/Types.py b/ibflex/Types.py index 1f21d3a..929c767 100644 --- a/ibflex/Types.py +++ b/ibflex/Types.py @@ -1387,6 +1387,9 @@ class SymbolSummary(FlexElement): tradeID: Optional[str] = None orderID: Optional[decimal.Decimal] = None execID: Optional[str] = None + ibExecID: Optional[str] = None + extExecID: Optional[str] = None + exchOrderId: Optional[str] = None brokerageOrderID: Optional[str] = None orderReference: Optional[str] = None volatilityOrderLink: Optional[str] = None @@ -1394,14 +1397,19 @@ class SymbolSummary(FlexElement): origTradePrice: Optional[decimal.Decimal] = None origTradeDate: Optional[datetime.date] = None origTradeID: Optional[str] = None + transactionID: Optional[str] = None # Despite the name, `orderTime` actually contains date/time data. orderTime: Optional[datetime.datetime] = None + openDateTime: Optional[datetime.datetime] = None + holdingPeriodDateTime: Optional[datetime.datetime] = None dateTime: Optional[datetime.datetime] = None reportDate: Optional[datetime.date] = None settleDate: Optional[datetime.date] = None + settleDateTarget: Optional[datetime.date] = None # expected date of ownership transfer taxes: Optional[decimal.Decimal] = None tradeDate: Optional[datetime.date] = None tradePrice: Optional[decimal.Decimal] = None + tradeMoney: Optional[decimal.Decimal] = None # TradeMoney = Proceeds + Fees + Commissions exchange: Optional[str] = None buySell: Optional[enums.BuySell] = None quantity: Optional[decimal.Decimal] = None @@ -1435,6 +1443,27 @@ class SymbolSummary(FlexElement): ibCommissionCurrency: Optional[str] = None netCash: Optional[decimal.Decimal] = None netCashInBase: Optional[decimal.Decimal] = None + closePrice: Optional[decimal.Decimal] = None + openCloseIndicator: Optional[enums.OpenClose] = None + notes: Optional[str] = None + cost: Optional[decimal.Decimal] = None + fifoPnlRealized: Optional[decimal.Decimal] = None + mtmPnl: Optional[decimal.Decimal] = None # PnL at the time of reportins + ibOrderID: Optional[str] = None + origOrderID: Optional[str] = None + rtn: Optional[str] = None + whenRealized: Optional[datetime.datetime] = None + whenReopened: Optional[datetime.datetime] = None + changeInPrice: Optional[decimal.Decimal] = None + changeInQuantity: Optional[decimal.Decimal] = None + initialInvestment: Optional[decimal.Decimal] = None + serialNumber: Optional[str] = None + deliveryType: Optional[str] = None + commodityType: Optional[str] = None + fineness: Optional[decimal.Decimal] = None + weight: Optional[str] = None + + @dataclass(frozen=True) From 7ade81b9feea34c1cf70e021fa97edb4f3a04cd1 Mon Sep 17 00:00:00 2001 From: Jan Vroonhof <38109466+vroonhof@users.noreply.github.com> Date: Tue, 27 May 2025 01:31:51 +0200 Subject: [PATCH 03/20] More attributes (now enabled all fields) --- ibflex/Types.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ibflex/Types.py b/ibflex/Types.py index 929c767..e76784b 100644 --- a/ibflex/Types.py +++ b/ibflex/Types.py @@ -2160,7 +2160,11 @@ class Transfer(FlexElement): fineness: Optional[decimal.Decimal] = None weight: Optional[str] = None figi: Optional[str] = None - settleDate: Optional[datetime.date] = None + settleDate: Optional[datetime.date] = None + issuerCountryCode: Optional[str] = None + levelOfDetail: Optional[str] = None + positionInstructionID: Optional[str] = None + positionInstructionSetID: Optional[str] = None @dataclass(frozen=True) class UnsettledTransfer(FlexElement): From 330e69032818c735da2beece920be61d906cd82f Mon Sep 17 00:00:00 2001 From: Jan Vroonhof <38109466+vroonhof@users.noreply.github.com> Date: Tue, 27 May 2025 01:32:16 +0200 Subject: [PATCH 04/20] The new summary fields have MULTI as the date values --- ibflex/parser.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ibflex/parser.py b/ibflex/parser.py index 67a43d6..8fc5dc9 100755 --- a/ibflex/parser.py +++ b/ibflex/parser.py @@ -170,9 +170,11 @@ def parse_element_attr( # INPUT VALUE PREP FUNCTIONS FOR DATA CONVERTERS # These are just implementation details for converters and don't need testing. ############################################################################### -def prep_date(value: str) -> Tuple[int, int, int]: +def prep_date(value: str) -> Optional[Tuple[int, int, int]]: """Returns a tuple of (year, month, day). """ + if value == "MULTI": + return None # Summaries have MULTI as date value. date_format = DATE_FORMATS[len(value)][value.count('/')] return datetime.datetime.strptime(value, date_format).timetuple()[:3] @@ -184,9 +186,11 @@ def prep_time(value: str) -> Tuple[int, int, int]: return datetime.datetime.strptime(value, time_format).timetuple()[3:6] -def prep_datetime(value: str) -> Tuple[int, ...]: +def prep_datetime(value: str) -> Optional[Tuple[int, ...]]: """Returns a tuple of (year, month, day, hour, minute, second). """ + if value == "MULTI": + return None # Summaries have MULTI as date value. # HACK - some old data has ", " separator instead of ",". value = value.replace(", ", ",") From ae82e52b9f0b31b58106fee9e01dff83c0097995 Mon Sep 17 00:00:00 2001 From: robcohen Date: Tue, 16 Dec 2025 10:41:51 -0800 Subject: [PATCH 05/20] Merge in fields form ibflex2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing fields for current IBKR Flex Query format - Add positionActionID to Trade, Lot, SymbolSummary, AssetSummary, Order - Add 32 missing fields to SymbolSummary (closePrice, cost, mtmPnl, etc.) - Add RUS to CURRENCY_CODES (used by IBKR for Russian-related data) - Remove hardcoded URL override that broke API calls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- ibflex/Types.py | 45 +++++++++++++++++++++++++++++---------------- ibflex/client.py | 2 -- ibflex/parser.py | 2 +- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/ibflex/Types.py b/ibflex/Types.py index e76784b..f4b0056 100644 --- a/ibflex/Types.py +++ b/ibflex/Types.py @@ -1147,6 +1147,7 @@ class Trade(FlexElement): issuerCountryCode: Optional[str] = None rtn: Optional[str] = None initialInvestment: Optional[decimal.Decimal] = None + positionActionID: Optional[str] = None @dataclass(frozen=True) @@ -1299,6 +1300,7 @@ class Lot(FlexElement): relatedTradeID: Optional[str] = None rtn: Optional[str] = None initialInvestment: Optional[decimal.Decimal] = None + positionActionID: Optional[str] = None @dataclass(frozen=True) @@ -1439,31 +1441,40 @@ class SymbolSummary(FlexElement): relatedTradeID: Optional[str] = None origTransactionID: Optional[str] = None relatedTransactionID: Optional[str] = None + positionActionID: Optional[str] = None + changeInPrice: Optional[decimal.Decimal] = None + changeInQuantity: Optional[decimal.Decimal] = None + closePrice: Optional[decimal.Decimal] = None + commodityType: Optional[str] = None + cost: Optional[decimal.Decimal] = None + deliveryType: Optional[str] = None + exchOrderId: Optional[str] = None + extExecID: Optional[str] = None + fifoPnlRealized: Optional[decimal.Decimal] = None + fineness: Optional[decimal.Decimal] = None + holdingPeriodDateTime: Optional[datetime.datetime] = None ibCommission: Optional[decimal.Decimal] = None ibCommissionCurrency: Optional[str] = None + ibExecID: Optional[str] = None + ibOrderID: Optional[str] = None + initialInvestment: Optional[decimal.Decimal] = None + mtmPnl: Optional[decimal.Decimal] = None netCash: Optional[decimal.Decimal] = None netCashInBase: Optional[decimal.Decimal] = None - closePrice: Optional[decimal.Decimal] = None - openCloseIndicator: Optional[enums.OpenClose] = None notes: Optional[str] = None - cost: Optional[decimal.Decimal] = None - fifoPnlRealized: Optional[decimal.Decimal] = None - mtmPnl: Optional[decimal.Decimal] = None # PnL at the time of reportins - ibOrderID: Optional[str] = None + openCloseIndicator: Optional[enums.OpenClose] = None + openDateTime: Optional[datetime.datetime] = None origOrderID: Optional[str] = None rtn: Optional[str] = None - whenRealized: Optional[datetime.datetime] = None - whenReopened: Optional[datetime.datetime] = None - changeInPrice: Optional[decimal.Decimal] = None - changeInQuantity: Optional[decimal.Decimal] = None - initialInvestment: Optional[decimal.Decimal] = None serialNumber: Optional[str] = None - deliveryType: Optional[str] = None - commodityType: Optional[str] = None - fineness: Optional[decimal.Decimal] = None + settleDateTarget: Optional[datetime.date] = None + taxes: Optional[decimal.Decimal] = None + tradeMoney: Optional[decimal.Decimal] = None + tradePrice: Optional[decimal.Decimal] = None + transactionID: Optional[str] = None weight: Optional[str] = None - - + whenRealized: Optional[datetime.datetime] = None + whenReopened: Optional[datetime.datetime] = None @dataclass(frozen=True) @@ -1573,6 +1584,7 @@ class AssetSummary(FlexElement): relatedTransactionID: Optional[str] = None rtn: Optional[str] = None initialInvestment: Optional[decimal.Decimal] = None + positionActionID: Optional[str] = None @dataclass(frozen=True) @@ -1681,6 +1693,7 @@ class Order(FlexElement): commodityType: Optional[str] = None fineness: Optional[decimal.Decimal] = None weight: Optional[str] = None + positionActionID: Optional[str] = None @dataclass(frozen=True) diff --git a/ibflex/client.py b/ibflex/client.py index f203e8b..041f53a 100755 --- a/ibflex/client.py +++ b/ibflex/client.py @@ -136,8 +136,6 @@ def request_statement( """First part of the 2-step download process. """ url = url or REQUEST_URL - ### AKE FIX - url = 'https://ndcdyn.interactivebrokers.com/portal.flexweb/api/v1/flexQuery' response = submit_request(url, token, query=query_id) stmt_access = parse_stmt_response(response) if isinstance(stmt_access, StatementError): diff --git a/ibflex/parser.py b/ibflex/parser.py index 8fc5dc9..32bf47f 100755 --- a/ibflex/parser.py +++ b/ibflex/parser.py @@ -467,7 +467,7 @@ def convert_enum(Type, value): "CNH", # RMB traded in HK "BASE_SUMMARY", # Fake currency code used in IB NAV/Performance reports "", # Lot element allows blank currency ?! - "RUS", # Appears with placeholder rate -1 in some list of rates. + "RUS", # Russian-related currency code used by IBKR ) From 97cd84a18c77468706113364cce1a07b7df4f0c2 Mon Sep 17 00:00:00 2001 From: vroonhof <38109466+vroonhof@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:57:03 +0000 Subject: [PATCH 06/20] Add a few more missing fields for CorporateAction --- ibflex/Types.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ibflex/Types.py b/ibflex/Types.py index f4b0056..ef1bc13 100644 --- a/ibflex/Types.py +++ b/ibflex/Types.py @@ -2300,6 +2300,11 @@ class CorporateAction(FlexElement): commodityType: Optional[str] = None fineness: Optional[decimal.Decimal] = None weight: Optional[str] = None + figi: Optional[str] = None + issuerCountryCode: Optional[str] = None + costBasis: Optional[decimal.Decimal] = None + + @dataclass(frozen=True) From d48d7b95dde8b00376f2814d42cbedfecc30318d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:19:22 +0000 Subject: [PATCH 07/20] Initial plan From c70b47fe91e931c9723b9e538f40b8c8bfd2383f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:23:37 +0000 Subject: [PATCH 08/20] Fix Trade.initialInvestment type and add liteSurchargeAccruals field Co-authored-by: vroonhof <38109466+vroonhof@users.noreply.github.com> --- ibflex/Types.py | 15 ++++++++------- ibflex/parser.py | 4 ++-- tests/test_parser.py | 4 +++- tests/test_types.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/ibflex/Types.py b/ibflex/Types.py index ef1bc13..fb8f4b2 100644 --- a/ibflex/Types.py +++ b/ibflex/Types.py @@ -479,6 +479,7 @@ class EquitySummaryByReportDateInBase(FlexElement): marginFinancingChargeAccrualsShort: Optional[decimal.Decimal] = None cryptoLong: Optional[decimal.Decimal] = None cryptoShort: Optional[decimal.Decimal] = None + liteSurchargeAccruals: Optional[decimal.Decimal] = None @dataclass(frozen=True) @@ -1146,7 +1147,7 @@ class Trade(FlexElement): subCategory: Optional[str] = None issuerCountryCode: Optional[str] = None rtn: Optional[str] = None - initialInvestment: Optional[decimal.Decimal] = None + initialInvestment: Optional[bool] = None positionActionID: Optional[str] = None @@ -1299,7 +1300,7 @@ class Lot(FlexElement): issuerCountryCode: Optional[str] = None relatedTradeID: Optional[str] = None rtn: Optional[str] = None - initialInvestment: Optional[decimal.Decimal] = None + initialInvestment: Optional[bool] = None positionActionID: Optional[str] = None @@ -1457,7 +1458,7 @@ class SymbolSummary(FlexElement): ibCommissionCurrency: Optional[str] = None ibExecID: Optional[str] = None ibOrderID: Optional[str] = None - initialInvestment: Optional[decimal.Decimal] = None + initialInvestment: Optional[bool] = None mtmPnl: Optional[decimal.Decimal] = None netCash: Optional[decimal.Decimal] = None netCashInBase: Optional[decimal.Decimal] = None @@ -1583,7 +1584,7 @@ class AssetSummary(FlexElement): origTransactionID: Optional[str] = None relatedTransactionID: Optional[str] = None rtn: Optional[str] = None - initialInvestment: Optional[decimal.Decimal] = None + initialInvestment: Optional[bool] = None positionActionID: Optional[str] = None @@ -1687,7 +1688,7 @@ class Order(FlexElement): origTransactionID: Optional[str] = None relatedTransactionID: Optional[str] = None rtn: Optional[str] = None - initialInvestment: Optional[decimal.Decimal] = None + initialInvestment: Optional[bool] = None serialNumber: Optional[str] = None deliveryType: Optional[str] = None commodityType: Optional[str] = None @@ -1846,7 +1847,7 @@ class OptionEAE(FlexElement): origTransactionID: Optional[str] = None relatedTransactionID: Optional[str] = None rtn: Optional[str] = None - initialInvestment: Optional[decimal.Decimal] = None + initialInvestment: Optional[bool] = None serialNumber: Optional[str] = None deliveryType: Optional[str] = None commodityType: Optional[str] = None @@ -2540,7 +2541,7 @@ class SecurityInfo(FlexElement): origTransactionID: Optional[str] = None relatedTransactionID: Optional[str] = None rtn: Optional[str] = None - initialInvestment: Optional[decimal.Decimal] = None + initialInvestment: Optional[bool] = None serialNumber: Optional[str] = None deliveryType: Optional[str] = None commodityType: Optional[str] = None diff --git a/ibflex/parser.py b/ibflex/parser.py index 32bf47f..1bf081a 100755 --- a/ibflex/parser.py +++ b/ibflex/parser.py @@ -332,8 +332,8 @@ def optional_convert(value): convert_string = make_optional(make_converter(str, prep=utils.identity_func)) convert_int = make_converter(int, prep=utils.identity_func) -# IB sends "Y"/"N" for True/False -convert_bool = make_converter(bool, prep=lambda x: {"Y": True, "N": False}[x]) +# IB sends "Y"/"N" or "Yes"/"No" for True/False +convert_bool = make_converter(bool, prep=lambda x: {"Y": True, "N": False, "Yes": True, "No": False}[x]) # IB sends numeric data with place delimiters (commas) convert_decimal = make_converter( decimal.Decimal, diff --git a/tests/test_parser.py b/tests/test_parser.py index 92fab3f..31fb665 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -333,9 +333,11 @@ def testConvertInt(self): parser.convert_int("") def testConvertBool(self): - """ Legal boolean values are 'Y'/'N' """ + """ Legal boolean values are 'Y'/'N' or 'Yes'/'No' """ self.assertEqual(parser.convert_bool("Y"), True) self.assertEqual(parser.convert_bool("N"), False) + self.assertEqual(parser.convert_bool("Yes"), True) + self.assertEqual(parser.convert_bool("No"), False) # Empty string raises FlexParserError. with self.assertRaises(parser.FlexParserError): diff --git a/tests/test_types.py b/tests/test_types.py index bf53cf4..175eaa5 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1985,5 +1985,49 @@ def testParse(self): self.assertEqual(instance.tradeID, None) +class TradeInitialInvestmentTestCase(unittest.TestCase): + """Test case for Trade.initialInvestment as boolean field. + + Tests the fix for https://github.com/vroonhof/opensteuerauszug/issues/106 + where initialInvestment="Yes" was causing parsing errors. + """ + data = ET.fromstring( + ('') + ) + + def testParse(self): + instance = parser.parse_data_element(self.data) + self.assertIsInstance(instance, Types.Trade) + self.assertEqual(instance.accountId, "U123456") + self.assertEqual(instance.currency, "USD") + self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) + self.assertEqual(instance.symbol, "TEST") + self.assertEqual(instance.initialInvestment, True) + self.assertEqual(instance.quantity, decimal.Decimal("100")) + + +class EquitySummaryLiteSurchargeAccrualsTestCase(unittest.TestCase): + """Test case for EquitySummaryByReportDateInBase.liteSurchargeAccruals field. + + Tests the fix for https://github.com/vroonhof/opensteuerauszug/issues/106 + where liteSurchargeAccruals attribute was missing. + """ + data = ET.fromstring( + ('') + ) + + def testParse(self): + instance = parser.parse_data_element(self.data) + self.assertIsInstance(instance, Types.EquitySummaryByReportDateInBase) + self.assertEqual(instance.accountId, "U123456") + self.assertEqual(instance.reportDate, datetime.date(2024, 1, 1)) + self.assertEqual(instance.cash, decimal.Decimal("1000.00")) + self.assertEqual(instance.total, decimal.Decimal("1000.00")) + self.assertEqual(instance.liteSurchargeAccruals, decimal.Decimal("5.50")) + + if __name__ == '__main__': unittest.main(verbosity=3) From 653b22f58791f4aeea7422d3ed66afdaa499dfe3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 21:40:22 +0000 Subject: [PATCH 09/20] Add unknown attribute tolerance for IB Flex XML parsing Interactive Brokers frequently adds new fields to their XML exports, causing parsing failures for attributes/elements not defined in Types.py. This commit adds an opt-in tolerance mechanism: - enable_unknown_attribute_tolerance(): silently ignore unknown XML attributes and element types during parsing - disable_unknown_attribute_tolerance(): restore strict behavior (default) When tolerance is enabled: - Unknown XML attributes on data elements are filtered out before parsing - Unknown element types return None and are filtered from containers - Unknown contained elements in FlexStatement/FlexQueryResponse are skipped - All known attributes continue to be parsed normally The functions are exported from the top-level ibflex package. On the original upstream ibflex package, these functions do not exist, so calling them raises AttributeError - providing a clear version guard. Includes 14 new tests covering: - Default off behavior - Enable/disable toggling - Package-level accessibility (version guard) - Unknown attributes with and without tolerance - Unknown element types - Full round-trip parsing with unknown fields - Container filtering Co-authored-by: vroonhof --- ibflex/__init__.py | 2 + ibflex/parser.py | 88 +++++++++++++++- tests/test_parser.py | 234 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+), 5 deletions(-) diff --git a/ibflex/__init__.py b/ibflex/__init__.py index 684a0d7..b6dcd6b 100644 --- a/ibflex/__init__.py +++ b/ibflex/__init__.py @@ -4,6 +4,8 @@ from .Types import * from . import parser from .parser import parse +from .parser import enable_unknown_attribute_tolerance +from .parser import disable_unknown_attribute_tolerance from . import utils from . import client diff --git a/ibflex/parser.py b/ibflex/parser.py index 1bf081a..83f1620 100755 --- a/ibflex/parser.py +++ b/ibflex/parser.py @@ -21,6 +21,56 @@ class FlexParserError(Exception): """ Error experienced while parsing Flex XML data. """ +############################################################################### +# UNKNOWN ATTRIBUTE TOLERANCE +############################################################################### +_UNKNOWN_ATTRIBUTE_TOLERANCE = False + + +def enable_unknown_attribute_tolerance(): + """Enable tolerance for unknown XML attributes in IB Flex data. + + When enabled, unknown attributes and element types in the XML data are + silently ignored instead of raising FlexParserError. This is useful when + Interactive Brokers adds new fields to their exports that are not yet + defined in the Types module. + + This function is only available in the forked version of ibflex. + Attempting to call it on the original upstream package will raise + AttributeError (the method does not exist there), providing a clear + signal that the feature is not supported. + + Default: off (strict mode - unknown attributes raise errors). + + See also: disable_unknown_attribute_tolerance() + """ + global _UNKNOWN_ATTRIBUTE_TOLERANCE + _UNKNOWN_ATTRIBUTE_TOLERANCE = True + + +def disable_unknown_attribute_tolerance(): + """Disable tolerance for unknown XML attributes (the default behavior). + + After calling this, unknown attributes in XML data will raise + FlexParserError as usual. + + See also: enable_unknown_attribute_tolerance() + """ + global _UNKNOWN_ATTRIBUTE_TOLERANCE + _UNKNOWN_ATTRIBUTE_TOLERANCE = False + + +def _get_known_attributes(Class): + """Get all known attribute names for a FlexElement subclass, + including inherited attributes from base classes. + """ + attrs = set() + for klass in Class.__mro__: + if hasattr(klass, '__annotations__'): + attrs.update(klass.__annotations__.keys()) + return attrs + + DataType = Union[ None, str, int, bool, decimal.Decimal, datetime.date, datetime.time, datetime.datetime, enums.EnumType, Tuple[str, ...], Tuple[enums.Code, ...] @@ -99,22 +149,40 @@ def parse_element_container(elem: ET.Element) -> Tuple[Types.FlexElement, ...]: return tuple(itertools.chain.from_iterable(fxlots)) instances = tuple(parse_data_element(child) for child in elem) + if _UNKNOWN_ATTRIBUTE_TOLERANCE: + instances = tuple(inst for inst in instances if inst is not None) return instances def parse_data_element( elem: ET.Element -) -> Types.FlexElement: +) -> Optional[Types.FlexElement]: """Parse an XML data element into a Types.FlexElement subclass instance. + + Returns None if unknown_attribute_tolerance is enabled and the element + type is not recognized. """ # Look up XML element's matching FlexElement subclass in ibflex.Types. - Class = getattr(Types, elem.tag) + try: + Class = getattr(Types, elem.tag) + except AttributeError: + if _UNKNOWN_ATTRIBUTE_TOLERANCE: + return None + raise + + # When tolerance is enabled, pre-compute known attributes and filter + known = _get_known_attributes(Class) if _UNKNOWN_ATTRIBUTE_TOLERANCE else None # Parse element attributes + if known is not None: + attrib_items = [(k, v) for k, v in elem.attrib.items() if k in known] + else: + attrib_items = list(elem.attrib.items()) + try: attrs = dict( parse_element_attr(Class, k, v) - for k, v in elem.attrib.items() + for k, v in attrib_items ) except KeyError as exc: msg = f"{Class.__name__} has no attribute " + str(exc) @@ -124,8 +192,18 @@ def parse_data_element( # that contain other data elements. contained_elements = {child.tag: parse_element(child) for child in elem} if contained_elements: - assert elem.tag in ("FlexQueryResponse", "FlexStatement") - attrs.update(contained_elements) + if _UNKNOWN_ATTRIBUTE_TOLERANCE: + if elem.tag in ("FlexQueryResponse", "FlexStatement"): + # Filter out unknown or unparseable contained elements + contained_elements = { + k: v for k, v in contained_elements.items() + if k in known and v is not None + } + attrs.update(contained_elements) + # else: tolerance is on, silently ignore children on other elements + else: + assert elem.tag in ("FlexQueryResponse", "FlexStatement") + attrs.update(contained_elements) try: return Class(**attrs) diff --git a/tests/test_parser.py b/tests/test_parser.py index 31fb665..26b20cc 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -538,5 +538,239 @@ def testMakeOptional(self): ) +class UnknownAttributeToleranceTestCase(unittest.TestCase): + """Tests for the unknown attribute tolerance feature. + + This feature allows the parser to silently ignore unknown XML attributes + and element types, which is useful when Interactive Brokers adds new fields + to their exports. + """ + + def setUp(self): + """Ensure tolerance is off before each test.""" + parser.disable_unknown_attribute_tolerance() + + def tearDown(self): + """Ensure tolerance is off after each test.""" + parser.disable_unknown_attribute_tolerance() + + def test_tolerance_default_off(self): + """Tolerance is off by default.""" + self.assertFalse(parser._UNKNOWN_ATTRIBUTE_TOLERANCE) + + def test_enable_disable(self): + """enable/disable functions toggle the flag correctly.""" + self.assertFalse(parser._UNKNOWN_ATTRIBUTE_TOLERANCE) + parser.enable_unknown_attribute_tolerance() + self.assertTrue(parser._UNKNOWN_ATTRIBUTE_TOLERANCE) + parser.disable_unknown_attribute_tolerance() + self.assertFalse(parser._UNKNOWN_ATTRIBUTE_TOLERANCE) + + def test_tolerance_functions_accessible_from_package(self): + """The tolerance functions are accessible from the top-level ibflex package. + + On the original upstream ibflex package these functions do not exist, + so calling ibflex.enable_unknown_attribute_tolerance() would raise + AttributeError, providing a clear version guard. + """ + import ibflex + self.assertTrue(hasattr(ibflex, 'enable_unknown_attribute_tolerance')) + self.assertTrue(hasattr(ibflex, 'disable_unknown_attribute_tolerance')) + # Verify they are callable + self.assertTrue(callable(ibflex.enable_unknown_attribute_tolerance)) + self.assertTrue(callable(ibflex.disable_unknown_attribute_tolerance)) + + def test_unknown_attr_raises_without_tolerance(self): + """Without tolerance, unknown XML attributes raise FlexParserError.""" + # AccountInformation with an unknown attribute "newIBField" + elem = ET.fromstring( + '' + ) + with self.assertRaises(parser.FlexParserError): + parser.parse_data_element(elem) + + def test_unknown_attr_ignored_with_tolerance(self): + """With tolerance enabled, unknown XML attributes are silently ignored.""" + parser.enable_unknown_attribute_tolerance() + + elem = ET.fromstring( + '' + ) + instance = parser.parse_data_element(elem) + self.assertIsInstance(instance, Types.AccountInformation) + self.assertEqual(instance.accountId, "U123456") + self.assertEqual(instance.currency, "USD") + # Unknown attributes are not present on the parsed object + self.assertFalse(hasattr(instance, 'newIBField')) + self.assertFalse(hasattr(instance, 'anotherNewField')) + + def test_known_attrs_still_parsed_with_tolerance(self): + """With tolerance, known attributes are still correctly parsed.""" + parser.enable_unknown_attribute_tolerance() + + elem = ET.fromstring( + '' + ) + instance = parser.parse_data_element(elem) + self.assertIsInstance(instance, Types.AccountInformation) + self.assertEqual(instance.accountId, "U123456") + self.assertEqual(instance.acctAlias, "test") + self.assertEqual(instance.currency, "USD") + self.assertEqual(instance.name, "Test User") + import datetime + self.assertEqual(instance.dateOpened, datetime.date(2020, 1, 15)) + + def test_disable_restores_strict_behavior(self): + """After disabling tolerance, unknown attributes raise errors again.""" + parser.enable_unknown_attribute_tolerance() + + elem = ET.fromstring( + '' + ) + # Should succeed with tolerance on + instance = parser.parse_data_element(elem) + self.assertIsInstance(instance, Types.AccountInformation) + + parser.disable_unknown_attribute_tolerance() + + # Should fail with tolerance off + with self.assertRaises(parser.FlexParserError): + parser.parse_data_element(elem) + + def test_unknown_element_type_raises_without_tolerance(self): + """Without tolerance, unknown XML element types raise AttributeError.""" + elem = ET.fromstring('') + with self.assertRaises(AttributeError): + parser.parse_data_element(elem) + + def test_unknown_element_type_returns_none_with_tolerance(self): + """With tolerance, unknown element types return None.""" + parser.enable_unknown_attribute_tolerance() + + elem = ET.fromstring('') + result = parser.parse_data_element(elem) + self.assertIsNone(result) + + def test_unknown_elements_filtered_in_container(self): + """With tolerance, unknown element types are filtered out of containers.""" + parser.enable_unknown_attribute_tolerance() + + container = ET.Element("Trades") + # Add a known Trade element + ET.SubElement(container, "Trade", attrib={ + "accountId": "U123456", + "currency": "USD", + "fxRateToBase": "1", + "assetCategory": "STK", + "symbol": "AAPL", + "description": "APPLE INC", + "conid": "265598", + "tradeID": "123", + "reportDate": "2020-01-15", + "tradeDate": "2020-01-15", + "quantity": "100", + "tradePrice": "150.00", + "tradeMoney": "15000.00", + "proceeds": "-15000.00", + "taxes": "0", + "ibCommission": "-1.00", + "ibCommissionCurrency": "USD", + "netCash": "-15001.00", + "buySell": "BUY", + }) + # Add an unknown element type + ET.SubElement(container, "BrandNewTradeType", attrib={ + "unknownField": "value" + }) + + result = parser.parse_element_container(container) + # Only the known Trade should be in the result + self.assertEqual(len(result), 1) + self.assertIsInstance(result[0], Types.Trade) + + def test_full_parse_with_unknown_attrs(self): + """Full round-trip: parse a FlexQueryResponse with unknown attributes.""" + parser.enable_unknown_attribute_tolerance() + + xml_data = ( + '' + '' + '' + '' + '' + ) + response = parser.parse(xml_data.encode()) + self.assertIsInstance(response, Types.FlexQueryResponse) + self.assertEqual(response.queryName, "test") + self.assertEqual(len(response.FlexStatements), 1) + stmt = response.FlexStatements[0] + self.assertEqual(stmt.accountId, "U123456") + + def test_full_parse_unknown_attrs_fails_without_tolerance(self): + """Without tolerance, unknown attributes in full parse raise errors.""" + xml_data = ( + '' + '' + '' + '' + '' + ) + with self.assertRaises(parser.FlexParserError): + parser.parse(xml_data.encode()) + + def test_unknown_contained_element_in_statement(self): + """With tolerance, unknown contained elements in FlexStatement are ignored.""" + parser.enable_unknown_attribute_tolerance() + + xml_data = ( + '' + '' + '' + '' + '' + '' + '' + '' + '' + ) + response = parser.parse(xml_data.encode()) + self.assertIsInstance(response, Types.FlexQueryResponse) + self.assertEqual(len(response.FlexStatements), 1) + self.assertEqual(response.FlexStatements[0].accountId, "U123456") + + def test_multiple_unknown_attrs_on_trade(self): + """Unknown attributes on Trade elements are ignored with tolerance.""" + parser.enable_unknown_attribute_tolerance() + + elem = ET.fromstring( + '' + ) + instance = parser.parse_data_element(elem) + self.assertIsInstance(instance, Types.Trade) + self.assertEqual(instance.symbol, "AAPL") + self.assertEqual(instance.tradeID, "123") + self.assertFalse(hasattr(instance, 'newField1')) + + if __name__ == '__main__': unittest.main(verbosity=3) From 18926db16728b714845f31ea360ba3f851524b47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:43:23 +0000 Subject: [PATCH 10/20] Initial plan From 0e62635ccf4be5ef5034a7f102645bbaebd30d28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:45:25 +0000 Subject: [PATCH 11/20] Mark package as vendored with version suffix and README notice Co-authored-by: vroonhof <38109466+vroonhof@users.noreply.github.com> --- README.rst | 9 +++++++++ ibflex/__version__.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2f66158..7319ea4 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,15 @@ Python parser for Interactive Brokers Flex XML statements ========================================================= +**VENDORED PACKAGE NOTICE** + +This is a vendored and enhanced version of the original ibflex library by Christopher Singley +(https://github.com/csingley/ibflex). This fork includes additional data fields beyond the +upstream version and incorporates improvements from robcohen/ibflex2. The version number +includes a "+vroonhof.vendored" suffix to distinguish it from the original package. + +--- + ``ibflex`` is a Python library for converting brokerage statement data in Interactive Brokers' Flex XML format into standard Python data structures, so it can be conveniently processed and analyzed with Python scripts. diff --git a/ibflex/__version__.py b/ibflex/__version__.py index 5cf7d39..a816cfe 100644 --- a/ibflex/__version__.py +++ b/ibflex/__version__.py @@ -16,8 +16,8 @@ __description__ = ( "Parse Interactive Brokers Flex XML reports and convert to Python types" ), -__url__ = "https://github.com/csingley/ibflex" -__version__ = "0.16" +__url__ = "https://github.com/vroonhof/ibflex" +__version__ = "0.16+vroonhof.vendored" __author__ = "Christopher Singley" __author_email__ = "csingley@gmail.com" __license__ = "MIT" From 3b8e76102b8e8eee035bd710bfc5719d2ea7368d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 21:50:12 +0000 Subject: [PATCH 12/20] Keep assertion that only FlexQueryResponse and FlexStatement contain other data elements The assertion fires in both strict and tolerant modes. When tolerance is on, unknown contained elements are filtered out after the assertion. Co-authored-by: vroonhof --- ibflex/parser.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/ibflex/parser.py b/ibflex/parser.py index 83f1620..f582187 100755 --- a/ibflex/parser.py +++ b/ibflex/parser.py @@ -192,18 +192,14 @@ def parse_data_element( # that contain other data elements. contained_elements = {child.tag: parse_element(child) for child in elem} if contained_elements: + assert elem.tag in ("FlexQueryResponse", "FlexStatement") if _UNKNOWN_ATTRIBUTE_TOLERANCE: - if elem.tag in ("FlexQueryResponse", "FlexStatement"): - # Filter out unknown or unparseable contained elements - contained_elements = { - k: v for k, v in contained_elements.items() - if k in known and v is not None - } - attrs.update(contained_elements) - # else: tolerance is on, silently ignore children on other elements - else: - assert elem.tag in ("FlexQueryResponse", "FlexStatement") - attrs.update(contained_elements) + # Filter out unknown or unparseable contained elements + contained_elements = { + k: v for k, v in contained_elements.items() + if k in known and v is not None + } + attrs.update(contained_elements) try: return Class(**attrs) From 7395223f0d705b90acaf5407984d8e7818534ce3 Mon Sep 17 00:00:00 2001 From: vroonhof <38109466+vroonhof@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:52:49 +0100 Subject: [PATCH 13/20] Revise README for vendored ibflex library details Updated README to clarify enhancements and features of the vendored ibflex library. --- README.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7319ea4..57e3fd9 100644 --- a/README.rst +++ b/README.rst @@ -3,13 +3,21 @@ Python parser for Interactive Brokers Flex XML statements ========================================================= **VENDORED PACKAGE NOTICE** +--------------------------- This is a vendored and enhanced version of the original ibflex library by Christopher Singley -(https://github.com/csingley/ibflex). This fork includes additional data fields beyond the -upstream version and incorporates improvements from robcohen/ibflex2. The version number +(https://github.com/csingley/ibflex). + +This fork + * includes additional data fields beyond the upstream version + * incorporates similar improvements from robcohen/ibflex2 + * includes an opt-in mechanism to be tolerant against IBKR adding new but unimportant attributes on a regular basis. + +The version number includes a "+vroonhof.vendored" suffix to distinguish it from the original package. ---- +**ORGINAL README** +------------------ ``ibflex`` is a Python library for converting brokerage statement data in Interactive Brokers' Flex XML format into standard Python data structures, From 3df5d4594a4e2c8a7c6b61d143d53be160e4be82 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:07:54 +0000 Subject: [PATCH 14/20] Add missing fields to EquitySummaryByReportDateInBase Added `liteSurchargeAccrualsLong`, `liteSurchargeAccrualsShort`, `cgtWithholdingAccruals`, `cgtWithholdingAccrualsLong`, and `cgtWithholdingAccrualsShort` to `EquitySummaryByReportDateInBase` dataclass in `ibflex/Types.py` to fix parsing errors reported in issue #132. Added a regression test `tests/test_equity_summary_fields.py` to verify the fix. Co-authored-by: vroonhof <38109466+vroonhof@users.noreply.github.com> --- ibflex/Types.py | 5 +++++ tests/test_equity_summary_fields.py | 35 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/test_equity_summary_fields.py diff --git a/ibflex/Types.py b/ibflex/Types.py index fb8f4b2..7f159e0 100644 --- a/ibflex/Types.py +++ b/ibflex/Types.py @@ -480,6 +480,11 @@ class EquitySummaryByReportDateInBase(FlexElement): cryptoLong: Optional[decimal.Decimal] = None cryptoShort: Optional[decimal.Decimal] = None liteSurchargeAccruals: Optional[decimal.Decimal] = None + liteSurchargeAccrualsLong: Optional[decimal.Decimal] = None + liteSurchargeAccrualsShort: Optional[decimal.Decimal] = None + cgtWithholdingAccruals: Optional[decimal.Decimal] = None + cgtWithholdingAccrualsLong: Optional[decimal.Decimal] = None + cgtWithholdingAccrualsShort: Optional[decimal.Decimal] = None @dataclass(frozen=True) diff --git a/tests/test_equity_summary_fields.py b/tests/test_equity_summary_fields.py new file mode 100644 index 0000000..56107b1 --- /dev/null +++ b/tests/test_equity_summary_fields.py @@ -0,0 +1,35 @@ +import unittest +from ibflex import parser + +class TestEquitySummaryNewFields(unittest.TestCase): + def test_new_fields_parsing(self): + xml_data = b""" + + + + + + + + + +""" + try: + response = parser.parse(xml_data) + self.assertIsInstance(response, parser.Types.FlexQueryResponse) + + equity_summary = response.FlexStatements[0].EquitySummaryInBase[0] + + # Check the new fields + self.assertEqual(equity_summary.liteSurchargeAccrualsLong, 0) + self.assertEqual(equity_summary.liteSurchargeAccrualsShort, 0) + self.assertEqual(equity_summary.cgtWithholdingAccruals, 0) + self.assertEqual(equity_summary.cgtWithholdingAccrualsLong, 0) + self.assertEqual(equity_summary.cgtWithholdingAccrualsShort, 0) + + print("\nSuccessfully parsed XML data and verified new fields") + except parser.FlexParserError as e: + self.fail(f"FlexParserError raised: {e}") + +if __name__ == '__main__': + unittest.main() From 3e771f4740a6a9c177828d51eb322ec7bc7eb731 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:16:50 +0000 Subject: [PATCH 15/20] Initial plan From ed5660b4e10c6f57081c4dcc5442c11f11f7c86e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:19:03 +0000 Subject: [PATCH 16/20] Add OTC transfer type support for crypto transfers Co-authored-by: vroonhof <38109466+vroonhof@users.noreply.github.com> --- ibflex/enums.py | 1 + tests/test_parser.py | 4 ++++ tests/test_types.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/ibflex/enums.py b/ibflex/enums.py index d5a4f3a..64a3120 100644 --- a/ibflex/enums.py +++ b/ibflex/enums.py @@ -236,6 +236,7 @@ class TransferType(str, enum.Enum): ACATS = "ACATS" ATON = "ATON" FOP = "FOP" + OTC = "OTC" @enum.unique diff --git a/tests/test_parser.py b/tests/test_parser.py index 26b20cc..a7b2dac 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -478,6 +478,10 @@ class TestEnum(enum.Enum): parser.convert_enum(enums.TransferType, "ACATS"), enums.TransferType.ACATS, ) + self.assertEqual( + parser.convert_enum(enums.TransferType, "OTC"), + enums.TransferType.OTC, + ) def testMakeOptional(self): """make_optional() wraps converter functions to return None for empty string. diff --git a/tests/test_types.py b/tests/test_types.py index 175eaa5..34279ea 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1138,6 +1138,38 @@ def testParse(self): self.assertEqual(instance.clientReference, None) +class TransferOTCTestCase(unittest.TestCase): + data = ET.fromstring( + ('') + ) + + def testParse(self): + instance = parser.parse_data_element(self.data) + self.assertIsInstance(instance, Types.Transfer) + self.assertEqual(instance.accountId, "U1234567") + self.assertEqual(instance.assetCategory, enums.AssetClass.CRYPTOCURRENCY) + self.assertEqual(instance.symbol, "BTC.USD-PAXOS") + self.assertEqual(instance.description, "Bitcoin cryptocurrency") + self.assertEqual(instance.type, enums.TransferType.OTC) + self.assertEqual(instance.direction, enums.InOut.OUT) + self.assertEqual(instance.quantity, decimal.Decimal("-0.5")) + self.assertEqual(instance.positionAmount, decimal.Decimal("-20000")) + self.assertEqual(instance.positionAmountInBase, decimal.Decimal("-16000")) + + + class TransferLotTestCase(unittest.TestCase): data = ET.fromstring( (' Date: Wed, 18 Feb 2026 20:08:22 +0000 Subject: [PATCH 17/20] Initial plan From 709fe30ab1884094ad518521c549db6e75b39f54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:11:45 +0000 Subject: [PATCH 18/20] Add FIFO (LF) code to Code enum with test Co-authored-by: vroonhof <38109466+vroonhof@users.noreply.github.com> --- ibflex/enums.py | 1 + tests/test_types.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/ibflex/enums.py b/ibflex/enums.py index 64a3120..ea063f6 100644 --- a/ibflex/enums.py +++ b/ibflex/enums.py @@ -77,6 +77,7 @@ class Code(str, enum.Enum): INVESTOR = "INV" # Investment Transfer from Investor MARGINLOW = "L" # Ordered by IB (Margin Violation) WASHSALE = "LD" # Adjusted by Loss Disallowed from Wash Sale + FIFO = "LF" # First In, First Out (FIFO) tax lot-matching method LIFO = "LI" # Last In, First Out (LIFO) tax lot-matching method LTCG = "LT" # Long-term P/L LOAN = "Lo" # Direct Loan diff --git a/tests/test_types.py b/tests/test_types.py index 34279ea..db1150b 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -757,6 +757,34 @@ def testParse(self): self.assertEqual(instance.assetCategory, enums.AssetClass.CASH) +class TradeFIFOTestCase(unittest.TestCase): + data = ET.fromstring( + ('') + ) + + def testParse(self): + instance = parser.parse_data_element(self.data) + self.assertIsInstance(instance, Types.Trade) + self.assertEqual(instance.currency, "USD") + self.assertEqual(instance.symbol, "AAPL") + self.assertEqual(instance.description, "APPLE INC") + self.assertEqual(instance.dateTime, datetime.datetime(2025, 1, 15, 14, 30, 45)) + self.assertEqual(instance.tradeDate, datetime.date(2025, 1, 15)) + self.assertEqual(instance.quantity, decimal.Decimal("10.0")) + self.assertEqual(instance.tradePrice, decimal.Decimal("150.0")) + self.assertEqual(instance.proceeds, decimal.Decimal("1500.0")) + self.assertEqual(instance.notes, (enums.Code.FIFO, )) + self.assertEqual(instance.buySell, enums.BuySell.SELL) + self.assertEqual(instance.levelOfDetail, "EXECUTION") + self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) + + class OptionEAETestCase(unittest.TestCase): data = ET.fromstring( (' Date: Wed, 18 Feb 2026 22:31:53 +0000 Subject: [PATCH 19/20] Fix LF enum naming: LIQUIDATION_FORCED not FIFO Co-authored-by: vroonhof <38109466+vroonhof@users.noreply.github.com> --- ibflex/enums.py | 2 +- tests/test_types.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ibflex/enums.py b/ibflex/enums.py index ea063f6..7b4a8ad 100644 --- a/ibflex/enums.py +++ b/ibflex/enums.py @@ -77,7 +77,7 @@ class Code(str, enum.Enum): INVESTOR = "INV" # Investment Transfer from Investor MARGINLOW = "L" # Ordered by IB (Margin Violation) WASHSALE = "LD" # Adjusted by Loss Disallowed from Wash Sale - FIFO = "LF" # First In, First Out (FIFO) tax lot-matching method + LIQUIDATION_FORCED = "LF" # Forced Liquidation LIFO = "LI" # Last In, First Out (LIFO) tax lot-matching method LTCG = "LT" # Long-term P/L LOAN = "Lo" # Direct Loan diff --git a/tests/test_types.py b/tests/test_types.py index db1150b..84d0819 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -757,7 +757,7 @@ def testParse(self): self.assertEqual(instance.assetCategory, enums.AssetClass.CASH) -class TradeFIFOTestCase(unittest.TestCase): +class TradeLiquidationForcedTestCase(unittest.TestCase): data = ET.fromstring( (' Date: Wed, 18 Feb 2026 22:32:33 +0000 Subject: [PATCH 20/20] Add docstring to TradeLiquidationForcedTestCase Co-authored-by: vroonhof <38109466+vroonhof@users.noreply.github.com> --- tests/test_types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_types.py b/tests/test_types.py index 84d0819..f1161ab 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -758,6 +758,7 @@ def testParse(self): class TradeLiquidationForcedTestCase(unittest.TestCase): + """Test parsing of Trade with LF (Forced Liquidation) notes code.""" data = ET.fromstring( ('