diff --git a/ibflex/Types.py b/ibflex/Types.py index 6ab7555..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) @@ -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 @@ -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) @@ -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) @@ -1385,6 +1390,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 @@ -1392,12 +1400,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 @@ -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) @@ -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) @@ -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) @@ -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 @@ -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): @@ -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) @@ -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 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 af6b270..1bf081a 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(", ", ",") @@ -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, @@ -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 ) 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)