Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: debug-statements
- id: double-quote-string-fixer
- id: name-tests-test
- id: requirements-txt-fixer
- repo: local
hooks:
- id: ruff-check
name: ruff check
entry: ruff check --force-exclude
language: system
types: [python]
- id: ruff-format
name: ruff format
entry: ruff format --force-exclude
language: system
types: [python]
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
`Decimal` typing was incorrect and would raise `FlexParserError` on real-world data. Code
doing arithmetic comparisons (`if trade.initialInvestment > 0:`) needs to be updated; under
Python's `True > 0` semantics it would silently misbehave with the new type.
- type `FlexStatement.TransactionTaxes` as `Tuple[Union[TransactionTax, TransactionTaxDetail], ...]`
instead of bare `Tuple`

### Added

Expand All @@ -31,6 +33,9 @@
faster than the type definitions can be updated. Off by default; toggle with
`disable_unknown_attribute_tolerance()`. Note: the flag is module-level global state and
is **not thread-safe**.
- New subCategory plus newer IB attributes on `TransactionTaxDetail` (`figi`,
`issuerCountryCode`, `settleDate`, `orderId`, `serialNumber`, `deliveryType`, `commodityType`,
`fineness`, `weight`)

### Fixed

Expand All @@ -57,6 +62,7 @@
trailing-TZ-offset values.
- CI matrix expanded to `ubuntu-latest`, `macos-latest`, and `windows-latest`
across Python 3.9-3.13.
- regression test for parsing `TransactionTaxDetail` with `subCategory`/`figi`/`issuerCountryCode`

### Docs

Expand Down
14 changes: 12 additions & 2 deletions ibflex/Types.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class decorator. Class attributes are annotated with PEP 484 type hints.
import datetime
import decimal
from dataclasses import dataclass
from typing import Optional, Tuple
from typing import Optional, Tuple, Union

from ibflex import enums

Expand Down Expand Up @@ -153,7 +153,7 @@ class FlexStatement(FlexElement):
Trades: Tuple["Trade", ...] = ()
HKIPOSubscriptionActivity: Tuple = () # TODO
TradeConfirms: Tuple["TradeConfirm", ...] = ()
TransactionTaxes: Tuple = ()
TransactionTaxes: Tuple[Union["TransactionTax", "TransactionTaxDetail"], ...] = ()
OptionEAE: Tuple["_OptionEAE", ...] = ()
# Not a typo - they really spell it "Excercises"
PendingExcercises: Tuple = () # TODO
Expand Down Expand Up @@ -2755,6 +2755,7 @@ class TransactionTaxDetail(FlexElement):
currency: Optional[str] = None
fxRateToBase: Optional[decimal.Decimal] = None
assetCategory: Optional[enums.AssetClass] = None
subCategory: Optional[str] = None
symbol: Optional[str] = None
description: Optional[str] = None
conid: Optional[str] = None
Expand Down Expand Up @@ -2783,6 +2784,15 @@ class TransactionTaxDetail(FlexElement):
source: Optional[str] = None
code: Tuple[enums.Code, ...] = ()
levelOfDetail: Optional[str] = None
figi: Optional[str] = None
issuerCountryCode: Optional[str] = None
settleDate: Optional[datetime.date] = None
orderId: Optional[str] = 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)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ dev = [
"types-defusedxml",
"build",
"requests",
"pre-commit",
]

[project.scripts]
Expand Down
4 changes: 3 additions & 1 deletion scripts/check_schema_drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# Add project root to path so this runs from `python scripts/check_schema_drift.py`.
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from ibflex import Types # noqa: E402
from ibflex import Types

REFERENCE_URL = (
"https://www.interactivebrokers.com/en/software/reportguide/"
Expand All @@ -43,6 +43,7 @@


def fetch_reference_html(url: str = REFERENCE_URL) -> str:
"""Fetch data from URL."""
req = urllib.request.Request(
url, headers={"User-Agent": "ibflex2-schema-drift/1.0"}
)
Expand Down Expand Up @@ -78,6 +79,7 @@ def known_attributes() -> set[str]:


def main() -> int:
"""Checks whether the schema on IBKR side has changed compared to the maps."""
html = fetch_reference_html()
candidates = extract_candidate_fields(html)
known = known_attributes()
Expand Down
31 changes: 31 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2104,5 +2104,36 @@ def testParse(self):
self.assertEqual(instance.liteSurchargeAccruals, decimal.Decimal("5.50"))



class TransactionTaxDetailSubCategoryTestCase(unittest.TestCase):
"""Ensure TransactionTaxDetail accepts subCategory from Flex XML."""

data = ET.fromstring(
(
'<TransactionTaxDetail accountId="U123456" acctAlias="" model="" '
'currency="AUD" fxRateToBase="1" assetCategory="STK" subCategory="COMMON" '
'symbol="ABC" description="Tax detail sample" conid="12345" '
'securityID="AU000000ABC1" securityIDType="ISIN" cusip="" isin="AU000000ABC1" figi="BBG000ABC123" issuerCountryCode="AU" '
'listingExchange="ASX" underlyingConid="" underlyingSecurityID="" '
'underlyingSymbol="ABC" underlyingListingExchange="ASX" issuer="" '
'multiplier="1" strike="" expiry="" putCall="" principalAdjustFactor="" '
'date="2026-05-17" taxDescription="Sample tax" quantity="10" '
'reportDate="2026-05-17" taxAmount="-1.23" tradeId="1" tradePrice="10" '
'source="IB" code="A" levelOfDetail="DETAIL" />'
)
)

def testParse(self):
instance = parser.parse_data_element(self.data)
self.assertIsInstance(instance, Types.TransactionTaxDetail)
self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK)
self.assertEqual(instance.subCategory, "COMMON")
self.assertEqual(instance.symbol, "ABC")
self.assertEqual(instance.figi, "BBG000ABC123")
self.assertEqual(instance.issuerCountryCode, "AU")
self.assertEqual(instance.taxAmount, decimal.Decimal("-1.23"))

if __name__ == '__main__':
unittest.main(verbosity=3)