Skip to content
75 changes: 68 additions & 7 deletions ibflex/Types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -709,6 +710,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
Expand Down Expand Up @@ -1144,7 +1147,8 @@ 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


@dataclass(frozen=True)
Expand Down Expand Up @@ -1296,7 +1300,8 @@ 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


@dataclass(frozen=True)
Expand Down Expand Up @@ -1385,19 +1390,29 @@ 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
clearingFirmID: Optional[str] = None
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
Expand Down Expand Up @@ -1427,6 +1442,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[bool] = None
mtmPnl: Optional[decimal.Decimal] = None
netCash: Optional[decimal.Decimal] = None
netCashInBase: Optional[decimal.Decimal] = None
notes: Optional[str] = None
openCloseIndicator: Optional[enums.OpenClose] = None
openDateTime: Optional[datetime.datetime] = None
origOrderID: Optional[str] = None
rtn: Optional[str] = None
serialNumber: Optional[str] = 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)
Expand Down Expand Up @@ -1535,7 +1584,8 @@ 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


@dataclass(frozen=True)
Expand Down Expand Up @@ -1638,12 +1688,13 @@ 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
fineness: Optional[decimal.Decimal] = None
weight: Optional[str] = None
positionActionID: Optional[str] = None


@dataclass(frozen=True)
Expand Down Expand Up @@ -1796,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
Expand Down Expand Up @@ -2122,7 +2173,12 @@ 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
issuerCountryCode: Optional[str] = None
levelOfDetail: Optional[str] = None
positionInstructionID: Optional[str] = None
positionInstructionSetID: Optional[str] = None

@dataclass(frozen=True)
class UnsettledTransfer(FlexElement):
Expand Down Expand Up @@ -2245,6 +2301,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)
Expand Down Expand Up @@ -2480,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
Expand Down
2 changes: 0 additions & 2 deletions ibflex/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
13 changes: 9 additions & 4 deletions ibflex/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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(", ", ",")

Expand Down Expand Up @@ -328,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,
Expand Down Expand Up @@ -463,6 +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", # Russian-related currency code used by IBKR
)


Expand Down
4 changes: 3 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
44 changes: 44 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
('<Trade accountId="U123456" currency="USD" assetCategory="STK" '
'symbol="TEST" initialInvestment="Yes" quantity="100" />')
)

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(
('<EquitySummaryByReportDateInBase accountId="U123456" '
'reportDate="2024-01-01" cash="1000.00" total="1000.00" '
'liteSurchargeAccruals="5.50" />')
)

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)