├── .github └── workflows │ └── commit-ibflex.yml ├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.rst ├── ibflex ├── Types.py ├── __init__.py ├── __version__.py ├── client.py ├── enums.py ├── parser.py ├── py.typed └── utils.py ├── requirements-development.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── test_client.py ├── test_parser.py └── test_types.py /.github/workflows/commit-ibflex.yml: -------------------------------------------------------------------------------- 1 | name: commit-ibflex 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.7, 3.8] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | # - name: Cache pip 20 | # uses: actions/cache@v2 21 | # with: 22 | # path: ~/.cache/pip # This path is specific to Ubuntu 23 | # # Look to see if there is a cache hit for the corresponding requirements file 24 | # key: ${{ runner.os }}-pip-${{ hashFiles('requirements-development.txt') }} 25 | # restore-keys: | 26 | # ${{ runner.os }}-pip- 27 | # ${{ runner.os }}- 28 | - name: Install dependencies 29 | run: pip install -r requirements-development.txt 30 | - name: Static analysis with mypy 31 | run: mypy ibflex tests 32 | - name: Test with nose 33 | run: nosetests -dsv --with-coverage --cover-package ibflex tests/*.py 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.db-journal 3 | *.egg-info 4 | *.egg-info/ 5 | *.pyc 6 | *~ 7 | .*.sw? 8 | .coverage 9 | .directory 10 | .env 11 | .idea/* 12 | .tox 13 | MANIFEST 14 | build 15 | dist 16 | reg_settings.py 17 | .mypy_cache/ 18 | .ycm_extra_conf.py 19 | /venv/ 20 | **/.DS_Store 21 | .vscode/ 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Christopher Singley 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | coverage erase 3 | mypy ibflex 4 | mypy tests 5 | python `which nosetests` -dsv --with-coverage --cover-package ibflex tests/*.py 6 | 7 | clean: 8 | find -regex '.*\.pyc' -exec rm {} \; 9 | find -regex '.*~' -exec rm {} \; 10 | rm -rf reg-settings.py 11 | rm -rf MANIFEST dist build *.egg-info 12 | rm -rf test.db 13 | 14 | install: 15 | make clean 16 | make uninstall 17 | python setup.py install 18 | 19 | uninstall: 20 | pip uninstall -y ibflex 21 | 22 | lint: 23 | pylint ibflex/*.py 24 | 25 | lint-tests: 26 | pylint tests/*.py 27 | 28 | .PHONY: test clean lint lint-tests install uninstall 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================================================= 2 | Python parser for Interactive Brokers Flex XML statements 3 | ========================================================= 4 | 5 | ``ibflex`` is a Python library for converting brokerage statement data in 6 | Interactive Brokers' Flex XML format into standard Python data structures, 7 | so it can be conveniently processed and analyzed with Python scripts. 8 | 9 | *N.B. This module has nothing to do with programmatic trading. 10 | It's about reading brokerage reports.* 11 | 12 | ``ibflex`` is compatible with Python version 3.7+. The parser has no 13 | dependencies beyond the Python standard library (although the optional client 14 | for fetching Flex Statements from Interactive Brokers' server does depend 15 | on `requests`_ ). 16 | 17 | **This module is alpha software!** It works and it's useful, but the 18 | API, data structures, etc. are likely to see major changes. Several XML 19 | schemata are missing, and a few of the more newly-introduced attributes 20 | for the existing schemata. There are probably bugs. 21 | 22 | `Pull requests`_ are welcome. If you're submitting a pull request for an updated 23 | type, please do me a favor and include a test case based on your real-world data 24 | (censored to remove any personal information) in `tests.test_types.py`. You should 25 | easily be able to cut&paste one of the existing `unittest.TestCase` subclasses in 26 | that file and adapt it to your own data. Thanks very much; my own datastream does 27 | not have full coverage of the object model! 28 | 29 | 30 | Installation 31 | ============ 32 | :: 33 | 34 | pip install ibflex 35 | 36 | 37 | Flex Parser 38 | =========== 39 | The primary facility provided is the ``ibflex.parser`` module, which parses 40 | Flex-format XML data into a hierarchy of Python objects whose structure 41 | corresponds to that of the original Flex statements, with the data converted 42 | into appropriate Python types (datetime.datetime, decimal.Decimal, etc.) 43 | 44 | Usage example: 45 | 46 | .. code:: python 47 | 48 | In [1]: from ibflex import parser 49 | 50 | In [2]: response = parser.parse("2017-01_ibkr.xml") 51 | 52 | In [3]: response 53 | Out[3]: FlexQueryResponse(queryName='SCP Everything', type='AF', len(FlexStatements)=1) 54 | 55 | In [4]: stmt = response.FlexStatements[0] 56 | 57 | In [5]: stmt 58 | Out[5]: FlexStatement(accountId='U770993', fromDate=datetime.date(2017, 1, 2), toDate=datetime.date(2017, 1, 31), period=None, whenGenerated=datetime.datetime(2017, 5, 10, 11, 41, 38), len(CashReport)=3, len(EquitySummaryInBase)=23, len(StmtFunds)=344, len(ChangeInPositionValues)=2, len(OpenPositions)=2140, len(FxPositions)=1, len(Trades)=339, len(CorporateActions)=1, len(CashTransactions)=4, len(InterestAccruals)=1, len(ChangeInDividendAccruals)=5, len(OpenDividendAccruals)=2, len(SecuritiesInfo)=30, len(ConversionRates)=550) 59 | 60 | In [6]: trade = stmt.Trades[-1] 61 | 62 | In [7]: trade 63 | Out[7]: Trade(transactionType=, openCloseIndicator=, buySell=, orderType=, assetCategory=, accountId='U770993', currency='USD', fxRateToBase=Decimal('1'), symbol='WMIH', description='WMIH CORP', conid='105068604', cusip=None, isin=None, listingExchange=None, multiplier=Decimal('1'), strike=None, expiry=None, putCall=None, tradeID='1742757182', reportDate=datetime.date(2017, 1, 30), tradeDate=datetime.date(2017, 1, 30), tradeTime=datetime.time(15, 39, 36), settleDateTarget=datetime.date(2017, 2, 2), exchange='BYX', quantity=Decimal('-8'), tradePrice=Decimal('1.4'), tradeMoney=Decimal('-11.2'), taxes=Decimal('0'), ibCommission=Decimal('-0.00680792'), ibCommissionCurrency='USD', netCash=Decimal('11.19319208'), netCashInBase=None, closePrice=Decimal('1.4'), notes=(,), cost=Decimal('-10.853621'), mtmPnl=Decimal('0'), origTradePrice=Decimal('0'), origTradeDate=None, origTradeID=None, origOrderID='0', openDateTime=None, fifoPnlRealized=Decimal('0.339571'), capitalGainsPnl=None, levelOfDetail='EXECUTION', ibOrderID='865480117', orderTime=datetime.datetime(2017, 1, 30, 15, 39, 36), changeInPrice=Decimal('0'), changeInQuantity=Decimal('0'), proceeds=Decimal('11.2'), fxPnl=Decimal('0'), clearingFirmID=None, transactionID='7248583136', holdingPeriodDateTime=None, ibExecID='0001090f.588f449a.01.01', brokerageOrderID=None, orderReference=None, volatilityOrderLink=None, exchOrderId=None, extExecID='S2367553204796', traderID=None, isAPIOrder=False, acctAlias='SCP 0-0', model=None, securityID=None, securityIDType=None, principalAdjustFactor=None, dateTime=None, underlyingConid=None, underlyingSecurityID=None, underlyingSymbol=None, underlyingListingExchange=None, issuer=None, sedol=None, whenRealized=None, whenReopened=None) 64 | 65 | In [8]: print(f"{trade.tradeDate} {trade.buySell.name} {abs(trade.quantity)} {trade.symbol} @ {trade.tradePrice} {trade.currency}") 66 | 2017-01-30 SELL 8 WMIH @ 1.4 USD 67 | 68 | In [9]: pos = stmt.OpenPositions[-1] 69 | 70 | In [10]: pos 71 | Out[10]: OpenPosition(side=, assetCategory=, accountId='U770993', currency='USD', fxRateToBase=Decimal('1'), reportDate=datetime.date(2017, 1, 31), symbol='VXX', description='IPATH S&P 500 VIX S/T FU ETN', conid='242500577', securityID=None, cusip=None, isin=None, multiplier=Decimal('1'), position=Decimal('-75'), markPrice=Decimal('19.42'), positionValue=Decimal('-1456.5'), openPrice=Decimal('109.210703693'), costBasisPrice=Decimal('109.210703693'), costBasisMoney=Decimal('-8190.802777'), fifoPnlUnrealized=Decimal('6734.302777'), levelOfDetail='LOT', openDateTime=datetime.datetime(2015, 8, 24, 9, 28, 9), holdingPeriodDateTime=datetime.datetime(2015, 8, 24, 9, 28, 9), securityIDType=None, issuer=None, underlyingConid=None, underlyingSymbol=None, code=(), originatingOrderID='699501861', originatingTransactionID='5634129129', accruedInt=None, acctAlias='SCP 0-0', model=None, sedol=None, percentOfNAV=None, strike=None, expiry=None, putCall=None, principalAdjustFactor=None, listingExchange=None, underlyingSecurityID=None, underlyingListingExchange=None, positionValueInBase=None, unrealizedCapitalGainsPnl=None, unrealizedlFxPnl=None) 72 | 73 | In [11]: print(f"{trade.tradeDate} {trade.buySell.name} {abs(trade.quantity)} {trade.symbol} @ {trade.tradePrice} {trade.currency}") 74 | 2017-01-30 SELL 8 WMIH @ 1.4 USD 75 | 76 | In [12]: [sec for sec in stmt.SecuritiesInfo if sec.conid == trade.conid][0] 77 | Out[12]: SecurityInfo(assetCategory=, symbol='WMIH', description='WMIH CORP', conid='105068604', securityID=None, cusip=None, isin=None, listingExchange=None, underlyingSecurityID=None, underlyingListingExchange=None, underlyingConid=None, underlyingCategory=None, subCategory=None, multiplier=Decimal('1'), strike=None, expiry=None, maturity=None, issueDate=None, type=None, sedol=None, securityIDType=None, underlyingSymbol=None, issuer=None, putCall=None, principalAdjustFactor=Decimal('1'), code=()) 78 | 79 | 80 | Flex Query Report Configuration 81 | =============================== 82 | Configure Flex statements through `Interactive Brokers account management`_ . 83 | Reports > Flex Queries > Custom Flex Queries > Configure 84 | 85 | You can configure whatever you like and ibflex should parse it, with these exceptions: 86 | 87 | * You can't use European-style date formats (dd/MM/yy or dd/MM/yyyy). 88 | Just accept the default (yyyyMMdd) or get with the program and use ISO-8601 (yyyy-MM-dd). 89 | 90 | * You should use some delimiter between dates & times. The default delimiter 91 | (semicolon) is fastest to process. 92 | 93 | * For the Trades section of the statement, you can't select the options at the 94 | top for "Symbol Summary", "Asset Class", or "Orders". These will blow up 95 | the parser. It's fine to check the box for "Asset Class" down below, along 96 | with the other selections for XML attributes. 97 | 98 | 99 | Flex Client 100 | =========== 101 | Once you've defined various Flex queries, you can generate an access token 102 | that will allow you to generate statements and download them through the web 103 | API, instead of logging in to get them. 104 | 105 | Reports > Settings > FlexWeb Service 106 | 107 | Once you've got that set up - armed with the token, and the ID# of the desired 108 | Flex query - ``ibflex.client`` contains the facilities necessary to retrieve 109 | them: 110 | 111 | .. code:: python 112 | 113 | In [1]: from ibflex import client 114 | In [2]: token = '111111111111111111111111' 115 | In [3]: query_id = '111111' 116 | In [4]: response = client.download(token, query_id) 117 | In [5]: response[:215] 118 | Out[5]: b'\n\n\n' 119 | 120 | 121 | You can also just execute client.main() as a script: 122 | 123 | .. code:: bash 124 | 125 | $ python client.py -t 111111111111111111111111 -q 111111 > 2018-01_ibkr.xml 126 | 127 | 128 | Finally, setup.py installs a script at ``~/.local/bin/flexget``... cron-tastic! 129 | 130 | .. code:: bash 131 | 132 | $ flexget -t 111111111111111111111111 -q 111111 > 2018-01_ibkr.xml 133 | 134 | 135 | Resources 136 | ========= 137 | * Interactive Brokers `Activity Flex Query Reference`_ 138 | * Interactive Brokers `FlexWeb Service Reference`_ 139 | * `capgains`_ - package that uses ibflex (inter alia) to calculate realized gains 140 | * `ib-flex-analyzer`_ - Analyze your Interactive Brokers Flex XML reports with pandas 141 | 142 | .. _Pull requests: https://github.com/csingley/ibflex/pull/new/master 143 | .. _requests: https://github.com/requests/requests 144 | .. _Interactive Brokers account management: https://gdcdyn.interactivebrokers.com/sso/Login 145 | .. _Activity Flex Query Reference: https://www.interactivebrokers.com/en/software/reportguide/reportguide.htm#reportguide/activity_flex_query_reference.htm 146 | .. _FlexWeb Service Reference: https://www.interactivebrokers.com/en/software/am/am/reports/flex_web_service_version_3.htm 147 | .. _capgains: https://github.com/csingley/capgains 148 | .. _ib-flex-analyzer: https://github.com/wesm/ib-flex-analyzer 149 | -------------------------------------------------------------------------------- /ibflex/__init__.py: -------------------------------------------------------------------------------- 1 | from . import enums 2 | from .enums import * 3 | from . import Types 4 | from .Types import * 5 | from . import parser 6 | from .parser import parse 7 | from . import utils 8 | from . import client 9 | 10 | from .__version__ import ( 11 | __title__, 12 | __description__, 13 | __url__, 14 | __version__, 15 | __author__, 16 | __author_email__, 17 | __license__, 18 | __copyright__, 19 | ) 20 | -------------------------------------------------------------------------------- /ibflex/__version__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get a job, save your money, listen to Jane 3 | Everybody knows umbrellas cost more in the rain 4 | All the news is bad 5 | Is there any other kind? 6 | Everybody's talking at the same time 7 | 8 | It's hard times for some 9 | For others it's sweet 10 | Someone makes money when there's blood in the street 11 | Don't take any lip 12 | Stay in line 13 | Everybody's talking at the same time 14 | """ 15 | __title__ = "ibflex" 16 | __description__ = ( 17 | "Parse Interactive Brokers Flex XML reports and convert to Python types" 18 | ), 19 | __url__ = "https://github.com/csingley/ibflex" 20 | __version__ = "0.16" 21 | __author__ = "Christopher Singley" 22 | __author_email__ = "csingley@gmail.com" 23 | __license__ = "MIT" 24 | __copyright__ = "Copyright 2017 Christopher Singley" 25 | -------------------------------------------------------------------------------- /ibflex/client.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Download Flex queries without logging into Account Management web page. 4 | 5 | https://www.interactivebrokers.com/en/software/am/am/reports/flex_web_service_version_3.htm 6 | """ 7 | # stdlib imports 8 | from dataclasses import dataclass 9 | import xml.etree.ElementTree as ET 10 | from datetime import datetime 11 | import time 12 | from typing import Union, Optional 13 | 14 | 15 | # 3rd party imports 16 | import requests 17 | 18 | 19 | ############################################################################### 20 | # SERVICE LOCATIONS 21 | ############################################################################### 22 | FLEX_URL = 'https://gdcdyn.interactivebrokers.com/Universal/servlet/' 23 | REQUEST_URL = FLEX_URL + 'FlexStatementService.SendRequest' 24 | STMT_URL = FLEX_URL + 'FlexStatementService.GetStatement' 25 | 26 | 27 | ############################################################################### 28 | # ERRORS 29 | ############################################################################### 30 | ERRORS = [ 31 | ("1003", "Statement is not available."), 32 | ("1004", "Statement is incomplete at this time. Please try again shortly."), 33 | ("1005", "Settlement data is not ready at this time. Please try again shortly."), 34 | ("1006", "FIFO P/L data is not ready at this time. Please try again shortly."), 35 | ("1007", "MTM P/L data is not ready at this time. Please try again shortly."), 36 | ("1008", "MTM and FIFO P/L data is not ready at this time. Please try again shortly."), 37 | ("1009", "The server is under heavy load. Statement could not be generated at this time. Please try again shortly."), 38 | ("1010", "Legacy Flex Queries are no longer supported. Please convert over to Activity Flex."), 39 | ("1011", "Service account is inactive."), 40 | ("1012", "Token has expired."), 41 | ("1013", "IP restriction."), 42 | ("1014", "Query is invalid."), 43 | ("1015", "Token is invalid."), 44 | ("1016", "Account in invalid."), 45 | ("1017", "Reference code is invalid."), 46 | ("1018", "Too many requests have been made from this token. Please try again shortly."), 47 | ("1019", "Statement generation in progress. Please try again shortly."), 48 | ("1020", "Invalid request or unable to validate request."), 49 | ("1021", "Statement could not be retrieved at this time. Please try again shortly."), 50 | ] 51 | ERROR_CODES, ERROR_MSGS = zip(*ERRORS) 52 | 53 | SERVER_BUSY = ("1009", "1019", ) 54 | CLIENT_THROTTLED = ("1018", ) 55 | 56 | 57 | class IbflexClientError(Exception): 58 | """ Base class for Exceptions defined in this module """ 59 | 60 | 61 | class BadResponseError(IbflexClientError): 62 | """ 63 | Exception raised for malformed Flex response. 64 | """ 65 | def __init__(self, response: requests.Response): 66 | self.response = response 67 | super(BadResponseError, self).__init__(response.content) 68 | 69 | 70 | class StatementGenerationTimeout(IbflexClientError): 71 | """ Exception raised when the Flex server says it is generating the response, 72 | but does not finish generating the statement in a timely fashion. 73 | """ 74 | 75 | 76 | class ResponseCodeError(IbflexClientError): 77 | """ 78 | Exception raised when Flex server returns a response with an error code. 79 | """ 80 | def __init__(self, code: str, msg: str): 81 | self.code = code 82 | self.msg = msg 83 | super(ResponseCodeError, self).__init__(f"Code={code}: {msg}") 84 | 85 | 86 | ############################################################################### 87 | # FlexStatementResponse TYPES 88 | ############################################################################### 89 | @dataclass(frozen=True) 90 | class StatementAccess: 91 | timestamp: datetime 92 | ReferenceCode: str 93 | Url: str 94 | 95 | 96 | @dataclass(frozen=True) 97 | class StatementError: 98 | timestamp: datetime 99 | ErrorCode: str 100 | ErrorMessage: str 101 | 102 | 103 | ############################################################################### 104 | # FUNCTIONS 105 | ############################################################################### 106 | def download(token: str, query_id: str, max_tries: Optional[int] = 5) -> bytes: 107 | """2-step FlexQueryReport download process. 108 | 109 | Args: 110 | token: Current access token from Reports > Settings > FlexWeb Service. 111 | query_id: Flex Query ID from 112 | Reports > Flex Queries > Custom Flex Queries > Configure. 113 | """ 114 | stmt_access = request_statement(token, query_id) 115 | status = 0 116 | tries = 0 117 | while status is not True: 118 | time.sleep(status) 119 | tries += 1 120 | response = submit_request( 121 | url=stmt_access.Url or STMT_URL, 122 | token=token, 123 | query=stmt_access.ReferenceCode, 124 | ) 125 | status = check_statement_response(response) 126 | if max_tries and tries > max_tries: 127 | raise StatementGenerationTimeout( 128 | "Exceeded max number of tries while attempting download" 129 | ) 130 | return response.content 131 | 132 | 133 | def request_statement( 134 | token: str, query_id: str, url: Optional[str] = None 135 | ) -> StatementAccess: 136 | """First part of the 2-step download process. 137 | """ 138 | url = url or REQUEST_URL 139 | ### AKE FIX 140 | url = 'https://ndcdyn.interactivebrokers.com/portal.flexweb/api/v1/flexQuery' 141 | response = submit_request(url, token, query=query_id) 142 | stmt_access = parse_stmt_response(response) 143 | if isinstance(stmt_access, StatementError): 144 | raise ResponseCodeError( 145 | stmt_access.ErrorCode, 146 | stmt_access.ErrorMessage, 147 | ) 148 | return stmt_access 149 | 150 | 151 | def submit_request(url: str, token: str, query: str) -> requests.Response: 152 | """Post a query to an API access point, along with an authentication token. 153 | 154 | Retry with a progressive timeout window. 155 | """ 156 | MAX_REQUESTS = 3 157 | TIMEOUT_INCREMENT = 5 158 | 159 | response = None 160 | req_count = 1 161 | while (not response): 162 | try: 163 | response = requests.get( 164 | url, 165 | params={"v": "3", "t": token, "q": query}, 166 | headers={"user-agent": "Java"}, 167 | timeout=req_count * TIMEOUT_INCREMENT, 168 | ) 169 | except requests.exceptions.Timeout: 170 | if req_count >= MAX_REQUESTS: 171 | raise 172 | else: 173 | print("Request Timeout, re-sending...") 174 | req_count += 1 175 | 176 | return response 177 | 178 | 179 | def parse_stmt_response( 180 | response: requests.Response 181 | ) -> Union[StatementAccess, StatementError]: 182 | """Read 1st step response; parse into StatementAccess or StatementError. 183 | """ 184 | try: 185 | elem = ET.fromstring(response.content) 186 | assert elem.tag == 'FlexStatementResponse' 187 | 188 | timestamp = elem.attrib['timestamp'] 189 | # Convert "EST"/"EDT" to UTC offset so datetime.strptime can understand 190 | tz = {'EST': '-0500', 'EDT': '-0400'}[timestamp[-3:]] 191 | timestamp = timestamp[:-3] + tz 192 | datetime_ = datetime.strptime(timestamp, '%d %B, %Y %I:%M %p %z') 193 | 194 | data = {child.tag: child.text for child in elem} 195 | status = data.pop("Status") 196 | assert status in {"Success", "Fail", "Warn"} 197 | Type = { 198 | "Success": StatementAccess, 199 | "Fail": StatementError, 200 | "Warn": StatementError, 201 | }[status] 202 | return Type(timestamp=datetime_, **data) # type: ignore 203 | except Exception: 204 | raise BadResponseError(response=response) 205 | 206 | 207 | def check_statement_response(response: requests.Response) -> Union[bool, int]: 208 | """Validate response received from 2nd step of download. 209 | 210 | Returns: 211 | True if `response` contains a FlexQueryResponse. 212 | Retry delay (seconds) if `response` is an error indicating that 213 | 'please try again shortly' means 'within several seconds'. 214 | 215 | Raises: 216 | ResponseCodeError if `response` is any other kind of error. 217 | BadResponseError if we can't parse `response`. 218 | """ 219 | # FlexQueryResponses can be massive; avoid parsing them. 220 | resp_str = str(response.content) 221 | if 'FlexQueryResponse' in resp_str: 222 | return True 223 | elif 'FlexStatementResponse' in resp_str: 224 | try: 225 | error = parse_stmt_response(response) 226 | assert isinstance(error, StatementError) 227 | except Exception: 228 | raise BadResponseError(response) 229 | if error.ErrorCode in SERVER_BUSY: 230 | # Statement generation in progress. Please try again shortly. 231 | return 5 232 | elif error.ErrorCode in CLIENT_THROTTLED: 233 | # Too many requests have been made from this token. Please try again shortly. 234 | return 10 235 | else: 236 | raise ResponseCodeError(error.ErrorCode, error.ErrorMessage) 237 | else: 238 | raise BadResponseError(response) 239 | 240 | 241 | ############################################################################## 242 | # CLI SCRIPT 243 | ############################################################################### 244 | def main(): 245 | from argparse import ArgumentParser 246 | description = 'Download Flex brokerage statement from Interactive Brokers' 247 | argparser = ArgumentParser(description=description) 248 | argparser.add_argument('--token', '-t', required=True, 249 | help='Current Flex Web Service token') 250 | argparser.add_argument('--query', '-q', required=True, 251 | help='Flex Query ID#') 252 | args = argparser.parse_args() 253 | 254 | statement = download(args.token, args.query) 255 | print(statement.decode()) 256 | 257 | 258 | if __name__ == '__main__': 259 | main() 260 | -------------------------------------------------------------------------------- /ibflex/enums.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Enumerated values (Python enum.Enum subclasses) used to define ibflex.Types. 3 | 4 | Values are the text sent by IB in XML element attribute. 5 | Names keep the convention of using UPPERCASE for Enums. 6 | 7 | When creating a new Enum subclass, be sure to add it to ENUMS 8 | and EnumType (as Optional type) at the end of the file. 9 | """ 10 | 11 | __all__ = [ 12 | "CashAction", 13 | "Code", 14 | "AssetClass", 15 | "TradeType", 16 | "BuySell", 17 | "OpenClose", 18 | "OrderType", 19 | "Reorg", 20 | "OptionAction", 21 | "LongShort", 22 | "TransferType", 23 | "ToFrom", 24 | "InOut", 25 | "DeliveredReceived", 26 | "ENUMS", 27 | "EnumType", 28 | "PutCall" 29 | ] 30 | 31 | import enum 32 | import typing 33 | 34 | 35 | @enum.unique 36 | class CashAction(str, enum.Enum): 37 | DEPOSITWITHDRAW = "Deposits & Withdrawals" 38 | BROKERINTPAID = "Broker Interest Paid" 39 | BROKERINTRCVD = "Broker Interest Received" 40 | WHTAX = "Withholding Tax" 41 | BONDINTRCVD = "Bond Interest Received" 42 | BONDINTPAID = "Bond Interest Paid" 43 | FEES = "Other Fees" 44 | DIVIDEND = "Dividends" 45 | PAYMENTINLIEU = "Payment In Lieu Of Dividends" 46 | COMMADJ = "Commission Adjustments" 47 | ADVISORFEES = "Advisor Fees" 48 | 49 | @enum.unique 50 | class Code(str, enum.Enum): 51 | """Used for both `code` and `notes` attributes. 52 | """ 53 | ASSIGNMENT = "A" 54 | AUTOEXERCISE = "AEx" # Automatic exercise for dividend-related recommendation 55 | AUTOFX = "AFx" # AutoFX conversion resulting from trading 56 | ADJUSTMENT = "Adj" # Adjustment 57 | ALLOCATION = "Al" # Allocation 58 | AWAY = "Aw" # Away Trade 59 | BUYIN = "B" # Automatic Buy-in 60 | BORROW = "Bo" # Direct Borrow 61 | CLOSING = "C" # Closing Trade 62 | CASHDELIVERY = "CD" # Cash Delivery 63 | COMPLEX = "CP" # Complex Position 64 | CANCEL = "Ca" # Cancelled 65 | CORRECT = "Co" # Corrected Trade 66 | CROSSING = "Cx" # Part or all of this transaction was a Crossing executed as dual agent by IB for two IB customers 67 | DUAL = "D" # IB acted as Dual Agent, UNIQUE TO TRADE CONFIRM REPORT 68 | ETF = "ETF" # ETF Creation/Redemption 69 | EXPIRED = "Ep" # Resulted from an Expired Position 70 | EXERCISE = "Ex" # Exercise 71 | GUARANTEED = "G" # Trade in Guaranteed Account Segment 72 | HIGHESTCOST = "HC" # Highest Cost tax lot-matching method 73 | HFINVESTMENT = "HFI" # Investment Transferred to Hedge Fund 74 | HFREDEMPTION = "HFR" # Redemption from Hedge Fund 75 | INTERNAL = "I" # Internal Transfer 76 | AFFILIATE = "IA" # This transaction was executed against an IB affiliate 77 | INVESTOR = "INV" # Investment Transfer from Investor 78 | MARGINLOW = "L" # Ordered by IB (Margin Violation) 79 | WASHSALE = "LD" # Adjusted by Loss Disallowed from Wash Sale 80 | LIFO = "LI" # Last In, First Out (LIFO) tax lot-matching method 81 | LTCG = "LT" # Long-term P/L 82 | LOAN = "Lo" # Direct Loan 83 | MANUAL = "M" # Entered manually by IB 84 | MANUALEXERCISE = "MEx" # Manual exercise for dividend-related recommendation 85 | MAXLOSS = "ML" # Maximize Losses tax basis election 86 | MAXLTCG = "MLG" # Maximize Long-Term Gain tax lot-matching method 87 | MINLTCG = "MLL" # Maximize Long-Term Loss tax lot-matching method 88 | MAXSTCG = "MSG" # Maximize Short-Term Gain tax lot-matching method 89 | MINSTCG = "MSL" # Maximize Short-Term Loss tax lot-matching method 90 | OPENING = "O" # Opening Trade 91 | PARTIAL = "P" # Partial Execution 92 | FRACRISKLESSPRINCIPAL = "RP" # IB acted as riskless principal for the fractional share portion of this trade 93 | FRACPRINCIPAL = "FP" # IB acted as principal for the fractional share portion of this trade 94 | PRICEIMPROVEMENT = "PI" # Price Improvement 95 | POSTACCRUAL = "Po" # Interest or Dividend Accrual Posting 96 | PRINCIPAL = "Pr" # Part or all of this transaction was executed by the Exchange as a Crossing by IB against an IB affiliate and is therefore classified as a Principal and not an agency trade 97 | REINVESTMENT = "R" # Dividend Reinvestment 98 | REDEMPTION = "RED" # Redemption to Investor 99 | REVERSE = "Re" # Interest or Dividend Accrual Reversal 100 | REIMBURSEMENT = "RI" # Reimbursement 101 | SOLICITEDIB = "SI" # This order was solicited by Interactive Brokers 102 | SPECIFICLOT = "SL" # Specific Lot tax lot-matching method 103 | SOLICITEDOTHER = "SO" # This order was marked as solicited by your Introducing Broker 104 | SHORTENEDSETTLEMENT = "SS" # Customer designated this trade for shortened settlement and so is subject to execution at prices above the prevailing market 105 | STCG = "ST" # Short-term P/L 106 | STOCKYIELD = "SY" # Positions that may be eligible for Stock Yield. 107 | TRANSFER = "T" # Transfer 108 | 109 | 110 | @enum.unique 111 | class AssetClass(str, enum.Enum): 112 | CASH = "CASH" 113 | BILL = "BILL" 114 | BOND = "BOND" 115 | STOCK = "STK" 116 | OPTION = "OPT" 117 | WARRANT = "WAR" 118 | FUTURE = "FUT" 119 | FUTUREOPTION = "FOP" 120 | CFD = "CFD" 121 | FOREXCFD = "FXCFD" 122 | CRYPTOCURRENCY = "CRYPTO" 123 | STRUCTUREDPRODUCTS = "IOPT" 124 | METALS = "CMDTY" 125 | OPTIONSONFUTURES = "FSFOP" 126 | OPTIONSFUTURESSTYLE = "FSOPT" 127 | MUTUALFUND = "FUND" 128 | 129 | 130 | @enum.unique 131 | class TradeType(str, enum.Enum): 132 | EXCHTRADE = "ExchTrade" 133 | TRADECANCEL = "TradeCancel" 134 | FRACSHARE = "FracShare" 135 | FRACSHARECANCEL = "FracShareCancel" 136 | TRADECORRECT = "TradeCorrect" 137 | BOOKTRADE = "BookTrade" 138 | DVPTRADE = "DvpTrade" 139 | 140 | 141 | @enum.unique 142 | class BuySell(str, enum.Enum): 143 | BUY = "BUY" 144 | CANCELBUY = "BUY (Ca.)" 145 | SELL = "SELL" 146 | CANCELSELL = "SELL (Ca.)" 147 | 148 | 149 | @enum.unique 150 | class OpenClose(str, enum.Enum): 151 | OPEN = "O" 152 | CLOSE = "C" 153 | OPENCLOSE = "C;O" 154 | UNKNOWN = "-" 155 | 156 | 157 | @enum.unique 158 | class OrderType(str, enum.Enum): 159 | LIMIT = "LMT" 160 | MARKET = "MKT" 161 | STOP = "STP" 162 | STOPLIMIT = "STPLMT" 163 | MARKETONCLOSE = "MOC" 164 | LIMITONCLOSE = "LOC" 165 | # MULTIPLE is not an actual IB order type. It is a catch-all value to use when an Order has an orderType like "LMT;MKT". 166 | # This way OrderType can remian a enum and not be a Tuple. 167 | MULTIPLE = "MULTIPLE" 168 | TRAILLMT = "TRAILLMT" 169 | MIDPX = "MIDPX" 170 | TRAIL = "TRAIL" 171 | REL = "REL" 172 | MIT = "MIT" 173 | 174 | @enum.unique 175 | class Reorg(str, enum.Enum): 176 | BONDCONVERSION = "BC" 177 | BONDMATURITY = "BM" 178 | CONTRACTSOULTE = "CA" 179 | CONTRACTCONSOLIDATION = "CC" 180 | CASHDIV = "CD" 181 | CHOICEDIV = "CH" 182 | CONVERTIBLEISSUE = "CI" 183 | CONTRACTSPINOFF = "CO" 184 | COUPONPAYMENT = "CP" 185 | CONTRACTSPLIT = "CS" 186 | CFDTERMINATION = "CT" 187 | DIVRIGHTSISSUE = "DI" 188 | DELISTWORTHLESS = "DW" 189 | EXPIREDIVRIGHT = "ED" 190 | FEEALLOCATION = "FA" 191 | FORWARDSPLITISSUE = "FI" 192 | FORWARDSPLIT = "FS" 193 | GENERICVOLUNTARY = "GV" 194 | CHOICEDIVDELIVERY = "HD" 195 | CHOICEDIVISSUE = "HI" 196 | ISSUECHANGE = "IC" 197 | ASSETPURCHASE = "OR" 198 | PURCHASEISSUE = "PI" 199 | PROXYVOTE = "PV" 200 | RIGHTSISSUE = "RI" 201 | REVERSESPLIT = "RS" 202 | STOCKDIV = "SD" 203 | SPINOFF = "SO" 204 | SUBSCRIBERIGHTS = "SR" 205 | MERGER = "TC" 206 | TENDERISSUE = "TI" 207 | TENDER = "TO" 208 | TBILLMATURITY = "TM" 209 | 210 | 211 | @enum.unique 212 | class OptionAction(str, enum.Enum): 213 | ASSIGN = "Assignment" 214 | EXERCISE = "Exercise" 215 | EXPIRE = "Expiration" 216 | SELL = "Sell" 217 | BUY = "Buy" 218 | CASHSETTLEMENT = "Cash Settlement" 219 | 220 | 221 | @enum.unique 222 | class LongShort(str, enum.Enum): 223 | LONG = "Long" 224 | SHORT = "Short" 225 | 226 | 227 | @enum.unique 228 | class ToFrom(str, enum.Enum): 229 | TO = "To" 230 | FROM = "From" 231 | 232 | 233 | @enum.unique 234 | class TransferType(str, enum.Enum): 235 | INTERNAL = "INTERNAL" 236 | ACATS = "ACATS" 237 | ATON = "ATON" 238 | FOP = "FOP" 239 | 240 | 241 | @enum.unique 242 | class InOut(str, enum.Enum): 243 | IN = "IN" 244 | OUT = "OUT" 245 | 246 | 247 | @enum.unique 248 | class DeliveredReceived(str, enum.Enum): 249 | DELIVERED = "Delivered" 250 | RECEIVED = "Received" 251 | 252 | 253 | @enum.unique 254 | class PutCall(str, enum.Enum): 255 | PUT = "P" 256 | CALL = "C" 257 | 258 | 259 | ENUMS = [ 260 | CashAction, 261 | Code, 262 | AssetClass, 263 | TradeType, 264 | BuySell, 265 | OpenClose, 266 | OrderType, 267 | Reorg, 268 | OptionAction, 269 | LongShort, 270 | ToFrom, 271 | TransferType, 272 | InOut, 273 | DeliveredReceived, 274 | PutCall, 275 | ] 276 | """Used by ibflex.parser.ATTRIB_CONVERTERS""" 277 | 278 | EnumType = typing.Union[ 279 | typing.Optional[CashAction], 280 | typing.Optional[Code], 281 | typing.Optional[AssetClass], 282 | typing.Optional[TradeType], 283 | typing.Optional[BuySell], 284 | typing.Optional[OpenClose], 285 | typing.Optional[OrderType], 286 | typing.Optional[Reorg], 287 | typing.Optional[OptionAction], 288 | typing.Optional[LongShort], 289 | typing.Optional[ToFrom], 290 | typing.Optional[TransferType], 291 | typing.Optional[InOut], 292 | typing.Optional[DeliveredReceived], 293 | typing.Optional[PutCall], 294 | ] 295 | """Used by ibflex.parser.DataType""" 296 | -------------------------------------------------------------------------------- /ibflex/parser.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Parser/type converter for data in Interactive Brokers' Flex XML format. 3 | 4 | https://www.interactivebrokers.com/en/software/reportguide/reportguide.htm 5 | 6 | Flex report configuration needed by this module: 7 | Date format: choose yyyy-MM-dd 8 | Trades: uncheck "Symbol Summary", "Asset Class", "Orders" 9 | """ 10 | import xml.etree.ElementTree as ET 11 | import datetime 12 | import decimal 13 | import itertools 14 | import functools 15 | from typing import Tuple, Union, Optional, Any, Callable, Iterable 16 | 17 | from ibflex import Types, enums, utils 18 | 19 | 20 | class FlexParserError(Exception): 21 | """ Error experienced while parsing Flex XML data. """ 22 | 23 | 24 | DataType = Union[ 25 | None, str, int, bool, decimal.Decimal, datetime.date, datetime.time, 26 | datetime.datetime, enums.EnumType, Tuple[str, ...], Tuple[enums.Code, ...] 27 | ] 28 | """Possible type annotations for class attributes of a FlexElement that is 29 | a data element (not a container). 30 | """ 31 | 32 | 33 | ############################################################################### 34 | # PARSE FUNCTIONS 35 | ############################################################################### 36 | def parse(source) -> Types.FlexQueryResponse: 37 | """Parse Flex XML data into a hierarchy of ibflex.Types class instances. 38 | 39 | Args: 40 | source: file name, file object, or bytes. 41 | """ 42 | tree = ET.ElementTree() 43 | 44 | # Accept output of client.download(), which is bytes. 45 | if isinstance(source, bytes): 46 | root = ET.XML(source) 47 | # Accept file name or file object. 48 | else: 49 | root = tree.parse(source) 50 | 51 | if root.tag != "FlexQueryResponse": 52 | raise FlexParserError("Not a FlexQueryResponse") 53 | parsed = parse_element(root) 54 | assert isinstance(parsed, Types.FlexQueryResponse) 55 | return parsed 56 | 57 | 58 | def parse_element( 59 | elem: ET.Element 60 | ) -> Union[Types.FlexElement, Tuple[Types.FlexElement, ...]]: 61 | """Distinguish XML data element from container element; dispatch accordingly. 62 | 63 | Flex format stores data as XML element attributes, while container elements 64 | have no attributes. The only exception is , which has a 65 | `count` attribute as a check on its contents. 66 | """ 67 | if elem.tag == "FlexStatements": 68 | # Verify that # of contained elements matches 69 | # what's reported in attribute. 70 | try: 71 | count = int(elem.get("count", "")) 72 | assert len(elem) == count 73 | except (ValueError): 74 | msg = f"Malformed FlexStatements.count={elem.get('count', '')}" 75 | raise FlexParserError(msg) 76 | except AssertionError: 77 | msg = f"Wrong FlexStatements.count={count} vs. {len(elem)}" 78 | raise FlexParserError(msg) 79 | 80 | return parse_element_container(elem) 81 | 82 | if not elem.attrib: 83 | return parse_element_container(elem) 84 | 85 | return parse_data_element(elem) 86 | 87 | 88 | def parse_element_container(elem: ET.Element) -> Tuple[Types.FlexElement, ...]: 89 | """Parse XML element container into FlexElement subclass instances. 90 | """ 91 | tag = elem.tag 92 | 93 | if tag == "FxPositions": 94 | # contains an wrapper per currency. 95 | # Element structure here is: 96 | # 97 | # Flatten the nesting to create FxPositions as a tuple of FxLots 98 | fxlots = (parse_element_container(child) for child in elem) 99 | return tuple(itertools.chain.from_iterable(fxlots)) 100 | 101 | instances = tuple(parse_data_element(child) for child in elem) 102 | return instances 103 | 104 | 105 | def parse_data_element( 106 | elem: ET.Element 107 | ) -> Types.FlexElement: 108 | """Parse an XML data element into a Types.FlexElement subclass instance. 109 | """ 110 | # Look up XML element's matching FlexElement subclass in ibflex.Types. 111 | Class = getattr(Types, elem.tag) 112 | 113 | # Parse element attributes 114 | try: 115 | attrs = dict( 116 | parse_element_attr(Class, k, v) 117 | for k, v in elem.attrib.items() 118 | ) 119 | except KeyError as exc: 120 | msg = f"{Class.__name__} has no attribute " + str(exc) 121 | raise FlexParserError(msg) 122 | 123 | # FlexQueryResponse & FlexStatement are the only data elements 124 | # that contain other data elements. 125 | contained_elements = {child.tag: parse_element(child) for child in elem} 126 | if contained_elements: 127 | assert elem.tag in ("FlexQueryResponse", "FlexStatement") 128 | attrs.update(contained_elements) 129 | 130 | try: 131 | return Class(**attrs) 132 | except Exception as exc: 133 | raise FlexParserError(f"{Class.__name__} - " + str(exc)) 134 | 135 | 136 | def parse_element_attr( 137 | Class: Types.FlexElement, name: str, value: str 138 | ) -> Tuple[str, Any]: 139 | """Convert an XML element attribute into its corresponding Python type, 140 | based on the FlexElement subclass attribute type hint. 141 | 142 | Args: 143 | Class: FlexElement subclass 144 | name: XML attribute name 145 | value: XML attribute value 146 | """ 147 | # Validate currency of any field named something like "currency". 148 | if "currency" in name.lower() and value not in CURRENCY_CODES: 149 | raise FlexParserError(f"{name}: Unknown currency {value!r}") 150 | 151 | # FIXME 152 | # This "dot reference" gets hit a lot by parse_data_element(), and `Class` 153 | # is always the same in the list comprehension that calls this function. 154 | # Consider moving `Class.__annotations__` up out of the list comprehension 155 | # in parse_data_element(), instead accepting it as a function arg here. 156 | Type = Class.__annotations__[name] 157 | 158 | try: 159 | converted = ATTRIB_CONVERTERS[Type](value=value) 160 | return name, converted 161 | except KeyError as exc: 162 | msg = f"{Class.__name__}.{name} - Don't know how to convert " # type: ignore 163 | raise FlexParserError(msg + str(exc)) 164 | except Exception as exc: 165 | msg = f"{Class.__name__}.{name} - " + str(exc) # type: ignore 166 | raise FlexParserError(msg) 167 | 168 | 169 | ############################################################################### 170 | # INPUT VALUE PREP FUNCTIONS FOR DATA CONVERTERS 171 | # These are just implementation details for converters and don't need testing. 172 | ############################################################################### 173 | def prep_date(value: str) -> Tuple[int, int, int]: 174 | """Returns a tuple of (year, month, day). 175 | """ 176 | date_format = DATE_FORMATS[len(value)][value.count('/')] 177 | return datetime.datetime.strptime(value, date_format).timetuple()[:3] 178 | 179 | 180 | def prep_time(value: str) -> Tuple[int, int, int]: 181 | """Returns a tuple of (hour, minute, second). 182 | """ 183 | time_format = TIME_FORMATS[len(value)] 184 | return datetime.datetime.strptime(value, time_format).timetuple()[3:6] 185 | 186 | 187 | def prep_datetime(value: str) -> Tuple[int, ...]: 188 | """Returns a tuple of (year, month, day, hour, minute, second). 189 | """ 190 | # HACK - some old data has ", " separator instead of ",". 191 | value = value.replace(", ", ",") 192 | 193 | def merge_date_time(datestr: str, timestr: str) -> Tuple[int, ...]: 194 | """Convert presplit date/time strings into args ready for datetime(). 195 | """ 196 | prepped_date = prep_date(datestr) 197 | assert prepped_date is not None 198 | 199 | prepped_time = prep_time(timestr) 200 | assert prepped_time is not None 201 | 202 | return prepped_date + prepped_time 203 | 204 | seps = [sep for sep in DATETIME_SEPARATORS if sep in value] 205 | if len(seps) == 1: 206 | sep = seps[0] 207 | datestr, timestr = value.split(sep) 208 | # HACK - some old data has TZ offset appended. Drop the offset. 209 | if "-" in timestr: 210 | timestr = timestr.split("-")[0] 211 | elif "+" in timestr: 212 | timestr = timestr.split("+")[0] 213 | 214 | return merge_date_time(datestr, timestr) 215 | elif len(seps) == 0: 216 | # If we can't find an explicit date/time separator in input value, 217 | # best case is that the value as a bare date (no time). 218 | try: 219 | return prep_date(value) 220 | except Exception: 221 | pass 222 | 223 | # If that doesn't work, assume null separator. Brute force guess 224 | # index of date/time split. Shortest loop is to iterate over 225 | # TIME_FORMATS (there's only 2 of them). 226 | 227 | def testtimelength( 228 | value: str, time_length: int 229 | ) -> Optional[Tuple[int, ...]]: 230 | """Assuming time substring is of given length, try to process value 231 | into time tuple. 232 | """ 233 | datestr, timestr = value[:-time_length], value[-time_length:] 234 | try: 235 | return merge_date_time(datestr, timestr) 236 | except Exception: 237 | return None 238 | 239 | tested = (testtimelength(value, length) for length in TIME_FORMATS) 240 | prepped = [t for t in tested if t is not None] 241 | if len(prepped) != 1: 242 | raise FlexParserError(f"Bad date/time format: {value}") 243 | return prepped[0] 244 | 245 | # Multiple date/time separators appear in input value. 246 | raise FlexParserError(f"Bad date/time format: {value}") 247 | 248 | sep = seps[0] 249 | 250 | try: 251 | # HACK - some old data has ", " separator, which shows up as 252 | # seps = [",", ""]. Keep the comma, strip the space. 253 | datestr, timestr = value.split(sep) 254 | timestr = timestr.strip() 255 | return merge_date_time(datestr, timestr) 256 | except Exception: 257 | raise FlexParserError(f"Bad date/time format: {value}") 258 | 259 | 260 | def prep_sequence(value: str) -> Iterable[str]: 261 | """Split a sequence string into its component items. 262 | 263 | Flex `notes` attribute is semicolon-delimited; other sequences use commas. 264 | 265 | Empty string input interpreted as null data; returns empty list. 266 | """ 267 | sep = ";" if ";" in value else "," 268 | return (v for v in value.split(sep) if v) if value != "" else [] 269 | 270 | 271 | def prep_code_sequence(value: str) -> Iterable[enums.Code]: 272 | """Split a code sequence string into its component codes. 273 | 274 | Flex `notes` attribute is semicolon-delimited; other sequences use commas. 275 | 276 | Empty string input interpreted as null data; returns empty list. 277 | """ 278 | sep = ";" if ";" in value else "," 279 | return ( 280 | enums.Code(v) 281 | for v in value.split(sep) 282 | if v 283 | ) if value != "" else [] 284 | 285 | 286 | ############################################################################### 287 | # DATA CONVERTER FUNCTIONS 288 | ############################################################################### 289 | def make_converter( 290 | Type: type, *, prep: Callable[[str], Any] 291 | ) -> Callable[[str], DataType]: 292 | """Factory producing converter function for type. 293 | 294 | Args: 295 | Type: type constructor e.g. str, Decimal, datetime 296 | prep: function that accepts string input and returns value(s) that can 297 | can be passed to the type constructor to create a Type instance. 298 | 299 | Returns: a function that accepts string input and returns a Type instance. 300 | """ 301 | def convert(value: str) -> DataType: 302 | try: 303 | prepped_value = prep(value) 304 | if prepped_value is None: 305 | # Preserve "null data" indicated by prep function. 306 | return None 307 | elif isinstance(prepped_value, tuple): 308 | # date/time values are prepped into time tuples; 309 | # unpack before passing to type constructor. 310 | return Type(*prepped_value) 311 | else: 312 | return Type(prepped_value) 313 | except Exception: 314 | raise FlexParserError( 315 | f"Can't convert {value!r} to {Type}" 316 | ) 317 | 318 | return convert 319 | 320 | 321 | def make_optional(func): 322 | 323 | def optional_convert(value): 324 | return None if value in ("", "-", "--", "N/A") else func(value) 325 | 326 | return optional_convert 327 | 328 | 329 | convert_string = make_optional(make_converter(str, prep=utils.identity_func)) 330 | convert_int = make_converter(int, prep=utils.identity_func) 331 | # IB sends "Y"/"N" for True/False 332 | convert_bool = make_converter(bool, prep=lambda x: {"Y": True, "N": False}[x]) 333 | # IB sends numeric data with place delimiters (commas) 334 | convert_decimal = make_converter( 335 | decimal.Decimal, 336 | prep=lambda x: x.replace(",", "") 337 | ) 338 | convert_date = make_converter(datetime.date, prep=prep_date) 339 | convert_time = make_converter(datetime.time, prep=prep_time) 340 | convert_datetime = make_converter(datetime.datetime, prep=prep_datetime) 341 | convert_sequence = make_converter(tuple, prep=prep_sequence) 342 | convert_code_sequence = make_converter(tuple, prep=prep_code_sequence) 343 | 344 | 345 | def convert_enum(Type, value): 346 | # Work around old versions of values; convert to the new format 347 | if Type is enums.CashAction and value == "Deposits/Withdrawals": 348 | value = "Deposits & Withdrawals" 349 | # Work around for Orders with orderType like "LMT;MKT" -> "MULTIPLE" 350 | if Type is enums.OrderType and ';' in value: 351 | value = "MULTIPLE" 352 | elif Type is enums.TransferType and value == "ACAT": 353 | value = "ACATS" 354 | 355 | # Enums bind custom names to the IB-supplied values. 356 | # To convert, just do a by-value lookup on the incoming string. 357 | # https://docs.python.org/3/library/enum.html#programmatic-access-to-enumeration-members-and-their-attributes 358 | return Type(value) if value != "" else None 359 | 360 | 361 | ATTRIB_CONVERTERS = { 362 | "str": convert_string, 363 | "Optional[str]": convert_string, 364 | "int": convert_int, 365 | "Optional[int]": make_optional(convert_int), 366 | "bool": convert_bool, 367 | "Optional[bool]": make_optional(convert_bool), 368 | "decimal.Decimal": convert_decimal, 369 | "Optional[decimal.Decimal]": make_optional(convert_decimal), 370 | "datetime.date": convert_date, 371 | "Optional[datetime.date]": make_optional(convert_date), 372 | "datetime.time": convert_time, 373 | "Optional[datetime.time]": make_optional(convert_time), 374 | "datetime.datetime": convert_datetime, 375 | "Optional[datetime.datetime]": make_optional(convert_datetime), 376 | "Tuple[str, ...]": convert_sequence, 377 | "Tuple[enums.Code, ...]": convert_code_sequence, 378 | } 379 | """Map of FlexElement attribute type hint to corresponding converter function. 380 | """ 381 | 382 | ATTRIB_CONVERTERS.update( 383 | { 384 | f"Optional[enums.{Enum.__name__}]": functools.partial(convert_enum, Type=Enum) 385 | for Enum in enums.ENUMS 386 | } 387 | ) 388 | """Map all Enum subclasses as Optional. 389 | """ 390 | 391 | 392 | ############################################################################### 393 | # IB DATE FORMATS 394 | # https://www.interactivebrokers.com/en/software/am/am/reports/activityflexqueries.htm 395 | ############################################################################### 396 | DATE_FORMATS = {8: {0: "%Y%m%d", 2: "%m/%d/%y"}, 397 | 9: {0: "%d-%b-%y"}, 398 | 10: {0: "%Y-%m-%d", 2: "%m/%d/%Y"}} 399 | """Keyed first by string length, then by "/" count within string. 400 | 401 | We can't distinguish in-band between US MM/dd/yyyy and Euro dd/MM/yyyy. 402 | Given an ambiguous date format, we assume US format. 403 | 404 | ALWAYS CONFIGURE YOUR REPORTS TO USE ISO-8601 DATE FORMAT (yyyy-MM-dd). 405 | 406 | Available date formats are: 407 | yyyyMMdd (default) 408 | yyyy-MM-dd 409 | MM/dd/yyyy 410 | MM/dd/yy 411 | dd/MM/yyyy [not implemented in ibflex] 412 | dd/MM/yy [not implemented in ibflex] 413 | dd-MMM-yy 414 | """ 415 | 416 | TIME_FORMATS = {6: "%H%M%S", 8: "%H:%M:%S"} 417 | """Keyed by string length. 418 | 419 | Available time formats are: 420 | HHmmss (default) 421 | HH:mm:ss 422 | """ 423 | 424 | DATETIME_SEPARATORS = [";", ",", " ", "T"] 425 | """We omit the null separator (empty string) because it screws up our logic. 426 | 427 | DO NOT CONFIGURE YOUR REPORTS TO USE NULL-SEPARATED DATE/TIME FIELDS. 428 | 429 | Available date/time separators are: 430 | ; (semi-colon, the default) 431 | , (comma) 432 | ' ' (single-spaced) 433 | No separator 434 | 435 | Additionally, some old values have 'T' as a date/time separator with TZ offset. 436 | """ 437 | 438 | 439 | ############################################################################### 440 | # CURRENCIES 441 | ############################################################################### 442 | ISO4217 = ( 443 | "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", 444 | "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", 445 | "BRL", "BSD", "BTN", "BWP", "BYR", "BZD", "CAD", "CDF", "CHE", "CHF", 446 | "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", 447 | "CZK", "DJF", "DKK", "DOP", "DZD", "EEK", "EGP", "ERN", "ETB", "EUR", 448 | "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", 449 | "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", 450 | "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", 451 | "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LTL", "LVL", 452 | "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MUR", 453 | "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", 454 | "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", 455 | "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", 456 | "SGD", "SHP", "SLL", "SOS", "SRD", "STD", "SVC", "SYP", "SZL", "THB", 457 | "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", 458 | "USD", "USN", "USS", "UYI", "UYU", "UZS", "VEF", "VND", "VUV", "WST", 459 | "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", 460 | "XPD", "XPF", "XPT", "XTS", "XXX", "YER", "ZAR", "ZMK", "ZWL", 461 | ) 462 | CURRENCY_CODES = ISO4217 + ( 463 | "CNH", # RMB traded in HK 464 | "BASE_SUMMARY", # Fake currency code used in IB NAV/Performance reports 465 | "", # Lot element allows blank currency ?! 466 | ) 467 | 468 | 469 | ############################################################################### 470 | # CLI SCRIPT 471 | ############################################################################### 472 | def main(): 473 | from argparse import ArgumentParser 474 | 475 | argparser = ArgumentParser( 476 | description="Quick test of Interactive Brokers Flex XML data parser" 477 | ) 478 | argparser.add_argument("file", nargs="+", help="XML data file(s)") 479 | args = argparser.parse_args() 480 | 481 | for file in args.file: 482 | parse(file) 483 | print(f"Successfully parsed {file}") 484 | 485 | 486 | if __name__ == "__main__": 487 | main() 488 | -------------------------------------------------------------------------------- /ibflex/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csingley/ibflex/df672d2b9048c524119149940c642ecee3593fa5/ibflex/py.typed -------------------------------------------------------------------------------- /ibflex/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Utility functions for ibflex """ 3 | import itertools 4 | 5 | 6 | def identity_func(x): 7 | return x 8 | 9 | 10 | def all_equal(iterable): 11 | """Returns True if all the elements are equal to each other 12 | 13 | https://docs.python.org/3/library/itertools.html#itertools-recipes 14 | """ 15 | g = itertools.groupby(iterable) 16 | return next(g, True) and not next(g, False) 17 | -------------------------------------------------------------------------------- /requirements-development.txt: -------------------------------------------------------------------------------- 1 | --index-url https://pypi.python.org/simple/ 2 | 3 | # `setup.py develop` 4 | -e . 5 | 6 | # web client 7 | requests 8 | types-requests 9 | 10 | # test running 11 | nose 12 | coverage 13 | pylint 14 | mypy 15 | black 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | --index-url https://pypi.python.org/simple/ 2 | . 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=2 3 | detailed-errors = 1 4 | with-coverage = 1 5 | nocapture = 1 6 | cover-package = ibflex 7 | 8 | [flake8] 9 | max-line-length = 88 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Release process: 3 | 1. Update ibflex.__version__.__version__ 4 | 2. Change download_url below 5 | 3. Commit changes & push 6 | 4. Test: `python setup.py sdist` 7 | 5. Test: `twine upload --repository-url https://test.pypi.org/legacy/ dist/*` 8 | 6. Test result: Check https://test.pypi.org/project/ibflex 9 | 7. `git tag` the release 10 | 8. `git push --tags` 11 | 9. Verify that new tag shows at https://github.com/csingley/ibflex/releases 12 | 10. `twine upload dist/*` 13 | 11. `make clean` 14 | 12. Change download_url back to master; commit & push 15 | """ 16 | import os.path 17 | from setuptools import setup, find_packages 18 | 19 | __here__ = os.path.dirname(os.path.realpath(__file__)) 20 | 21 | ABOUT = {} 22 | with open(os.path.join(__here__, "ibflex", "__version__.py"), "r") as f: 23 | exec(f.read(), ABOUT) 24 | 25 | with open(os.path.join(__here__, "README.rst"), "r") as f: 26 | README = f.read() 27 | 28 | URL_BASE = "{}/tarball".format(ABOUT["__url__"]) 29 | 30 | setup( 31 | name=ABOUT["__title__"], 32 | version=ABOUT["__version__"], 33 | description=ABOUT["__description__"], 34 | long_description=README, 35 | long_description_content_type="text/x-rst", 36 | author=ABOUT["__author__"], 37 | author_email=ABOUT["__author_email__"], 38 | url=ABOUT["__url__"], 39 | packages=find_packages(), 40 | package_data={ 41 | "ibflex": ["README.rst", "tests/*", "py.typed"], 42 | }, 43 | python_requires=">=3.7", 44 | license=ABOUT["__license__"], 45 | # Note: change 'master' to the tag name when releasing a new verion 46 | # download_url="{}/master".format(URL_BASE), 47 | download_url="{}/{}".format(URL_BASE, ABOUT["__version__"]), 48 | classifiers=[ 49 | "Development Status :: 3 - Alpha", 50 | "Intended Audience :: Developers", 51 | "Intended Audience :: Financial and Insurance Industry", 52 | "Topic :: Software Development :: Libraries :: Python Modules", 53 | "Topic :: Utilities", 54 | "Topic :: Office/Business", 55 | "Topic :: Office/Business :: Financial", 56 | "Topic :: Office/Business :: Financial :: Accounting", 57 | "Topic :: Office/Business :: Financial :: Investment", 58 | "License :: OSI Approved :: MIT License", 59 | "Operating System :: OS Independent", 60 | "Natural Language :: English", 61 | "Programming Language :: Python", 62 | "Programming Language :: Python :: 3 :: Only", 63 | "Programming Language :: Python :: 3.7", 64 | "Programming Language :: Python :: 3.8", 65 | "Programming Language :: Python :: 3.9", 66 | ], 67 | keywords=["Interactive Brokers", "ibkr", "flex", "xml"], 68 | extras_require={ 69 | "web": ["requests"], 70 | }, 71 | entry_points={ 72 | "console_scripts": [ 73 | "flexget=ibflex.client:main [web]", 74 | ], 75 | }, 76 | ) 77 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Unit tests for ibflex.parser module """ 3 | 4 | import unittest 5 | from unittest.mock import Mock, patch, call 6 | import datetime 7 | from io import BytesIO 8 | 9 | import requests 10 | from ibflex import client, parser, Types 11 | 12 | 13 | RESPONSE_SUCCESS = ( 14 | '' 15 | 'Success' 16 | '1234567890' 17 | 'https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.GetStatement' 18 | '' 19 | ) 20 | 21 | 22 | RESPONSE_FAIL = ( 23 | '' 24 | 'Fail' 25 | '1012' 26 | 'Token has expired.' 27 | '' 28 | ) 29 | 30 | 31 | FLEX_QUERY_RESPONSE = ( 32 | '' 33 | '' 34 | '' 35 | '' 36 | '' 37 | ) 38 | 39 | 40 | def mock_response(*args, **kwargs) -> object: 41 | class MockResponse: 42 | def __init__(self, content: str): 43 | self._content = content 44 | 45 | @property 46 | def content(self) -> bytes: 47 | return bytes(self._content, encoding="utf8") 48 | 49 | params = kwargs["params"] 50 | token = params["t"] 51 | query = params["q"] 52 | 53 | if token == "DEADBEEF" and query == "0987654321": 54 | return MockResponse(RESPONSE_SUCCESS) 55 | elif token == "DEADBEEF" and query == "1234567890": 56 | return MockResponse(FLEX_QUERY_RESPONSE) 57 | 58 | return MockResponse(RESPONSE_FAIL) 59 | 60 | 61 | @patch("requests.get", side_effect=requests.exceptions.Timeout) 62 | class SubmitRequestTestCase(unittest.TestCase): 63 | def test_submit_request_retry(self, mock_requests_get): 64 | with self.assertRaises(requests.exceptions.Timeout): 65 | client.submit_request( 66 | url=client.REQUEST_URL, 67 | token="DEADBEEF", 68 | query="0987654321", 69 | ) 70 | 71 | self.assertEqual( 72 | mock_requests_get.call_args_list, 73 | [ 74 | call( 75 | client.REQUEST_URL, 76 | params={"v": "3", "t": "DEADBEEF", "q": "0987654321"}, 77 | headers={"user-agent": "Java"}, 78 | timeout=5, 79 | ), 80 | call( 81 | client.REQUEST_URL, 82 | params={"v": "3", "t": "DEADBEEF", "q": "0987654321"}, 83 | headers={"user-agent": "Java"}, 84 | timeout=10, 85 | ), 86 | call( 87 | client.REQUEST_URL, 88 | params={"v": "3", "t": "DEADBEEF", "q": "0987654321"}, 89 | headers={"user-agent": "Java"}, 90 | timeout=15, 91 | ), 92 | ], 93 | ) 94 | 95 | 96 | @patch("requests.get", side_effect=mock_response) 97 | class RequestStatementTestCase(unittest.TestCase): 98 | def test_request_statement(self, mock_requests_get): 99 | # `url` arg defaults to client.REQUEST_URL 100 | output = client.request_statement( 101 | token="DEADBEEF", 102 | query_id="0987654321", 103 | ) 104 | 105 | mock_requests_get.assert_called_once_with( 106 | client.REQUEST_URL, 107 | params={"v": "3", "t": "DEADBEEF", "q": "0987654321"}, 108 | headers={"user-agent": "Java"}, 109 | timeout=5, 110 | ) 111 | 112 | self.assertIsInstance(output, client.StatementAccess) 113 | self.assertEqual( 114 | output.timestamp, 115 | datetime.datetime(2012, 8, 28, 10, 37, tzinfo=datetime.timezone( 116 | datetime.timedelta(hours=-4))) 117 | ) 118 | self.assertEqual(output.ReferenceCode, "1234567890") 119 | self.assertEqual(output.Url, client.STMT_URL) 120 | 121 | 122 | @patch("requests.get", side_effect=mock_response) 123 | class DownloadTestCase(unittest.TestCase): 124 | def test_request_statement(self, mock_requests_get: Mock): 125 | output = client.download( 126 | token="DEADBEEF", 127 | query_id="0987654321", 128 | ) 129 | self.assertIsInstance(output, bytes) 130 | 131 | self.assertEqual( 132 | mock_requests_get.call_args_list, 133 | [ 134 | call( 135 | client.REQUEST_URL, 136 | params={"v": "3", "t": "DEADBEEF", "q": "0987654321"}, 137 | headers={"user-agent": "Java"}, 138 | timeout=5, 139 | ), 140 | call( 141 | client.STMT_URL, 142 | params={"v": "3", "t": "DEADBEEF", "q": "1234567890"}, 143 | headers={"user-agent": "Java"}, 144 | timeout=5, 145 | ), 146 | ], 147 | ) 148 | 149 | response = parser.parse(BytesIO(output)) 150 | self.assertIsInstance(response, Types.FlexQueryResponse) 151 | 152 | 153 | if __name__ == '__main__': 154 | unittest.main(verbosity=3) 155 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Unit tests for ibflex.parser module """ 3 | 4 | # PEP 563 compliance 5 | # https://www.python.org/dev/peps/pep-0563/#resolving-type-hints-at-runtime 6 | from __future__ import annotations 7 | 8 | import unittest 9 | from unittest.mock import patch, sentinel 10 | import xml.etree.ElementTree as ET 11 | import datetime 12 | import decimal 13 | import enum 14 | from typing import Tuple, Optional 15 | import functools 16 | 17 | from ibflex import parser, Types, enums 18 | 19 | 20 | @patch("ibflex.parser.parse_element_container") 21 | @patch("ibflex.parser.parse_data_element") 22 | class ParseElementTestCase(unittest.TestCase): 23 | """FlexStatements, elements w/o attributes go to parse_element_container(). 24 | Everything else goes to parse_data_element(). 25 | """ 26 | def testFlexStatements(self, mock_parse_data, mock_parse_container): 27 | mock_parse_container.return_value = sentinel.TUPLE 28 | mock_parse_data.return_value = sentinel.FLEX_ELEMENT 29 | # Elements without attributes get routed to parse_element_container() 30 | 31 | # Elements with attributes get routed to parse_data_element() 32 | elem = ET.Element("FlexStatement", attrib={"foo": "bar"}) 33 | output = parser.parse_element(elem) 34 | self.assertEqual(output, sentinel.FLEX_ELEMENT) 35 | 36 | # ...except for , which gets routed to 37 | # parse_element_container() 38 | elem = ET.Element("FlexStatements", attrib={"count": "2"}) 39 | ET.SubElement(elem, "FooBar") 40 | ET.SubElement(elem, "FooBar") 41 | output = parser.parse_element(elem) 42 | mock_parse_container.assert_called_with(elem) 43 | self.assertEqual(output, sentinel.TUPLE) 44 | 45 | # missing count throws an FlexParserError. 46 | elem = ET.Element("FlexStatements") 47 | with self.assertRaises(parser.FlexParserError): 48 | parser.parse_element(elem) 49 | 50 | # Empty is legal; returns an empty tuple. 51 | elem = ET.Element("FlexStatements", attrib={"count": "0"}) 52 | output = parser.parse_element(elem) 53 | self.assertEqual(output, sentinel.TUPLE) 54 | 55 | # count attr must match # of contained elements 56 | elem = ET.Element("FlexStatements", attrib={"count": "2"}) 57 | ET.SubElement(elem, "FooBar") 58 | with self.assertRaises(parser.FlexParserError): 59 | parser.parse_element(elem) 60 | 61 | def testEmptyAttributes(self, mock_parse_data, mock_parse_container): 62 | mock_parse_container.return_value = sentinel.TUPLE 63 | mock_parse_data.return_value = sentinel.FLEX_ELEMENT 64 | 65 | attrib = {"foo": "bar"} 66 | 67 | elem0 = ET.Element("FlexStatement") 68 | output0 = parser.parse_element(elem0) 69 | mock_parse_container.assert_called_with(elem0) 70 | self.assertEqual(output0, sentinel.TUPLE) 71 | 72 | elem1 = ET.Element("FlexStatement", attrib=attrib) 73 | output1 = parser.parse_element(elem1) 74 | self.assertEqual(output1, sentinel.FLEX_ELEMENT) 75 | 76 | 77 | @patch("ibflex.parser.parse_data_element") 78 | class ParseElementContainerTestCase(unittest.TestCase): 79 | def testBasic(self, mock_parse_data_element): 80 | """parse_element_container() returns parse_data_element() for each child. 81 | """ 82 | elem = ET.Element("Foo") 83 | ET.SubElement(elem, "Bar") 84 | ET.SubElement(elem, "Bar") 85 | 86 | mock_parse_data_element.side_effect = range(10) 87 | output = parser.parse_element_container(elem) 88 | self.assertEqual(output, (0, 1)) 89 | 90 | def testFxPositions(self, mock_parse_data_element): 91 | """parse_element_container() concatenates grandchildren. 92 | """ 93 | mock_parse_data_element.return_value = sentinel.FLEX_ELEMENT 94 | 95 | # with no children returns an empty tuple 96 | elem = ET.Element("FxPositions") 97 | output = parser.parse_element_container(elem) 98 | self.assertEqual(output, ()) 99 | 100 | # with one child returns parse_data_element() 101 | # for each grandchild. 102 | child = ET.SubElement(elem, "Bar") 103 | ET.SubElement(child, "Baz") 104 | ET.SubElement(child, "Baz") 105 | ET.SubElement(child, "Baz") 106 | 107 | output = parser.parse_element_container(elem) 108 | self.assertEqual(output, (sentinel.FLEX_ELEMENT, )*3) 109 | 110 | # FxPositions with multiple children concatenates all 111 | # grandchildren into a flat tuple. 112 | sibling = ET.SubElement(elem, "Bar") 113 | ET.SubElement(sibling, "Baz") 114 | ET.SubElement(sibling, "Baz") 115 | ET.SubElement(sibling, "Baz") 116 | 117 | output = parser.parse_element_container(elem) 118 | self.assertEqual(output, (sentinel.FLEX_ELEMENT, )*6) 119 | 120 | 121 | class ParseDataElementTestCase(unittest.TestCase): 122 | def testContainedElements(self): 123 | # Only FlexQueryResponse & FlexStatement may have contained elements. 124 | pass 125 | 126 | 127 | class ParseElementAttrTestCase(unittest.TestCase): 128 | def testBasicType(self): 129 | """parse_element_attr(): class attribute type hint controls parsing.""" 130 | 131 | class TestClass: 132 | foo: str 133 | bar: int 134 | baz: bool 135 | datetime: datetime.datetime 136 | date: datetime.date 137 | time: datetime.time 138 | sequence: Tuple[str, ...] 139 | 140 | # Return (attr_name, type-converted value) 141 | 142 | # int 143 | self.assertEqual( 144 | parser.parse_element_attr(TestClass, "foo", "1"), 145 | ("foo", "1") 146 | ) 147 | self.assertEqual( 148 | parser.parse_element_attr(TestClass, "bar", "1"), 149 | ("bar", 1) 150 | ) 151 | 152 | # bool 153 | self.assertEqual( 154 | parser.parse_element_attr(TestClass, "foo", "Y"), 155 | ("foo", "Y") 156 | ) 157 | self.assertEqual( 158 | parser.parse_element_attr(TestClass, "baz", "Y"), 159 | ("baz", True) 160 | ) 161 | 162 | # datetime vs date 163 | self.assertEqual( 164 | parser.parse_element_attr(TestClass, "datetime", "20100411"), 165 | ("datetime", datetime.datetime(2010, 4, 11)) 166 | ) 167 | self.assertEqual( 168 | parser.parse_element_attr(TestClass, "date", "20100411"), 169 | ("date", datetime.date(2010, 4, 11)) 170 | ) 171 | 172 | # time 173 | self.assertEqual( 174 | parser.parse_element_attr(TestClass, "foo", "152559"), 175 | ("foo", "152559") 176 | ) 177 | self.assertEqual( 178 | parser.parse_element_attr(TestClass, "time", "152559"), 179 | ("time", datetime.time(15, 25, 59)) 180 | ) 181 | 182 | def testOptional(self): 183 | 184 | class TestClass: 185 | string: str 186 | optstring: Optional[str] 187 | integer: int 188 | optinteger: Optional[int] 189 | boolean: bool 190 | optboolean: Optional[bool] 191 | decimal: decimal.Decimal 192 | optdecimal: Optional[decimal.Decimal] 193 | datetime: datetime.datetime 194 | optdatetime: Optional[datetime.datetime] 195 | date: datetime.date 196 | optdate: Optional[datetime.date] 197 | time: datetime.time 198 | opttime: Optional[datetime.time] 199 | 200 | # Strings always return None if empty, whether or not hinted Optional. 201 | self.assertEqual( 202 | parser.parse_element_attr(TestClass, "string", ""), 203 | ("string", None) 204 | ) 205 | self.assertEqual( 206 | parser.parse_element_attr(TestClass, "optstring", ""), 207 | ("optstring", None) 208 | ) 209 | 210 | # Other basic types return None for empty string if hinted Optional, 211 | # otherwise raise FlexParseError for input empty string. 212 | self.assertEqual( 213 | parser.parse_element_attr(TestClass, "optinteger", ""), 214 | ("optinteger", None) 215 | ) 216 | with self.assertRaises(parser.FlexParserError): 217 | parser.parse_element_attr(TestClass, "integer", ""), 218 | 219 | self.assertEqual( 220 | parser.parse_element_attr(TestClass, "optboolean", ""), 221 | ("optboolean", None) 222 | ) 223 | with self.assertRaises(parser.FlexParserError): 224 | parser.parse_element_attr(TestClass, "boolean", ""), 225 | 226 | self.assertEqual( 227 | parser.parse_element_attr(TestClass, "optdecimal", ""), 228 | ("optdecimal", None) 229 | ) 230 | with self.assertRaises(parser.FlexParserError): 231 | parser.parse_element_attr(TestClass, "decimal", ""), 232 | 233 | self.assertEqual( 234 | parser.parse_element_attr(TestClass, "optdatetime", ""), 235 | ("optdatetime", None) 236 | ) 237 | with self.assertRaises(parser.FlexParserError): 238 | parser.parse_element_attr(TestClass, "datetime", ""), 239 | 240 | self.assertEqual( 241 | parser.parse_element_attr(TestClass, "optdate", ""), 242 | ("optdate", None) 243 | ) 244 | with self.assertRaises(parser.FlexParserError): 245 | parser.parse_element_attr(TestClass, "date", ""), 246 | 247 | self.assertEqual( 248 | parser.parse_element_attr(TestClass, "opttime", ""), 249 | ("opttime", None) 250 | ) 251 | with self.assertRaises(parser.FlexParserError): 252 | parser.parse_element_attr(TestClass, "time", ""), 253 | 254 | def testSequence(self): 255 | """parse_element_attr(): Sequence is always converted to tuple.""" 256 | 257 | class TestClass: 258 | foo: str 259 | sequence: Tuple[str, ...] 260 | 261 | self.assertEqual( 262 | parser.parse_element_attr(TestClass, "foo", "A,B,C"), 263 | ("foo", "A,B,C") 264 | ) 265 | self.assertEqual( 266 | parser.parse_element_attr(TestClass, "sequence", "A,B,C"), 267 | ("sequence", ("A", "B", "C")) 268 | ) 269 | 270 | # Sequence null data parses as empty tuple 271 | self.assertEqual( 272 | parser.parse_element_attr(TestClass, "sequence", ""), 273 | ("sequence", ()) 274 | ) 275 | 276 | def testEnum(self): 277 | """parse_element_attr() converts Enum values to names. 278 | """ 279 | 280 | class TestEnum(enum.Enum): 281 | FOO = "1" 282 | BAR = "2" 283 | 284 | class TestClass: 285 | foobar: Optional[TestEnum] = None 286 | 287 | # Enum must be added to ATTRIB_CONVERTERS in order to be converted. 288 | with patch.dict( 289 | "ibflex.parser.ATTRIB_CONVERTERS", 290 | {"Optional[TestEnum]": functools.partial(parser.convert_enum, Type=TestEnum)} 291 | ): 292 | self.assertEqual( 293 | parser.parse_element_attr(TestClass, "foobar", "1"), 294 | ("foobar", TestEnum.FOO) 295 | ) 296 | self.assertEqual( 297 | parser.parse_element_attr(TestClass, "foobar", "2"), 298 | ("foobar", TestEnum.BAR) 299 | ) 300 | 301 | # Illegal enum values raise FlexParserError 302 | with self.assertRaises(parser.FlexParserError): 303 | parser.parse_element_attr(TestClass, "foobar", "3") 304 | 305 | def testCurrency(self): 306 | """parse_element_attr() checks attributes named 'currency' vs ISO4217. 307 | """ 308 | 309 | class TestClass: 310 | fooCurREncY: str 311 | notcurrencies: str 312 | 313 | self.assertEqual( 314 | parser.parse_element_attr(TestClass, "notcurrencies", "FOO"), 315 | ("notcurrencies", "FOO") 316 | ) 317 | with self.assertRaises(parser.FlexParserError): 318 | parser.parse_element_attr(TestClass, "fooCurREncY", "FOO") 319 | 320 | 321 | class ConverterFunctionTestCase(unittest.TestCase): 322 | def testConvertString(self): 323 | self.assertEqual(parser.convert_string("Foo"), "Foo") 324 | 325 | # Empty string returns None. 326 | self.assertEqual(parser.convert_string(""), None) 327 | 328 | def testConvertInt(self): 329 | self.assertEqual(parser.convert_int("12"), 12) 330 | 331 | # Empty string raises FlexParserError. 332 | with self.assertRaises(parser.FlexParserError): 333 | parser.convert_int("") 334 | 335 | def testConvertBool(self): 336 | """ Legal boolean values are 'Y'/'N' """ 337 | self.assertEqual(parser.convert_bool("Y"), True) 338 | self.assertEqual(parser.convert_bool("N"), False) 339 | 340 | # Empty string raises FlexParserError. 341 | with self.assertRaises(parser.FlexParserError): 342 | parser.convert_bool("") 343 | 344 | # Illegal input raises FlexParserError. 345 | for bogus in ("y", "n", True, False, 1, 0, "YES", "NO", "yes", "no"): 346 | with self.assertRaises(parser.FlexParserError): 347 | parser.convert_bool(bogus) 348 | 349 | def testConvertDecimal(self): 350 | """ Decimal strings may include comma place delimiters """ 351 | dec = parser.convert_decimal("2,345,678.99") 352 | self.assertEqual(dec, decimal.Decimal("2345678.99")) 353 | 354 | # Empty string raises FlexParserError. 355 | with self.assertRaises(parser.FlexParserError): 356 | parser.convert_decimal("") 357 | 358 | def testConvertDate(self): 359 | """Legal date fmt yyyyMMdd, yyyy-MM-dd, MM/dd/yyyy, MM/dd/yy, dd-MMM-yy 360 | 361 | Empty string returns None. 362 | """ 363 | for string in ( 364 | "20160229", "2016-02-29", "02/29/2016", "02/29/16", "29-feb-16" 365 | ): 366 | date = parser.convert_date(string) 367 | self.assertEqual(date, datetime.date(2016, 2, 29)) 368 | 369 | # Illegal dates fail with FlexParserError 370 | with self.assertRaises(parser.FlexParserError): 371 | parser.convert_date("20150229") 372 | 373 | # Empty string raises FlexParserError. 374 | with self.assertRaises(parser.FlexParserError): 375 | parser.convert_date("") 376 | 377 | def testConvertTime(self): 378 | """Legal time formats: HHmmss, HH:mm:ss""" 379 | for string in ( 380 | "143529", "14:35:29", 381 | ): 382 | time = parser.convert_time(string) 383 | self.assertEqual(time, datetime.time(14, 35, 29)) 384 | 385 | # Illegal times fail with FlexParserError 386 | with self.assertRaises(parser.FlexParserError): 387 | parser.convert_time("240000") # datetime.time has no leap seconds 388 | 389 | # Empty string raises FlexParserError. 390 | with self.assertRaises(parser.FlexParserError): 391 | parser.convert_time("") 392 | 393 | def testConvertDateTime(self): 394 | """Legal datetime formats: date & time joined by {";", ",", " ", ""} 395 | """ 396 | for datestr in ( 397 | "20160229", "2016-02-29", "02/29/2016", "02/29/16", "29-feb-16" 398 | ): 399 | for timestr in ( 400 | "143529", "14:35:29", 401 | ): 402 | for sep in (";", ",", " ", ""): 403 | datetimestr = sep.join((datestr, timestr)) 404 | datetime_ = parser.convert_datetime(datetimestr) 405 | self.assertEqual( 406 | datetime_, datetime.datetime(2016, 2, 29, 14, 35, 29) 407 | ) 408 | 409 | # Plain dates (without time) also get converted to datetime. 410 | self.assertEqual( 411 | parser.convert_datetime("20160229"), datetime.datetime(2016, 2, 29) 412 | ) 413 | 414 | # Illegal datetimes fail with FlexParserError 415 | with self.assertRaises(parser.FlexParserError): 416 | parser.convert_datetime("20150229") 417 | 418 | # Empty string raises FlexParserError. 419 | with self.assertRaises(parser.FlexParserError): 420 | parser.convert_datetime("") 421 | 422 | # Hacks to work around messed-up formats from old data. 423 | self.assertEqual( 424 | parser.convert_datetime("2010-01-04T15:37:49-05:00"), 425 | datetime.datetime(2010, 1, 4, 15, 37, 49) 426 | ) 427 | self.assertEqual( 428 | parser.convert_datetime("2009-12-23, 20:25:00"), 429 | datetime.datetime(2009, 12, 23, 20, 25) 430 | ) 431 | self.assertEqual( 432 | parser.convert_datetime("2010-01-08, 14:02:30"), 433 | datetime.datetime(2010, 1, 8, 14, 2, 30) 434 | ) 435 | 436 | def testConvertSequence(self): 437 | """String sequences can be comma- or semicolon-delimited. 438 | """ 439 | self.assertEqual(parser.convert_sequence("Foo,Bar"), ("Foo", "Bar")) 440 | self.assertEqual(parser.convert_sequence("Foo;Bar"), ("Foo", "Bar")) 441 | 442 | # Single element (undelimited) still gets converted to tuple 443 | self.assertEqual(parser.convert_sequence("Foobar"), ("Foobar", )) 444 | 445 | # Empty string returns empty tuple. 446 | self.assertEqual(parser.convert_sequence(""), ()) 447 | 448 | def testConvertEnum(self): 449 | """convert_enum() looks up by value not name. 450 | """ 451 | class TestEnum(enum.Enum): 452 | FOO = "1" 453 | BAR = "2" 454 | 455 | self.assertEqual(parser.convert_enum(TestEnum, "1"), TestEnum.FOO) 456 | self.assertEqual(parser.convert_enum(TestEnum, "2"), TestEnum.BAR) 457 | 458 | # Empty string returns None. 459 | self.assertEqual(parser.convert_enum(TestEnum, ""), None) 460 | 461 | # Old and new versions of enum values work. 462 | self.assertEqual( 463 | parser.convert_enum(enums.CashAction, "Deposits/Withdrawals"), 464 | enums.CashAction.DEPOSITWITHDRAW, 465 | ) 466 | self.assertEqual( 467 | parser.convert_enum(enums.CashAction, "Deposits & Withdrawals"), 468 | enums.CashAction.DEPOSITWITHDRAW, 469 | ) 470 | 471 | self.assertEqual( 472 | parser.convert_enum(enums.TransferType, "ACAT"), 473 | enums.TransferType.ACATS, 474 | ) 475 | self.assertEqual( 476 | parser.convert_enum(enums.TransferType, "ACATS"), 477 | enums.TransferType.ACATS, 478 | ) 479 | 480 | def testMakeOptional(self): 481 | """make_optional() wraps converter functions to return None for empty string. 482 | """ 483 | opt = parser.make_optional 484 | 485 | self.assertEqual(opt(parser.convert_int)("12"), 12) 486 | self.assertEqual(opt(parser.convert_int)(""), None) 487 | 488 | self.assertEqual(opt(parser.convert_bool)("Y"), True) 489 | self.assertEqual(opt(parser.convert_bool)("N"), False) 490 | self.assertEqual(opt(parser.convert_bool)(""), None) 491 | 492 | self.assertEqual( 493 | opt(parser.convert_decimal)("2,345,678.99"), 494 | decimal.Decimal("2345678.99") 495 | ) 496 | self.assertEqual( 497 | opt(parser.convert_decimal)(""), None 498 | ) 499 | 500 | for string in ( 501 | "20160229", "2016-02-29", "02/29/2016", "02/29/16", "29-feb-16" 502 | ): 503 | self.assertEqual( 504 | opt(parser.convert_date)(string), 505 | datetime.date(2016, 2, 29) 506 | ) 507 | self.assertEqual( 508 | opt(parser.convert_date)(""), None 509 | ) 510 | 511 | for string in ( 512 | "143529", "14:35:29", 513 | ): 514 | self.assertEqual( 515 | opt(parser.convert_time)(string), 516 | datetime.time(14, 35, 29) 517 | ) 518 | self.assertEqual( 519 | opt(parser.convert_time)(""), None 520 | ) 521 | 522 | for datestr in ( 523 | "20160229", "2016-02-29", "02/29/2016", "02/29/16", "29-feb-16" 524 | ): 525 | for timestr in ( 526 | "143529", "14:35:29", 527 | ): 528 | for sep in (";", ",", " ", ""): 529 | datetimestr = sep.join((datestr, timestr)) 530 | self.assertEqual( 531 | opt(parser.convert_datetime)(datetimestr), 532 | datetime.datetime(2016, 2, 29, 14, 35, 29) 533 | ) 534 | self.assertEqual( 535 | opt(parser.convert_datetime)(""), None 536 | ) 537 | 538 | 539 | if __name__ == '__main__': 540 | unittest.main(verbosity=3) 541 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Unit tests for ibflex.Types module """ 3 | 4 | # stdlib imports 5 | import unittest 6 | import xml.etree.ElementTree as ET 7 | import datetime 8 | import decimal 9 | 10 | 11 | # local imports 12 | from ibflex import Types, enums, parser 13 | 14 | 15 | class AccountInformationTestCase(unittest.TestCase): 16 | data = ET.fromstring( 17 | ('') 24 | ) 25 | 26 | def testParse(self): 27 | instance = parser.parse_data_element(self.data) 28 | self.assertIsInstance(instance, Types.AccountInformation) 29 | self.assertEqual(instance.accountId, "U123456") 30 | self.assertEqual(instance.acctAlias, "ibflex test") 31 | self.assertEqual(instance.currency, "USD") 32 | self.assertEqual(instance.name, "Porky Pig") 33 | self.assertEqual(instance.accountType, "Advisor Client") 34 | self.assertEqual(instance.customerType, "Partnership") 35 | self.assertEqual( 36 | instance.accountCapabilities, 37 | ("Portfolio Margin", "IBPrime") 38 | ) 39 | self.assertEqual( 40 | instance.tradingPermissions, 41 | ("Stocks", "Options", "Warrants", "Bonds", "Forex", "Stock Borrow") 42 | ) 43 | self.assertEqual(instance.dateOpened, datetime.date(2009, 6, 25)) 44 | self.assertEqual(instance.dateFunded, datetime.date(2009, 7, 13)) 45 | self.assertEqual(instance.dateClosed, None) 46 | self.assertEqual(instance.masterName, "Dewey Cheatham & Howe") 47 | self.assertEqual(instance.ibEntity, "IBLLC-US") 48 | 49 | 50 | class FlexStatementTestCase(unittest.TestCase): 51 | data = ET.fromstring( 52 | ('') 54 | ) 55 | data.append(AccountInformationTestCase.data) 56 | 57 | def testParse(self): 58 | instance = parser.parse_data_element(self.data) 59 | self.assertIsInstance(instance, Types.FlexStatement) 60 | self.assertEqual(instance.accountId , 'U123456') 61 | self.assertEqual(instance.fromDate, datetime.date(2011, 1, 3)) 62 | self.assertEqual(instance.toDate, datetime.date(2011, 12, 30)) 63 | self.assertIs(instance.period, None) 64 | self.assertEqual(instance.whenGenerated, datetime.datetime(2017, 5, 10, 16, 41, 37)) 65 | self.assertIsInstance(instance.AccountInformation, Types.AccountInformation) 66 | 67 | 68 | class FlexQueryResponseTestCase(unittest.TestCase): 69 | data = ET.fromstring( 70 | """""" 71 | ) 72 | data.append(ET.fromstring("""""")) 73 | 74 | def testParse(self): 75 | self.data[0].append(FlexStatementTestCase.data) 76 | instance = parser.parse_data_element(self.data) 77 | self.assertIsInstance(instance, Types.FlexQueryResponse) 78 | self.assertEqual(instance.queryName, 'ibflex test') 79 | self.assertEqual(instance.type, 'AF') 80 | 81 | self.assertIsInstance(instance.FlexStatements, tuple) 82 | self.assertEqual(len(instance.FlexStatements), 1) 83 | self.assertIsInstance(instance.FlexStatements[0], Types.FlexStatement) 84 | 85 | def testParseWrongStatementCount(self): 86 | """Error if `count` attr doesn't match # FlexStatement 87 | """ 88 | # `count` == 1; no FlexStatement 89 | with self.assertRaises(parser.FlexParserError): 90 | parser.parse_data_element(self.data) 91 | 92 | # `count` == 1; 2 FlexStatements 93 | self.data[0].append(FlexStatementTestCase.data) 94 | self.data[0].append(FlexStatementTestCase.data) 95 | with self.assertRaises(parser.FlexParserError): 96 | parser.parse_data_element(self.data) 97 | 98 | def testParseNoStatements(self): 99 | # Error if FlexStatements `count` attribute doesn't match # FlexStatement 100 | self.data[0].append(FlexStatementTestCase.data) 101 | self.data[0].append(FlexStatementTestCase.data) 102 | with self.assertRaises(parser.FlexParserError): 103 | parser.parse_data_element(self.data) 104 | 105 | 106 | class EquitySummaryByReportDateInBaseTestCase(unittest.TestCase): 107 | data = ET.fromstring( 108 | ('') 128 | ) 129 | 130 | def testParse(self): 131 | instance = parser.parse_data_element(self.data) 132 | self.assertIsInstance(instance, Types.EquitySummaryByReportDateInBase) 133 | 134 | self.assertEqual(instance.accountId, 'U123456') 135 | self.assertEqual(instance.acctAlias, "ibflex test") 136 | self.assertEqual(instance.model, None) 137 | self.assertEqual(instance.reportDate, datetime.date(2011, 12, 30)) 138 | self.assertEqual(instance.cash, decimal.Decimal("51.730909701")) 139 | self.assertEqual(instance.cashLong, decimal.Decimal("51.730909701")) 140 | self.assertEqual(instance.cashShort, decimal.Decimal("0")) 141 | self.assertEqual(instance.slbCashCollateral, decimal.Decimal("0")) 142 | self.assertEqual(instance.slbCashCollateralLong, decimal.Decimal("0")) 143 | self.assertEqual(instance.slbCashCollateralShort, decimal.Decimal("0")) 144 | self.assertEqual(instance.stock, decimal.Decimal("39.68")) 145 | self.assertEqual(instance.stockLong, decimal.Decimal("44.68")) 146 | self.assertEqual(instance.stockShort, decimal.Decimal("-46")) 147 | self.assertEqual(instance.slbDirectSecuritiesBorrowed, decimal.Decimal("0")) 148 | self.assertEqual(instance.slbDirectSecuritiesBorrowedLong, decimal.Decimal("0")) 149 | self.assertEqual(instance.slbDirectSecuritiesBorrowedShort, decimal.Decimal("0")) 150 | self.assertEqual(instance.slbDirectSecuritiesLent, decimal.Decimal("0")) 151 | self.assertEqual(instance.slbDirectSecuritiesLentLong, decimal.Decimal("0")) 152 | self.assertEqual(instance.slbDirectSecuritiesLentShort, decimal.Decimal("0")) 153 | self.assertEqual(instance.options, decimal.Decimal("0")) 154 | self.assertEqual(instance.optionsLong, decimal.Decimal("0")) 155 | self.assertEqual(instance.optionsShort, decimal.Decimal("0")) 156 | self.assertEqual(instance.commodities, decimal.Decimal("0")) 157 | self.assertEqual(instance.commoditiesLong, decimal.Decimal("0")) 158 | self.assertEqual(instance.commoditiesShort, decimal.Decimal("0")) 159 | self.assertEqual(instance.bonds, decimal.Decimal("0")) 160 | self.assertEqual(instance.bondsLong, decimal.Decimal("0")) 161 | self.assertEqual(instance.bondsShort, decimal.Decimal("0")) 162 | self.assertEqual(instance.notes, decimal.Decimal("0")) 163 | self.assertEqual(instance.notesLong, decimal.Decimal("0")) 164 | self.assertEqual(instance.notesShort, decimal.Decimal("0")) 165 | self.assertEqual(instance.funds, decimal.Decimal("0")) 166 | self.assertEqual(instance.fundsLong, decimal.Decimal("0")) 167 | self.assertEqual(instance.fundsShort, decimal.Decimal("0")) 168 | self.assertEqual(instance.interestAccruals, decimal.Decimal("-1111.05")) 169 | self.assertEqual(instance.interestAccrualsLong, decimal.Decimal("0")) 170 | self.assertEqual(instance.interestAccrualsShort, decimal.Decimal("-1111.05")) 171 | self.assertEqual(instance.softDollars, decimal.Decimal("0")) 172 | self.assertEqual(instance.softDollarsLong, decimal.Decimal("0")) 173 | self.assertEqual(instance.softDollarsShort, decimal.Decimal("0")) 174 | self.assertEqual(instance.forexCfdUnrealizedPl, decimal.Decimal("0")) 175 | self.assertEqual(instance.forexCfdUnrealizedPlLong, decimal.Decimal("0")) 176 | self.assertEqual(instance.forexCfdUnrealizedPlShort, decimal.Decimal("0")) 177 | self.assertEqual(instance.dividendAccruals, decimal.Decimal("3299.79")) 178 | self.assertEqual(instance.dividendAccrualsLong, decimal.Decimal("3299.79")) 179 | self.assertEqual(instance.dividendAccrualsShort, decimal.Decimal("0")) 180 | self.assertEqual(instance.fdicInsuredBankSweepAccount, decimal.Decimal("0")) 181 | self.assertEqual(instance.fdicInsuredBankSweepAccountLong, decimal.Decimal("0")) 182 | self.assertEqual(instance.fdicInsuredBankSweepAccountShort, decimal.Decimal("0")) 183 | self.assertEqual(instance.fdicInsuredBankSweepAccountCashComponent, None) 184 | self.assertEqual(instance.fdicInsuredBankSweepAccountCashComponentLong, None) 185 | self.assertEqual(instance.fdicInsuredBankSweepAccountCashComponentShort, None) 186 | self.assertEqual(instance.fdicInsuredAccountInterestAccruals, decimal.Decimal("0")) 187 | self.assertEqual(instance.fdicInsuredAccountInterestAccrualsLong, decimal.Decimal("0")) 188 | self.assertEqual(instance.fdicInsuredAccountInterestAccrualsShort, decimal.Decimal("0")) 189 | self.assertEqual(instance.fdicInsuredAccountInterestAccrualsComponent, None) 190 | self.assertEqual(instance.fdicInsuredAccountInterestAccrualsComponentLong, None) 191 | self.assertEqual(instance.fdicInsuredAccountInterestAccrualsComponentShort, None) 192 | self.assertEqual(instance.total, decimal.Decimal("40.1509097")) 193 | self.assertEqual(instance.totalLong, decimal.Decimal("44.2009097")) 194 | self.assertEqual(instance.totalShort, decimal.Decimal("-46.05")) 195 | self.assertEqual(instance.brokerInterestAccrualsComponent, None) 196 | self.assertEqual(instance.brokerCashComponent, None) 197 | self.assertEqual(instance.cfdUnrealizedPl, None) 198 | 199 | 200 | class CashReportCurrencyTestCase(unittest.TestCase): 201 | data = ET.fromstring( 202 | ('') 241 | ) 242 | 243 | def testParse(self): 244 | instance = parser.parse_data_element(self.data) 245 | self.assertIsInstance(instance, Types.CashReportCurrency) 246 | self.assertEqual(instance.accountId, "U123456") 247 | self.assertEqual(instance.acctAlias, "ibflex test") 248 | self.assertEqual(instance.model, None) 249 | self.assertEqual(instance.currency, "USD") 250 | self.assertEqual(instance.fromDate, datetime.date(2011, 1, 3)) 251 | self.assertEqual(instance.toDate, datetime.date(2011, 12, 30)) 252 | self.assertEqual(instance.startingCash, decimal.Decimal("30.702569078")) 253 | self.assertEqual(instance.startingCashSec, decimal.Decimal("30.702569078")) 254 | self.assertEqual(instance.startingCashCom, decimal.Decimal("0")) 255 | self.assertEqual(instance.clientFees, decimal.Decimal("0")) 256 | self.assertEqual(instance.clientFeesSec, decimal.Decimal("0")) 257 | self.assertEqual(instance.clientFeesCom, decimal.Decimal("0")) 258 | self.assertEqual(instance.commissions, decimal.Decimal("-45.445684")) 259 | self.assertEqual(instance.commissionsSec, decimal.Decimal("-45.445684")) 260 | self.assertEqual(instance.commissionsCom, decimal.Decimal("0")) 261 | self.assertEqual(instance.billableCommissions, decimal.Decimal("0")) 262 | self.assertEqual(instance.billableCommissionsSec, decimal.Decimal("0")) 263 | self.assertEqual(instance.billableCommissionsCom, decimal.Decimal("0")) 264 | self.assertEqual(instance.depositWithdrawals, decimal.Decimal("10.62")) 265 | self.assertEqual(instance.depositWithdrawalsSec, decimal.Decimal("10.62")) 266 | self.assertEqual(instance.depositWithdrawalsCom, decimal.Decimal("0")) 267 | self.assertEqual(instance.deposits, decimal.Decimal("13.62")) 268 | self.assertEqual(instance.depositsSec, decimal.Decimal("13.62")) 269 | self.assertEqual(instance.depositsCom, decimal.Decimal("0")) 270 | self.assertEqual(instance.withdrawals, decimal.Decimal("-24")) 271 | self.assertEqual(instance.withdrawalsSec, decimal.Decimal("-24")) 272 | self.assertEqual(instance.withdrawalsCom, decimal.Decimal("0")) 273 | self.assertEqual(instance.accountTransfers, decimal.Decimal("0")) 274 | self.assertEqual(instance.accountTransfersSec, decimal.Decimal("0")) 275 | self.assertEqual(instance.accountTransfersCom, decimal.Decimal("0")) 276 | self.assertEqual(instance.linkingAdjustments, decimal.Decimal("0")) 277 | self.assertEqual(instance.linkingAdjustmentsSec, decimal.Decimal("0")) 278 | self.assertEqual(instance.linkingAdjustmentsCom, decimal.Decimal("0")) 279 | self.assertEqual(instance.internalTransfers, decimal.Decimal("0")) 280 | self.assertEqual(instance.internalTransfersSec, decimal.Decimal("0")) 281 | self.assertEqual(instance.internalTransfersCom, decimal.Decimal("0")) 282 | self.assertEqual(instance.dividends, decimal.Decimal("34.74")) 283 | self.assertEqual(instance.dividendsSec, decimal.Decimal("34.74")) 284 | self.assertEqual(instance.dividendsCom, decimal.Decimal("0")) 285 | self.assertEqual(instance.insuredDepositInterest, decimal.Decimal("0")) 286 | self.assertEqual(instance.insuredDepositInterestSec, decimal.Decimal("0")) 287 | self.assertEqual(instance.insuredDepositInterestCom, decimal.Decimal("0")) 288 | self.assertEqual(instance.brokerInterest, decimal.Decimal("-64.57")) 289 | self.assertEqual(instance.brokerInterestSec, decimal.Decimal("-64.57")) 290 | self.assertEqual(instance.brokerInterestCom, decimal.Decimal("0")) 291 | self.assertEqual(instance.bondInterest, decimal.Decimal("0")) 292 | self.assertEqual(instance.bondInterestSec, decimal.Decimal("0")) 293 | self.assertEqual(instance.bondInterestCom, decimal.Decimal("0")) 294 | self.assertEqual(instance.cashSettlingMtm, decimal.Decimal("0")) 295 | self.assertEqual(instance.cashSettlingMtmSec, decimal.Decimal("0")) 296 | self.assertEqual(instance.cashSettlingMtmCom, decimal.Decimal("0")) 297 | self.assertEqual(instance.realizedVm, decimal.Decimal("0")) 298 | self.assertEqual(instance.realizedVmSec, decimal.Decimal("0")) 299 | self.assertEqual(instance.realizedVmCom, decimal.Decimal("0")) 300 | self.assertEqual(instance.cfdCharges, decimal.Decimal("0")) 301 | self.assertEqual(instance.cfdChargesSec, decimal.Decimal("0")) 302 | self.assertEqual(instance.cfdChargesCom, decimal.Decimal("0")) 303 | self.assertEqual(instance.netTradesSales, decimal.Decimal("19.608813")) 304 | self.assertEqual(instance.netTradesSalesSec, decimal.Decimal("19.608813")) 305 | self.assertEqual(instance.netTradesSalesCom, decimal.Decimal("0")) 306 | self.assertEqual(instance.netTradesPurchases, decimal.Decimal("-33.164799999")) 307 | self.assertEqual(instance.netTradesPurchasesSec, decimal.Decimal("-33.164799999")) 308 | self.assertEqual(instance.netTradesPurchasesCom, decimal.Decimal("0")) 309 | self.assertEqual(instance.advisorFees, decimal.Decimal("0")) 310 | self.assertEqual(instance.advisorFeesSec, decimal.Decimal("0")) 311 | self.assertEqual(instance.advisorFeesCom, decimal.Decimal("0")) 312 | self.assertEqual(instance.feesReceivables, decimal.Decimal("0")) 313 | self.assertEqual(instance.feesReceivablesSec, decimal.Decimal("0")) 314 | self.assertEqual(instance.feesReceivablesCom, decimal.Decimal("0")) 315 | self.assertEqual(instance.paymentInLieu, decimal.Decimal("-44.47")) 316 | self.assertEqual(instance.paymentInLieuSec, decimal.Decimal("-44.47")) 317 | self.assertEqual(instance.paymentInLieuCom, decimal.Decimal("0")) 318 | self.assertEqual(instance.transactionTax, decimal.Decimal("0")) 319 | self.assertEqual(instance.transactionTaxSec, decimal.Decimal("0")) 320 | self.assertEqual(instance.transactionTaxCom, decimal.Decimal("0")) 321 | self.assertEqual(instance.taxReceivables, decimal.Decimal("0")) 322 | self.assertEqual(instance.taxReceivablesSec, decimal.Decimal("0")) 323 | self.assertEqual(instance.taxReceivablesCom, decimal.Decimal("0")) 324 | self.assertEqual(instance.withholdingTax, decimal.Decimal("-27.07")) 325 | self.assertEqual(instance.withholdingTaxSec, decimal.Decimal("-27.07")) 326 | self.assertEqual(instance.withholdingTaxCom, decimal.Decimal("0")) 327 | self.assertEqual(instance.withholding871m, decimal.Decimal("0")) 328 | self.assertEqual(instance.withholding871mSec, decimal.Decimal("0")) 329 | self.assertEqual(instance.withholding871mCom, decimal.Decimal("0")) 330 | self.assertEqual(instance.withholdingCollectedTax, decimal.Decimal("0")) 331 | self.assertEqual(instance.withholdingCollectedTaxSec, decimal.Decimal("0")) 332 | self.assertEqual(instance.withholdingCollectedTaxCom, decimal.Decimal("0")) 333 | self.assertEqual(instance.salesTax, decimal.Decimal("0")) 334 | self.assertEqual(instance.salesTaxSec, decimal.Decimal("0")) 335 | self.assertEqual(instance.salesTaxCom, decimal.Decimal("0")) 336 | self.assertEqual(instance.fxTranslationGainLoss, decimal.Decimal("0")) 337 | self.assertEqual(instance.fxTranslationGainLossSec, decimal.Decimal("0")) 338 | self.assertEqual(instance.fxTranslationGainLossCom, decimal.Decimal("0")) 339 | self.assertEqual(instance.otherFees, decimal.Decimal("-521.22")) 340 | self.assertEqual(instance.otherFeesSec, decimal.Decimal("-521.22")) 341 | self.assertEqual(instance.otherFeesCom, decimal.Decimal("0")) 342 | self.assertEqual(instance.other, decimal.Decimal("0")) 343 | self.assertEqual(instance.otherSec, decimal.Decimal("0")) 344 | self.assertEqual(instance.otherCom, decimal.Decimal("0")) 345 | self.assertEqual(instance.endingCash, decimal.Decimal("51.730897778")) 346 | self.assertEqual(instance.endingCashSec, decimal.Decimal("51.730897778")) 347 | self.assertEqual(instance.endingCashCom, decimal.Decimal("0")) 348 | self.assertEqual(instance.endingSettledCash, decimal.Decimal("51.730897778")) 349 | self.assertEqual(instance.endingSettledCashSec, decimal.Decimal("51.730897778")) 350 | self.assertEqual(instance.endingSettledCashCom, decimal.Decimal("0")) 351 | 352 | 353 | class StatementOfFundsLineTestCase(unittest.TestCase): 354 | data = ET.fromstring( 355 | ('') 364 | ) 365 | 366 | def testParse(self): 367 | instance = parser.parse_data_element(self.data) 368 | self.assertIsInstance(instance, Types.StatementOfFundsLine) 369 | self.assertEqual(instance.accountId, "U123456") 370 | self.assertEqual(instance.acctAlias, "ibflex test") 371 | self.assertEqual(instance.model, None) 372 | self.assertEqual(instance.currency, "USD") 373 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 374 | self.assertEqual(instance.symbol, "ECRO") 375 | self.assertEqual(instance.description, "ECC CAPITAL CORP") 376 | self.assertEqual(instance.conid, "33205002") 377 | self.assertEqual(instance.securityID, None) 378 | self.assertEqual(instance.securityIDType, None) 379 | self.assertEqual(instance.cusip, None) 380 | self.assertEqual(instance.isin, None) 381 | self.assertEqual(instance.underlyingConid, None) 382 | self.assertEqual(instance.underlyingSymbol, None) 383 | self.assertEqual(instance.issuer, None) 384 | self.assertEqual(instance.multiplier, 1) 385 | self.assertEqual(instance.strike, None) 386 | self.assertEqual(instance.expiry, None) 387 | self.assertEqual(instance.putCall, None) 388 | self.assertEqual(instance.principalAdjustFactor, None) 389 | self.assertEqual(instance.reportDate, datetime.date(2011, 12, 27)) 390 | self.assertEqual(instance.date, datetime.datetime(2011, 12, 27)) 391 | self.assertEqual(instance.activityDescription, "Buy 38,900 ECC CAPITAL CORP ") 392 | self.assertEqual(instance.tradeID, "657898717") 393 | self.assertEqual(instance.debit, decimal.Decimal("-3185.60925")) 394 | self.assertEqual(instance.credit, None) 395 | self.assertEqual(instance.amount, decimal.Decimal("-3185.60925")) 396 | self.assertEqual(instance.balance, decimal.Decimal("53409.186538632")) 397 | self.assertEqual(instance.buySell, "BUY") 398 | 399 | 400 | class ChangeInPositionValueTestCase(unittest.TestCase): 401 | data = ET.fromstring( 402 | ('') 408 | ) 409 | 410 | def testParse(self): 411 | instance = parser.parse_data_element(self.data) 412 | self.assertIsInstance(instance, Types.ChangeInPositionValue) 413 | self.assertEqual(instance.accountId, "U123456") 414 | self.assertEqual(instance.acctAlias, "ibflex test") 415 | self.assertEqual(instance.model, None) 416 | self.assertEqual(instance.currency, "USD") 417 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 418 | self.assertEqual(instance.priorPeriodValue, decimal.Decimal("18.57")) 419 | self.assertEqual(instance.transactions, decimal.Decimal("14.931399999")) 420 | self.assertEqual(instance.mtmPriorPeriodPositions, decimal.Decimal("-16.1077")) 421 | self.assertEqual(instance.mtmTransactions, decimal.Decimal("-22.2354")) 422 | self.assertEqual(instance.corporateActions, decimal.Decimal("-11.425")) 423 | self.assertEqual(instance.other, decimal.Decimal("0")) 424 | self.assertEqual(instance.accountTransfers, decimal.Decimal("94.18")) 425 | self.assertEqual(instance.linkingAdjustments, decimal.Decimal("0")) 426 | self.assertEqual(instance.fxTranslationPnl, decimal.Decimal("0")) 427 | self.assertEqual(instance.futurePriceAdjustments, decimal.Decimal("0")) 428 | self.assertEqual(instance.settledCash, decimal.Decimal("0")) 429 | self.assertEqual(instance.endOfPeriodValue, decimal.Decimal("39.68")) 430 | 431 | 432 | class OpenPositionTestCase(unittest.TestCase): 433 | data = ET.fromstring( 434 | ('') 446 | ) 447 | 448 | def testParse(self): 449 | instance = parser.parse_data_element(self.data) 450 | self.assertIsInstance(instance, Types.OpenPosition) 451 | self.assertEqual(instance.accountId, "U123456") 452 | self.assertEqual(instance.acctAlias, "ibflex test") 453 | self.assertEqual(instance.model, None) 454 | self.assertEqual(instance.currency, "USD") 455 | self.assertEqual(instance.fxRateToBase, 1) 456 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 457 | self.assertEqual(instance.symbol, "VXX") 458 | self.assertEqual(instance.description, "IPATH S&P 500 VIX S/T FU ETN") 459 | self.assertEqual(instance.conid, "80789235") 460 | self.assertEqual(instance.securityID, None) 461 | self.assertEqual(instance.securityIDType, None) 462 | self.assertEqual(instance.cusip, None) 463 | self.assertEqual(instance.isin, None) 464 | self.assertEqual(instance.underlyingConid, None) 465 | self.assertEqual(instance.underlyingSymbol, None) 466 | self.assertEqual(instance.issuer, None) 467 | self.assertEqual(instance.multiplier, 1) 468 | self.assertEqual(instance.strike, None) 469 | self.assertEqual(instance.expiry, None) 470 | self.assertEqual(instance.putCall, None) 471 | self.assertEqual(instance.principalAdjustFactor, None) 472 | self.assertEqual(instance.reportDate, datetime.date(2011, 12, 30)) 473 | self.assertEqual(instance.position, decimal.Decimal("-100")) 474 | self.assertEqual(instance.markPrice, decimal.Decimal("35.53")) 475 | self.assertEqual(instance.positionValue, decimal.Decimal("-3553")) 476 | self.assertEqual(instance.openPrice, decimal.Decimal("34.405")) 477 | self.assertEqual(instance.costBasisPrice, decimal.Decimal("34.405")) 478 | self.assertEqual(instance.costBasisMoney, decimal.Decimal("-3440.5")) 479 | self.assertEqual(instance.percentOfNAV, None) 480 | self.assertEqual(instance.fifoPnlUnrealized, decimal.Decimal("-112.5")) 481 | self.assertEqual(instance.side, enums.LongShort.SHORT) 482 | self.assertEqual(instance.levelOfDetail, "LOT") 483 | self.assertEqual(instance.openDateTime, datetime.datetime(2011, 8, 8, 13, 44, 13)) 484 | self.assertEqual(instance.holdingPeriodDateTime, datetime.datetime(2011, 8, 8, 13, 44, 13)) 485 | self.assertEqual(instance.code, ()) 486 | self.assertEqual(instance.originatingOrderID, "308163094") 487 | self.assertEqual(instance.originatingTransactionID, "2368917073") 488 | self.assertEqual(instance.accruedInt, None) 489 | 490 | 491 | class FxLotTestCase(unittest.TestCase): 492 | data = ET.fromstring( 493 | ('') 499 | ) 500 | 501 | def testParse(self): 502 | instance = parser.parse_data_element(self.data) 503 | self.assertIsInstance(instance, Types.FxLot) 504 | self.assertEqual(instance.accountId, "U123456") 505 | self.assertEqual(instance.acctAlias, "ibflex test") 506 | self.assertEqual(instance.model, None) 507 | self.assertEqual(instance.assetCategory, enums.AssetClass.CASH) 508 | self.assertEqual(instance.reportDate, datetime.date(2013, 12, 31)) 509 | self.assertEqual(instance.functionalCurrency, "USD") 510 | self.assertEqual(instance.fxCurrency, "CAD") 511 | self.assertEqual(instance.quantity, decimal.Decimal("0.000012")) 512 | self.assertEqual(instance.costPrice, decimal.Decimal("1")) 513 | self.assertEqual(instance.costBasis, decimal.Decimal("-0.000012")) 514 | self.assertEqual(instance.closePrice, decimal.Decimal("0.94148")) 515 | self.assertEqual(instance.value, decimal.Decimal("0.000011")) 516 | self.assertEqual(instance.unrealizedPL, decimal.Decimal("-0.000001")) 517 | self.assertEqual(instance.code, ()) 518 | self.assertEqual(instance.lotDescription, "CASH: -0.0786 USD.CAD") 519 | self.assertEqual(instance.lotOpenDateTime, datetime.datetime(2011, 1, 25, 18, 4, 27)) 520 | self.assertEqual(instance.levelOfDetail, "LOT") 521 | 522 | 523 | class TradeTestCase(unittest.TestCase): 524 | data = ET.fromstring( 525 | ('') 545 | ) 546 | 547 | def testParse(self): 548 | instance = parser.parse_data_element(self.data) 549 | self.assertIsInstance(instance, Types.Trade) 550 | self.assertEqual(instance.accountId, "U123456") 551 | self.assertEqual(instance.acctAlias, "ibflex test") 552 | self.assertEqual(instance.model, None) 553 | self.assertEqual(instance.currency, "USD") 554 | self.assertEqual(instance.fxRateToBase, decimal.Decimal('1')) 555 | self.assertEqual(instance.assetCategory, enums.AssetClass.OPTION) 556 | self.assertEqual(instance.symbol, "VXX 110917C00005000") 557 | self.assertEqual(instance.description, "VXX 17SEP11 5.0 C") 558 | self.assertEqual(instance.conid, "83615386") 559 | self.assertEqual(instance.securityID, None) 560 | self.assertEqual(instance.securityIDType, None) 561 | self.assertEqual(instance.cusip, None) 562 | self.assertEqual(instance.isin, None) 563 | self.assertEqual(instance.underlyingConid, "80789235") 564 | self.assertEqual(instance.underlyingSymbol, "VXX") 565 | self.assertEqual(instance.issuer, None) 566 | self.assertEqual(instance.multiplier, decimal.Decimal('100')) 567 | self.assertEqual(instance.strike, decimal.Decimal('5')) 568 | self.assertEqual(instance.expiry, datetime.date(2011, 9, 17)) 569 | self.assertEqual(instance.putCall, enums.PutCall.CALL) 570 | self.assertEqual(instance.principalAdjustFactor, None) 571 | self.assertEqual(instance.tradeID, "594763148") 572 | self.assertEqual(instance.reportDate, datetime.date(2011, 8, 12)) 573 | self.assertEqual(instance.tradeDate, datetime.date(2011, 8, 11)) 574 | self.assertEqual(instance.tradeTime, datetime.time(16, 20, 0)) 575 | self.assertEqual(instance.settleDateTarget, datetime.date(2011, 8, 12)) 576 | self.assertEqual(instance.transactionType, enums.TradeType.BOOKTRADE) 577 | self.assertEqual(instance.exchange, None) 578 | self.assertEqual(instance.quantity, decimal.Decimal("3")) 579 | self.assertEqual(instance.tradePrice, decimal.Decimal("0")) 580 | self.assertEqual(instance.tradeMoney, decimal.Decimal("0")) 581 | self.assertEqual(instance.proceeds, decimal.Decimal("-0")) 582 | self.assertEqual(instance.taxes, decimal.Decimal("0")) 583 | self.assertEqual(instance.ibCommission, decimal.Decimal("0")) 584 | self.assertEqual(instance.ibCommissionCurrency, "USD") 585 | self.assertEqual(instance.netCash, decimal.Decimal("0")) 586 | self.assertEqual(instance.closePrice, decimal.Decimal("29.130974")) 587 | self.assertEqual(instance.openCloseIndicator, enums.OpenClose.CLOSE) 588 | self.assertEqual(instance.notes, (enums.Code.ASSIGNMENT, )) 589 | self.assertEqual(instance.cost, decimal.Decimal("8398.81122")) 590 | self.assertEqual(instance.fifoPnlRealized, decimal.Decimal("0")) 591 | self.assertEqual(instance.fxPnl, decimal.Decimal("0")) 592 | self.assertEqual(instance.mtmPnl, decimal.Decimal("8739.2922")) 593 | self.assertEqual(instance.origTradePrice, decimal.Decimal("0")) 594 | self.assertEqual(instance.origTradeDate, None) 595 | self.assertEqual(instance.origTradeID, None) 596 | self.assertEqual(instance.origOrderID, "0") 597 | self.assertEqual(instance.clearingFirmID, None) 598 | self.assertEqual(instance.transactionID, "2381339439") 599 | self.assertEqual(instance.buySell, enums.BuySell.BUY) 600 | self.assertEqual(instance.ibOrderID, "2381339439") 601 | self.assertEqual(instance.ibExecID, None) 602 | self.assertEqual(instance.brokerageOrderID, None) 603 | self.assertEqual(instance.orderReference, None) 604 | self.assertEqual(instance.volatilityOrderLink, None) 605 | self.assertEqual(instance.exchOrderId, None) 606 | self.assertEqual(instance.extExecID, None) 607 | self.assertEqual(instance.orderTime, None) 608 | self.assertEqual(instance.openDateTime, None) 609 | self.assertEqual(instance.holdingPeriodDateTime, None) 610 | self.assertEqual(instance.whenRealized, None) 611 | self.assertEqual(instance.whenReopened, None) 612 | self.assertEqual(instance.levelOfDetail, "EXECUTION") 613 | self.assertEqual(instance.changeInPrice, decimal.Decimal("0")) 614 | self.assertEqual(instance.changeInQuantity, decimal.Decimal("0")) 615 | self.assertEqual(instance.orderType, None) 616 | self.assertEqual(instance.traderID, None) 617 | self.assertEqual(instance.isAPIOrder, False) 618 | self.assertEqual(instance.accruedInt, decimal.Decimal("0")) 619 | self.assertEqual(instance.serialNumber, None) 620 | self.assertEqual(instance.deliveryType, None) 621 | self.assertEqual(instance.commodityType, None) 622 | self.assertEqual(instance.fineness, decimal.Decimal("0")) 623 | self.assertEqual(instance.weight, "0.0 ()") 624 | 625 | 626 | class TradeLotTestCase(unittest.TestCase): 627 | data = ET.fromstring( 628 | ('') 649 | ) 650 | 651 | def testParse(self): 652 | instance = parser.parse_data_element(self.data) 653 | self.assertIsInstance(instance, Types.Lot) 654 | self.assertEqual(instance.accountId, "U123456") 655 | self.assertEqual(instance.acctAlias, "ibflex test") 656 | self.assertEqual(instance.model, None) 657 | self.assertEqual(instance.currency, "USD") 658 | self.assertEqual(instance.fxRateToBase, decimal.Decimal('1')) 659 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 660 | self.assertEqual(instance.symbol, "VXX 110917C00005000") 661 | self.assertEqual(instance.description, "VXX 17SEP11 5.0 C") 662 | self.assertEqual(instance.conid, "83615386") 663 | self.assertEqual(instance.securityID, None) 664 | self.assertEqual(instance.securityIDType, None) 665 | self.assertEqual(instance.cusip, None) 666 | self.assertEqual(instance.isin, None) 667 | self.assertEqual(instance.underlyingConid, "80789235") 668 | self.assertEqual(instance.underlyingSymbol, "VXX") 669 | self.assertEqual(instance.issuer, None) 670 | self.assertEqual(instance.multiplier, decimal.Decimal('100')) 671 | self.assertEqual(instance.strike, decimal.Decimal('5')) 672 | self.assertEqual(instance.expiry, datetime.date(2011, 9, 17)) 673 | self.assertEqual(instance.putCall, enums.PutCall.CALL) 674 | self.assertEqual(instance.principalAdjustFactor, None) 675 | self.assertEqual(instance.tradeID, "594763148") 676 | self.assertEqual(instance.reportDate, datetime.date(2011, 8, 12)) 677 | self.assertEqual(instance.tradeDate, datetime.date(2011, 8, 11)) 678 | self.assertEqual(instance.tradeTime, datetime.time(16, 20, 0)) 679 | self.assertEqual(instance.settleDateTarget, datetime.date(2011, 8, 12)) 680 | self.assertEqual(instance.transactionType, enums.TradeType.BOOKTRADE) 681 | self.assertEqual(instance.exchange, None) 682 | self.assertEqual(instance.quantity, decimal.Decimal("3")) 683 | self.assertEqual(instance.tradePrice, decimal.Decimal("0")) 684 | self.assertEqual(instance.tradeMoney, decimal.Decimal("0")) 685 | self.assertEqual(instance.proceeds, decimal.Decimal("-0")) 686 | self.assertEqual(instance.taxes, decimal.Decimal("0")) 687 | self.assertEqual(instance.ibCommission, decimal.Decimal("0")) 688 | self.assertEqual(instance.ibCommissionCurrency, "USD") 689 | self.assertEqual(instance.netCash, decimal.Decimal("0")) 690 | self.assertEqual(instance.closePrice, decimal.Decimal("29.130974")) 691 | self.assertEqual(instance.openCloseIndicator, enums.OpenClose.CLOSE) 692 | self.assertEqual(instance.notes, (enums.Code.ASSIGNMENT, )) 693 | self.assertEqual(instance.cost, decimal.Decimal("8398.81122")) 694 | self.assertEqual(instance.fifoPnlRealized, decimal.Decimal("0")) 695 | self.assertEqual(instance.fxPnl, decimal.Decimal("0")) 696 | self.assertEqual(instance.mtmPnl, decimal.Decimal("8739.2922")) 697 | self.assertEqual(instance.origTradePrice, decimal.Decimal("0")) 698 | self.assertEqual(instance.origTradeDate, None) 699 | self.assertEqual(instance.origTradeID, None) 700 | self.assertEqual(instance.origOrderID, "0") 701 | self.assertEqual(instance.clearingFirmID, None) 702 | self.assertEqual(instance.transactionID, "2381339439") 703 | self.assertEqual(instance.buySell, enums.BuySell.BUY) 704 | self.assertEqual(instance.ibOrderID, "2381339439") 705 | self.assertEqual(instance.ibExecID, None) 706 | self.assertEqual(instance.brokerageOrderID, None) 707 | self.assertEqual(instance.orderReference, None) 708 | self.assertEqual(instance.volatilityOrderLink, None) 709 | self.assertEqual(instance.exchOrderId, None) 710 | self.assertEqual(instance.extExecID, None) 711 | self.assertEqual(instance.orderTime, None) 712 | self.assertEqual(instance.openDateTime, None) 713 | self.assertEqual(instance.holdingPeriodDateTime, None) 714 | self.assertEqual(instance.whenRealized, None) 715 | self.assertEqual(instance.whenReopened, None) 716 | self.assertEqual(instance.levelOfDetail, "EXECUTION") 717 | self.assertEqual(instance.changeInPrice, decimal.Decimal("0")) 718 | self.assertEqual(instance.changeInQuantity, decimal.Decimal("0")) 719 | self.assertEqual(instance.orderType, None) 720 | self.assertEqual(instance.traderID, None) 721 | self.assertEqual(instance.isAPIOrder, False) 722 | self.assertEqual(instance.accruedInt, decimal.Decimal("0")) 723 | self.assertEqual(instance.serialNumber, None) 724 | self.assertEqual(instance.deliveryType, None) 725 | self.assertEqual(instance.commodityType, None) 726 | self.assertEqual(instance.fineness, decimal.Decimal("0")) 727 | self.assertEqual(instance.weight, "0.0 ()") 728 | self.assertEqual(instance.origTransactionID, "1234") 729 | self.assertEqual(instance.relatedTransactionID, "3456") 730 | 731 | 732 | class TradeAutoFXTestCase(unittest.TestCase): 733 | data = ET.fromstring( 734 | ('') 741 | ) 742 | 743 | def testParse(self): 744 | instance = parser.parse_data_element(self.data) 745 | self.assertIsInstance(instance, Types.Trade) 746 | self.assertEqual(instance.currency, "USD") 747 | self.assertEqual(instance.symbol, "USD.EUR") 748 | self.assertEqual(instance.description, "USD.EUR") 749 | self.assertEqual(instance.dateTime, datetime.datetime(2024, 8, 1, 15, 30, 45)) 750 | self.assertEqual(instance.tradeDate, datetime.date(2024, 8, 1)) 751 | self.assertEqual(instance.quantity, decimal.Decimal("1337.0")) 752 | self.assertEqual(instance.tradePrice, decimal.Decimal("1.0")) 753 | self.assertEqual(instance.proceeds, decimal.Decimal("1337.0")) 754 | self.assertEqual(instance.notes, (enums.Code.AUTOFX, )) 755 | self.assertEqual(instance.buySell, enums.BuySell.BUY) 756 | self.assertEqual(instance.levelOfDetail, "EXECUTION") 757 | self.assertEqual(instance.assetCategory, enums.AssetClass.CASH) 758 | 759 | 760 | class OptionEAETestCase(unittest.TestCase): 761 | data = ET.fromstring( 762 | ('') 774 | ) 775 | 776 | def testParse(self): 777 | instance = parser.parse_data_element(self.data) 778 | self.assertIsInstance(instance, Types.OptionEAE) 779 | self.assertEqual(instance.accountId, "U123456") 780 | self.assertEqual(instance.acctAlias, "ibflex test") 781 | self.assertEqual(instance.model, None) 782 | self.assertEqual(instance.currency, "USD") 783 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 784 | self.assertEqual(instance.assetCategory, enums.AssetClass.OPTION) 785 | self.assertEqual(instance.symbol, "VXX 110805C00020000") 786 | self.assertEqual(instance.description, "VXX 05AUG11 20.0 C") 787 | self.assertEqual(instance.conid, "91900358") 788 | self.assertEqual(instance.securityID, None) 789 | self.assertEqual(instance.securityIDType, None) 790 | self.assertEqual(instance.cusip, None) 791 | self.assertEqual(instance.isin, None) 792 | self.assertEqual(instance.underlyingConid, "80789235") 793 | self.assertEqual(instance.underlyingSymbol, "VXX") 794 | self.assertEqual(instance.listingExchange, "IBIS") 795 | self.assertEqual(instance.underlyingSecurityID, None) 796 | self.assertEqual(instance.underlyingListingExchange, None) 797 | self.assertEqual(instance.issuer, None) 798 | self.assertEqual(instance.multiplier, decimal.Decimal("100")) 799 | self.assertEqual(instance.strike, decimal.Decimal("20")) 800 | self.assertEqual(instance.expiry, datetime.date(2011, 8, 5)) 801 | self.assertEqual(instance.putCall, enums.PutCall.CALL) 802 | self.assertEqual(instance.principalAdjustFactor, None) 803 | self.assertEqual(instance.date, datetime.date(2011, 8, 5)) 804 | self.assertEqual(instance.transactionType, enums.OptionAction.ASSIGN) 805 | self.assertEqual(instance.quantity, decimal.Decimal("20")) 806 | self.assertEqual(instance.tradePrice, decimal.Decimal("0.0000")) 807 | self.assertEqual(instance.markPrice, decimal.Decimal("0.0000")) 808 | self.assertEqual(instance.proceeds, decimal.Decimal("0.00")) 809 | self.assertEqual(instance.commisionsAndTax, decimal.Decimal("0.00")) 810 | self.assertEqual(instance.costBasis, decimal.Decimal("21792.73")) 811 | self.assertEqual(instance.realizedPnl, decimal.Decimal("0.00")) 812 | self.assertEqual(instance.fxPnl, decimal.Decimal("0.00")) 813 | self.assertEqual(instance.mtmPnl, decimal.Decimal("20620.00")) 814 | self.assertEqual(instance.tradeID, None) 815 | 816 | 817 | class TradeTransferTestCase(unittest.TestCase): 818 | data = ET.fromstring( 819 | ('') 838 | ) 839 | 840 | def testParse(self): 841 | instance = parser.parse_data_element(self.data) 842 | self.assertIsInstance(instance, Types.TradeTransfer) 843 | self.assertEqual(instance.accountId, "U123456") 844 | self.assertEqual(instance.acctAlias, "ibflex test") 845 | self.assertEqual(instance.model, None) 846 | self.assertEqual(instance.currency, "USD") 847 | self.assertEqual(instance.fxRateToBase, decimal.Decimal('1')) 848 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 849 | self.assertEqual(instance.symbol, "ADGI") 850 | self.assertEqual(instance.description, "ALLIED DEFENSE GROUP INC/THE") 851 | self.assertEqual(instance.conid, "764451") 852 | self.assertEqual(instance.securityID, None) 853 | self.assertEqual(instance.securityIDType, None) 854 | self.assertEqual(instance.cusip, None) 855 | self.assertEqual(instance.isin, None) 856 | self.assertEqual(instance.underlyingConid, None) 857 | self.assertEqual(instance.underlyingSymbol, None) 858 | self.assertEqual(instance.issuer, None) 859 | self.assertEqual(instance.multiplier, decimal.Decimal('1')) 860 | self.assertEqual(instance.strike, None) 861 | self.assertEqual(instance.expiry, None) 862 | self.assertEqual(instance.putCall, None) 863 | self.assertEqual(instance.principalAdjustFactor, None) 864 | self.assertEqual(instance.tradeID, "599063639") 865 | self.assertEqual(instance.reportDate, datetime.date(2011, 8, 22)) 866 | self.assertEqual(instance.tradeDate, datetime.date(2011, 8, 19)) 867 | self.assertEqual(instance.tradeTime, datetime.time(20,20, 0)) 868 | self.assertEqual(instance.settleDateTarget, datetime.date(2011, 8, 24)) 869 | self.assertEqual(instance.transactionType, enums.TradeType.DVPTRADE) 870 | self.assertEqual(instance.exchange, None) 871 | self.assertEqual(instance.quantity, decimal.Decimal("10000")) 872 | self.assertEqual(instance.tradePrice, decimal.Decimal("3.1")) 873 | self.assertEqual(instance.tradeMoney, decimal.Decimal("31000")) 874 | self.assertEqual(instance.proceeds, decimal.Decimal("-31010")) 875 | self.assertEqual(instance.taxes, decimal.Decimal("0")) 876 | self.assertEqual(instance.ibCommission, decimal.Decimal("-1")) 877 | self.assertEqual(instance.ibCommissionCurrency, "USD") 878 | self.assertEqual(instance.netCash, decimal.Decimal("-31011")) 879 | self.assertEqual(instance.closePrice, decimal.Decimal("3.02")) 880 | self.assertEqual(instance.openCloseIndicator, enums.OpenClose.OPEN) 881 | self.assertEqual(instance.notes, ()) 882 | self.assertEqual(instance.cost, decimal.Decimal("31011")) 883 | self.assertEqual(instance.fifoPnlRealized, decimal.Decimal("0")) 884 | self.assertEqual(instance.fxPnl, decimal.Decimal("0")) 885 | self.assertEqual(instance.mtmPnl, decimal.Decimal("-810")) 886 | self.assertEqual(instance.origTradePrice, decimal.Decimal("0")) 887 | self.assertEqual(instance.origTradeDate, None) 888 | self.assertEqual(instance.origTradeID, None) 889 | self.assertEqual(instance.origOrderID, "0") 890 | self.assertEqual(instance.clearingFirmID, "94378") 891 | self.assertEqual(instance.transactionID, None) 892 | self.assertEqual(instance.brokerName, "E*Trade Clearing LLC") 893 | self.assertEqual(instance.brokerAccount, "1234-5678") 894 | self.assertEqual(instance.awayBrokerCommission, decimal.Decimal("10")) 895 | self.assertEqual(instance.regulatoryFee, decimal.Decimal("0")) 896 | self.assertEqual(instance.direction, enums.ToFrom.FROM) 897 | self.assertEqual(instance.deliveredReceived, enums.DeliveredReceived.RECEIVED) 898 | self.assertEqual(instance.netTradeMoney, decimal.Decimal("31010")) 899 | self.assertEqual(instance.netTradeMoneyInBase, decimal.Decimal("31010")) 900 | self.assertEqual(instance.netTradePrice, decimal.Decimal("3.101")) 901 | self.assertEqual(instance.openDateTime, None) 902 | self.assertEqual(instance.holdingPeriodDateTime, None) 903 | self.assertEqual(instance.whenRealized, None) 904 | self.assertEqual(instance.whenReopened, None) 905 | self.assertEqual(instance.levelOfDetail, "TRADE_TRANSFERS") 906 | 907 | 908 | class FxTransactionTestCase(unittest.TestCase): 909 | data = ET.fromstring( 910 | ('') 915 | ) 916 | 917 | def testParse(self): 918 | instance = parser.parse_data_element(self.data) 919 | self.assertIsInstance(instance, Types.FxTransaction) 920 | self.assertEqual(instance.accountId, "U123456") 921 | self.assertEqual(instance.acctAlias, "ibflex test") 922 | self.assertEqual(instance.model, None) 923 | self.assertEqual(instance.assetCategory, enums.AssetClass.CASH) 924 | self.assertEqual(instance.reportDate, datetime.date(2023, 1, 5)) 925 | self.assertEqual(instance.functionalCurrency, "CAD") 926 | self.assertEqual(instance.fxCurrency, "USD") 927 | self.assertEqual(instance.activityDescription, "Net cash activity") 928 | self.assertEqual(instance.dateTime, datetime.datetime(2023, 1, 5)) 929 | self.assertEqual(instance.quantity, decimal.Decimal("55.94")) 930 | self.assertEqual(instance.proceeds, decimal.Decimal("75.904986")) 931 | self.assertEqual(instance.cost, decimal.Decimal("-75.904986")) 932 | self.assertEqual(instance.realizedPL, decimal.Decimal("0")) 933 | self.assertEqual(instance.code, (enums.Code.OPENING, )) 934 | self.assertEqual(instance.levelOfDetail, "TRANSACTION") 935 | 936 | 937 | class CashTransactionTestCase(unittest.TestCase): 938 | data = ET.fromstring( 939 | ('') 947 | ) 948 | 949 | def testParse(self): 950 | instance = parser.parse_data_element(self.data) 951 | self.assertIsInstance(instance, Types.CashTransaction) 952 | self.assertEqual(instance.accountId, "U123456") 953 | self.assertEqual(instance.acctAlias, "ibflex test") 954 | self.assertEqual(instance.model, None) 955 | self.assertEqual(instance.currency, "USD") 956 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 957 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 958 | self.assertEqual(instance.symbol, "RHDGF") 959 | self.assertEqual(instance.description, "RHDGF(ANN741081064) CASH DIVIDEND 1.00000000 USD PER SHARE (Return of Capital)") 960 | self.assertEqual(instance.conid, "62049667") 961 | self.assertEqual(instance.securityID, "ANN741081064") 962 | self.assertEqual(instance.securityIDType, "ISIN") 963 | self.assertEqual(instance.cusip, None) 964 | self.assertEqual(instance.isin, "ANN741081064") 965 | self.assertEqual(instance.underlyingConid, None) 966 | self.assertEqual(instance.underlyingSymbol, None) 967 | self.assertEqual(instance.issuer, None) 968 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 969 | self.assertEqual(instance.strike, None) 970 | self.assertEqual(instance.expiry, None) 971 | self.assertEqual(instance.putCall, None) 972 | self.assertEqual(instance.principalAdjustFactor, None) 973 | self.assertEqual(instance.dateTime, datetime.datetime(2015, 10, 6)) 974 | self.assertEqual(instance.amount, decimal.Decimal("27800")) 975 | self.assertEqual(instance.type, enums.CashAction.DIVIDEND) 976 | self.assertEqual(instance.tradeID, None) 977 | self.assertEqual(instance.code, ()) 978 | self.assertEqual(instance.transactionID, "5767420360") 979 | self.assertEqual(instance.reportDate, datetime.date(2015,10, 6)) 980 | self.assertEqual(instance.clientReference, None) 981 | 982 | 983 | class DebitCardActivityTestCase(unittest.TestCase): 984 | data = ET.fromstring( 985 | ('') 990 | ) 991 | 992 | def testParse(self): 993 | instance = parser.parse_data_element(self.data) 994 | self.assertIsInstance(instance, Types.DebitCardActivity) 995 | self.assertEqual(instance.accountId, "U123456") 996 | self.assertEqual(instance.acctAlias, "ibflex test") 997 | self.assertEqual(instance.model, None) 998 | self.assertEqual(instance.currency, "BASE_SUMMARY") 999 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1000 | self.assertEqual(instance.assetCategory, None) 1001 | self.assertEqual(instance.status, "Settled") 1002 | self.assertEqual(instance.reportDate, datetime.date(2020, 11, 1)) 1003 | self.assertEqual(instance.postingDate, datetime.date(2020, 11, 2)) 1004 | self.assertEqual(instance.transactionDateTime, datetime.datetime(2020, 11, 10, 17, 20, 30)) 1005 | self.assertEqual(instance.category, "RETAIL") 1006 | self.assertEqual(instance.merchantNameLocation, "DTN") 1007 | self.assertEqual(instance.amount, decimal.Decimal("-117.00")) 1008 | 1009 | 1010 | class InterestAccrualsCurrencyTestCase(unittest.TestCase): 1011 | data = ET.fromstring( 1012 | ('') 1017 | ) 1018 | 1019 | def testParse(self): 1020 | instance = parser.parse_data_element(self.data) 1021 | self.assertIsInstance(instance, Types.InterestAccrualsCurrency) 1022 | self.assertEqual(instance.accountId, "U123456") 1023 | self.assertEqual(instance.acctAlias, "ibflex test") 1024 | self.assertEqual(instance.model, None) 1025 | self.assertEqual(instance.currency, "BASE_SUMMARY") 1026 | self.assertEqual(instance.fromDate, datetime.date(2011, 1, 3)) 1027 | self.assertEqual(instance.toDate, datetime.date(2011, 12, 30)) 1028 | self.assertEqual(instance.startingAccrualBalance, decimal.Decimal("-11.558825")) 1029 | self.assertEqual(instance.interestAccrued, decimal.Decimal("-7516.101776")) 1030 | self.assertEqual(instance.accrualReversal, decimal.Decimal("6416.624437")) 1031 | self.assertEqual(instance.fxTranslation, decimal.Decimal("-0.013836")) 1032 | self.assertEqual(instance.endingAccrualBalance, decimal.Decimal("-1111.05")) 1033 | 1034 | 1035 | class SLBActivityTestCase(unittest.TestCase): 1036 | data = ET.fromstring( 1037 | ('') 1046 | ) 1047 | 1048 | def testParse(self): 1049 | instance = parser.parse_data_element(self.data) 1050 | self.assertIsInstance(instance, Types.SLBActivity) 1051 | self.assertEqual(instance.accountId, "U123456") 1052 | self.assertEqual(instance.acctAlias, "ibflex test") 1053 | self.assertEqual(instance.model, None) 1054 | self.assertEqual(instance.currency, "USD") 1055 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1056 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1057 | self.assertEqual(instance.symbol, "CHTP.CVR") 1058 | self.assertEqual(instance.description, "CHELSEA THERAPEUTICS INTERNA - ESCROW") 1059 | self.assertEqual(instance.conid, "158060456") 1060 | self.assertEqual(instance.securityID, None) 1061 | self.assertEqual(instance.securityIDType, None) 1062 | self.assertEqual(instance.cusip, None) 1063 | self.assertEqual(instance.isin, None) 1064 | self.assertEqual(instance.underlyingConid, None) 1065 | self.assertEqual(instance.underlyingSymbol, None) 1066 | self.assertEqual(instance.issuer, None) 1067 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 1068 | self.assertEqual(instance.strike, None) 1069 | self.assertEqual(instance.expiry, None) 1070 | self.assertEqual(instance.putCall, None) 1071 | self.assertEqual(instance.principalAdjustFactor, None) 1072 | self.assertEqual(instance.date, datetime.date(2015, 6, 1)) 1073 | self.assertEqual(instance.slbTransactionId, "SLB.32117554") 1074 | self.assertEqual(instance.activityDescription, "New Loan Allocation") 1075 | self.assertEqual(instance.type, "ManagedLoan") 1076 | self.assertEqual(instance.exchange, None) 1077 | self.assertEqual(instance.quantity, decimal.Decimal("-48330")) 1078 | self.assertEqual(instance.feeRate, decimal.Decimal("0.44")) 1079 | self.assertEqual(instance.collateralAmount, decimal.Decimal("48330")) 1080 | self.assertEqual(instance.markQuantity, decimal.Decimal("0")) 1081 | self.assertEqual(instance.markPriorPrice, decimal.Decimal("0")) 1082 | self.assertEqual(instance.markCurrentPrice, decimal.Decimal("0")) 1083 | 1084 | 1085 | class TransferTestCase(unittest.TestCase): 1086 | data = ET.fromstring( 1087 | ('') 1097 | ) 1098 | 1099 | def testParse(self): 1100 | instance = parser.parse_data_element(self.data) 1101 | self.assertIsInstance(instance, Types.Transfer) 1102 | self.assertEqual(instance.accountId, "U123456") 1103 | self.assertEqual(instance.acctAlias, "ibflex test") 1104 | self.assertEqual(instance.model, None) 1105 | self.assertEqual(instance.currency, "USD") 1106 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1107 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1108 | self.assertEqual(instance.symbol, "FMTIF") 1109 | self.assertEqual(instance.description, "FMI HOLDINGS LTD") 1110 | self.assertEqual(instance.conid, "86544467") 1111 | self.assertEqual(instance.securityID, None) 1112 | self.assertEqual(instance.securityIDType, None) 1113 | self.assertEqual(instance.cusip, None) 1114 | self.assertEqual(instance.isin, None) 1115 | self.assertEqual(instance.underlyingConid, None) 1116 | self.assertEqual(instance.underlyingSymbol, None) 1117 | self.assertEqual(instance.issuer, None) 1118 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 1119 | self.assertEqual(instance.strike, None) 1120 | self.assertEqual(instance.expiry, None) 1121 | self.assertEqual(instance.putCall, None) 1122 | self.assertEqual(instance.principalAdjustFactor, None) 1123 | self.assertEqual(instance.date, datetime.date(2011, 7, 18)) 1124 | self.assertEqual(instance.type, enums.TransferType.ACATS) 1125 | self.assertEqual(instance.direction, enums.InOut.IN) 1126 | self.assertEqual(instance.company, None) 1127 | self.assertEqual(instance.account, "12345678") 1128 | self.assertEqual(instance.accountName, None) 1129 | self.assertEqual(instance.quantity, decimal.Decimal("226702")) 1130 | self.assertEqual(instance.transferPrice, decimal.Decimal("0")) 1131 | self.assertEqual(instance.positionAmount, decimal.Decimal("11.51")) 1132 | self.assertEqual(instance.positionAmountInBase, decimal.Decimal("11.51")) 1133 | self.assertEqual(instance.pnlAmount, decimal.Decimal("0")) 1134 | self.assertEqual(instance.pnlAmountInBase, decimal.Decimal("0")) 1135 | self.assertEqual(instance.fxPnl, decimal.Decimal("0")) 1136 | self.assertEqual(instance.cashTransfer, decimal.Decimal("0")) 1137 | self.assertEqual(instance.code, ()) 1138 | self.assertEqual(instance.clientReference, None) 1139 | 1140 | 1141 | class TransferLotTestCase(unittest.TestCase): 1142 | data = ET.fromstring( 1143 | ('') 1151 | ) 1152 | 1153 | def testParse(self): 1154 | instance = parser.parse_data_element(self.data) 1155 | self.assertIsInstance(instance, Types.TransferLot) 1156 | self.assertEqual(instance.accountId, "U123456") 1157 | self.assertEqual(instance.currency, "USD") 1158 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1159 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1160 | self.assertEqual(instance.symbol, "FMTIF") 1161 | self.assertEqual(instance.description, "FMI HOLDINGS LTD") 1162 | self.assertEqual(instance.conid, "86544467") 1163 | self.assertEqual(instance.securityID, None) 1164 | self.assertEqual(instance.securityIDType, None) 1165 | self.assertEqual(instance.cusip, "02K123K") 1166 | self.assertEqual(instance.isin, None) 1167 | self.assertEqual(instance.listingExchange, "NYSE") 1168 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 1169 | self.assertEqual(instance.reportDate, datetime.date(2011, 7, 18)) 1170 | self.assertEqual(instance.date, datetime.date(2011, 7, 18)) 1171 | self.assertEqual(instance.dateTime, datetime.datetime(2011, 7, 18, 0, 0, 0)) 1172 | self.assertEqual(instance.type, enums.TransferType.FOP) 1173 | self.assertEqual(instance.direction, enums.InOut.IN) 1174 | self.assertEqual(instance.company, 'HOOLI') 1175 | self.assertEqual(instance.account, "12345678") 1176 | self.assertEqual(instance.deliveringBroker, "12345") 1177 | self.assertEqual(instance.quantity, decimal.Decimal("701.5")) 1178 | self.assertEqual(instance.transferPrice, decimal.Decimal("0")) 1179 | self.assertEqual(instance.pnlAmount, decimal.Decimal("0")) 1180 | self.assertEqual(instance.pnlAmountInBase, decimal.Decimal("0")) 1181 | self.assertEqual(instance.code, (enums.Code.STCG, )) 1182 | 1183 | 1184 | class CorporateActionTestCase(unittest.TestCase): 1185 | data = ET.fromstring( 1186 | ('') 1194 | ) 1195 | 1196 | def testParse(self): 1197 | instance = parser.parse_data_element(self.data) 1198 | self.assertIsInstance(instance, Types.CorporateAction) 1199 | self.assertEqual(instance.accountId, "U123456") 1200 | self.assertEqual(instance.acctAlias, "ibflex test") 1201 | self.assertEqual(instance.model, None) 1202 | self.assertEqual(instance.currency, "USD") 1203 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1204 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1205 | self.assertEqual(instance.symbol, "NILSY.TEN") 1206 | self.assertEqual(instance.description, "NILSY.TEN(466992534) MERGED(Voluntary Offer Allocation) FOR USD 30.60000000 PER SHARE (NILSY.TEN, MMC NORILSK NICKEL JSC-ADR - TENDER, 466992534)") 1207 | self.assertEqual(instance.conid, "96835898") 1208 | self.assertEqual(instance.securityID, None) 1209 | self.assertEqual(instance.securityIDType, None) 1210 | self.assertEqual(instance.cusip, None) 1211 | self.assertEqual(instance.isin, None) 1212 | self.assertEqual(instance.underlyingConid, None) 1213 | self.assertEqual(instance.underlyingSymbol, None) 1214 | self.assertEqual(instance.issuer, None) 1215 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 1216 | self.assertEqual(instance.strike, None) 1217 | self.assertEqual(instance.expiry, None) 1218 | self.assertEqual(instance.putCall, None) 1219 | self.assertEqual(instance.principalAdjustFactor, None) 1220 | self.assertEqual(instance.reportDate, datetime.date(2011, 11, 3)) 1221 | self.assertEqual(instance.dateTime, datetime.datetime(2011, 11, 2, 20, 25, 0)) 1222 | self.assertEqual(instance.amount, decimal.Decimal("-30600")) 1223 | self.assertEqual(instance.proceeds, decimal.Decimal("30600")) 1224 | self.assertEqual(instance.value, decimal.Decimal("-18110")) 1225 | self.assertEqual(instance.quantity, decimal.Decimal("-1000")) 1226 | self.assertEqual(instance.fifoPnlRealized, decimal.Decimal("10315")) 1227 | self.assertEqual(instance.mtmPnl, decimal.Decimal("12490")) 1228 | self.assertEqual(instance.code, ()) 1229 | self.assertEqual(instance.type, enums.Reorg.MERGER) 1230 | 1231 | 1232 | class ChangeInDividendAccrualTestCase(unittest.TestCase): 1233 | data = ET.fromstring( 1234 | ('') 1242 | ) 1243 | 1244 | def testParse(self): 1245 | instance = parser.parse_data_element(self.data) 1246 | self.assertIsInstance(instance, Types.ChangeInDividendAccrual) 1247 | self.assertEqual(instance.accountId, "U123456") 1248 | self.assertEqual(instance.acctAlias, "ibflex test") 1249 | self.assertEqual(instance.model, None) 1250 | self.assertEqual(instance.currency, "USD") 1251 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1252 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1253 | self.assertEqual(instance.symbol, "RHDGF") 1254 | self.assertEqual(instance.description, "RETAIL HOLDINGS NV") 1255 | self.assertEqual(instance.conid, "62049667") 1256 | self.assertEqual(instance.securityID, "ANN741081064") 1257 | self.assertEqual(instance.securityIDType, "ISIN") 1258 | self.assertEqual(instance.cusip, None) 1259 | self.assertEqual(instance.isin, "ANN741081064") 1260 | self.assertEqual(instance.underlyingConid, None) 1261 | self.assertEqual(instance.underlyingSymbol, None) 1262 | self.assertEqual(instance.issuer, None) 1263 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 1264 | self.assertEqual(instance.strike, None) 1265 | self.assertEqual(instance.expiry, None) 1266 | self.assertEqual(instance.putCall, None) 1267 | self.assertEqual(instance.principalAdjustFactor, None) 1268 | self.assertEqual(instance.date, datetime.date(2011, 9, 21)) 1269 | self.assertEqual(instance.exDate, datetime.date(2011, 9, 22)) 1270 | self.assertEqual(instance.payDate, datetime.date(2011, 10, 11)) 1271 | self.assertEqual(instance.quantity, decimal.Decimal("13592")) 1272 | self.assertEqual(instance.tax, decimal.Decimal("0")) 1273 | self.assertEqual(instance.fee, decimal.Decimal("0")) 1274 | self.assertEqual(instance.grossRate, decimal.Decimal("2.5")) 1275 | self.assertEqual(instance.grossAmount, decimal.Decimal("33980")) 1276 | self.assertEqual(instance.netAmount, decimal.Decimal("33980")) 1277 | self.assertEqual(instance.code, (enums.Code.POSTACCRUAL, )) 1278 | self.assertEqual(instance.fromAcct, None) 1279 | self.assertEqual(instance.toAcct, None) 1280 | 1281 | 1282 | class OpenDividendAccrualTestCase(unittest.TestCase): 1283 | data = ET.fromstring( 1284 | ('') 1293 | ) 1294 | 1295 | def testParse(self): 1296 | instance = parser.parse_data_element(self.data) 1297 | self.assertIsInstance(instance, Types.OpenDividendAccrual) 1298 | self.assertEqual(instance.accountId, "U123456") 1299 | self.assertEqual(instance.acctAlias, "ibflex test") 1300 | self.assertEqual(instance.model, None) 1301 | self.assertEqual(instance.currency, "USD") 1302 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1303 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1304 | self.assertEqual(instance.symbol, "CASH") 1305 | self.assertEqual(instance.description, "META FINANCIAL GROUP INC") 1306 | self.assertEqual(instance.conid, "3655441") 1307 | self.assertEqual(instance.securityID, None) 1308 | self.assertEqual(instance.securityIDType, None) 1309 | self.assertEqual(instance.cusip, None) 1310 | self.assertEqual(instance.isin, None) 1311 | self.assertEqual(instance.listingExchange, "NYSE") 1312 | self.assertEqual(instance.underlyingConid, None) 1313 | self.assertEqual(instance.underlyingSymbol, None) 1314 | self.assertEqual(instance.underlyingSecurityID, None) 1315 | self.assertEqual(instance.underlyingListingExchange, None) 1316 | self.assertEqual(instance.issuer, None) 1317 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 1318 | self.assertEqual(instance.strike, None) 1319 | self.assertEqual(instance.expiry, None) 1320 | self.assertEqual(instance.putCall, None) 1321 | self.assertEqual(instance.principalAdjustFactor, None) 1322 | self.assertEqual(instance.exDate, datetime.date(2011, 12, 8)) 1323 | self.assertEqual(instance.payDate, datetime.date(2012, 1, 1)) 1324 | self.assertEqual(instance.quantity, decimal.Decimal("25383")) 1325 | self.assertEqual(instance.tax, decimal.Decimal("0")) 1326 | self.assertEqual(instance.fee, decimal.Decimal("0")) 1327 | self.assertEqual(instance.grossRate, decimal.Decimal("0.13")) 1328 | self.assertEqual(instance.grossAmount, decimal.Decimal("3299.79")) 1329 | self.assertEqual(instance.netAmount, decimal.Decimal("3299.79")) 1330 | self.assertEqual(instance.code, ()) 1331 | self.assertEqual(instance.fromAcct, None) 1332 | self.assertEqual(instance.toAcct, None) 1333 | 1334 | 1335 | class SecurityInfoTestCase(unittest.TestCase): 1336 | data = ET.fromstring( 1337 | ('') 1342 | ) 1343 | 1344 | def testParse(self): 1345 | instance = parser.parse_data_element(self.data) 1346 | self.assertIsInstance(instance, Types.SecurityInfo) 1347 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1348 | self.assertEqual(instance.symbol, "VXX") 1349 | self.assertEqual(instance.description, "IPATH S&P 500 VIX S/T FU ETN") 1350 | self.assertEqual(instance.conid, "80789235") 1351 | self.assertEqual(instance.securityID, None) 1352 | self.assertEqual(instance.securityIDType, None) 1353 | self.assertEqual(instance.cusip, None) 1354 | self.assertEqual(instance.isin, None) 1355 | self.assertEqual(instance.underlyingConid, None) 1356 | self.assertEqual(instance.underlyingSymbol, None) 1357 | self.assertEqual(instance.issuer, None) 1358 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 1359 | self.assertEqual(instance.strike, None) 1360 | self.assertEqual(instance.expiry, None) 1361 | self.assertEqual(instance.putCall, None) 1362 | self.assertEqual(instance.principalAdjustFactor, decimal.Decimal("1")) 1363 | self.assertEqual(instance.maturity, None) 1364 | self.assertEqual(instance.issueDate, None) 1365 | self.assertEqual(instance.code, ()) 1366 | 1367 | 1368 | class ConversionRateTestCase(unittest.TestCase): 1369 | data = ET.fromstring( 1370 | """""" 1371 | ) 1372 | 1373 | def testParse(self): 1374 | instance = parser.parse_data_element(self.data) 1375 | self.assertIsInstance(instance, Types.ConversionRate) 1376 | self.assertEqual(instance.reportDate, datetime.date(2011, 12, 30)) 1377 | self.assertEqual(instance.fromCurrency, "HKD") 1378 | self.assertEqual(instance.toCurrency, "USD") 1379 | self.assertEqual(instance.rate, decimal.Decimal("0.12876")) 1380 | 1381 | 1382 | class TransactionTaxTestCase(unittest.TestCase): 1383 | data = ET.fromstring( 1384 | ('') 1393 | ) 1394 | 1395 | def testParse(self): 1396 | instance = parser.parse_data_element(self.data) 1397 | self.assertIsInstance(instance, Types.TransactionTax) 1398 | self.assertEqual(instance.accountId, "U123456") 1399 | self.assertEqual(instance.acctAlias, "ibflex test") 1400 | self.assertEqual(instance.model, None) 1401 | self.assertEqual(instance.currency, "USD") 1402 | self.assertEqual(instance.fxRateToBase, decimal.Decimal('1')) 1403 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1404 | self.assertEqual(instance.symbol, "SNY") 1405 | self.assertEqual(instance.description, "SANOFI-ADR") 1406 | self.assertEqual(instance.conid, "1234578") 1407 | self.assertEqual(instance.securityID, "80105N105") 1408 | self.assertEqual(instance.securityIDType, "CUSIP") 1409 | self.assertEqual(instance.cusip, "80105N105") 1410 | self.assertEqual(instance.isin, None) 1411 | self.assertEqual(instance.listingExchange, "NASDAQ") 1412 | self.assertEqual(instance.underlyingConid, None) 1413 | self.assertEqual(instance.underlyingSymbol, None) 1414 | self.assertEqual(instance.underlyingSecurityID, None) 1415 | self.assertEqual(instance.underlyingListingExchange, None) 1416 | self.assertEqual(instance.issuer, None) 1417 | self.assertEqual(instance.multiplier, decimal.Decimal("1")) 1418 | self.assertEqual(instance.strike, None) 1419 | self.assertEqual(instance.expiry, None) 1420 | self.assertEqual(instance.putCall, None) 1421 | self.assertEqual(instance.principalAdjustFactor, None) 1422 | self.assertEqual(instance.date, datetime.datetime(2013, 11, 2)) 1423 | self.assertEqual(instance.taxDescription, "French Transaction Tax") 1424 | self.assertEqual(instance.quantity, decimal.Decimal('0')) 1425 | self.assertEqual(instance.reportDate, datetime.date(2013, 11, 2)) 1426 | self.assertEqual(instance.taxAmount, decimal.Decimal("-0.347098")) 1427 | self.assertEqual(instance.tradeId, "12345678550") 1428 | self.assertEqual(instance.tradePrice, decimal.Decimal("0")) 1429 | self.assertEqual(instance.source, "STANDALONE") 1430 | self.assertEqual(instance.code, ()) 1431 | self.assertEqual(instance.levelOfDetail, "SUMMARY") 1432 | 1433 | 1434 | class SalesTaxTestCase(unittest.TestCase): 1435 | data = ET.fromstring( 1436 | ('')) 1446 | 1447 | def testParse(self): 1448 | instance = parser.parse_data_element(self.data) 1449 | self.assertIsInstance(instance, Types.SalesTax) 1450 | self.assertEqual(instance.accountId, "U123456") 1451 | self.assertEqual(instance.acctAlias, None) 1452 | self.assertEqual(instance.model, None) 1453 | self.assertEqual(instance.currency, "USD") 1454 | self.assertEqual(instance.fxRateToBase, decimal.Decimal('1')) 1455 | self.assertEqual(instance.assetCategory, None) 1456 | self.assertEqual(instance.symbol, None) 1457 | self.assertEqual(instance.description, None) 1458 | self.assertEqual(instance.conid, None) 1459 | self.assertEqual(instance.securityID, None) 1460 | self.assertEqual(instance.securityIDType, None) 1461 | self.assertEqual(instance.cusip, None) 1462 | self.assertEqual(instance.isin, None) 1463 | self.assertEqual(instance.listingExchange, None) 1464 | self.assertEqual(instance.underlyingConid, None) 1465 | self.assertEqual(instance.underlyingSymbol, None) 1466 | self.assertEqual(instance.underlyingSecurityID, None) 1467 | self.assertEqual(instance.underlyingListingExchange, None) 1468 | self.assertEqual(instance.issuer, None) 1469 | self.assertEqual(instance.multiplier, None) 1470 | self.assertEqual(instance.strike, None) 1471 | self.assertEqual(instance.expiry, None) 1472 | self.assertEqual(instance.putCall, None) 1473 | self.assertEqual(instance.principalAdjustFactor, None) 1474 | self.assertEqual(instance.date, datetime.date(2015, 1, 3)) 1475 | self.assertEqual(instance.country, "Finland") 1476 | self.assertEqual(instance.taxType, "VAT") 1477 | self.assertEqual(instance.payer, "U123456") 1478 | self.assertEqual(instance.taxableDescription, "b****32:CUSIP (NP)") 1479 | self.assertEqual(instance.taxableAmount, decimal.Decimal('0.2')) 1480 | self.assertEqual(instance.taxRate, decimal.Decimal('0.21')) 1481 | self.assertEqual(instance.salesTax, decimal.Decimal('-0.042')) 1482 | self.assertEqual(instance.taxableTransactionID, "12913231356") 1483 | self.assertEqual(instance.transactionID, "12913221785") 1484 | self.assertEqual(instance.code, ()) 1485 | 1486 | 1487 | class OrderTestCase(unittest.TestCase): 1488 | data = ET.fromstring( 1489 | ('')) 1508 | 1509 | def testParse(self): 1510 | instance = parser.parse_data_element(self.data) 1511 | self.assertIsInstance(instance, Types.Order) 1512 | self.assertEqual(instance.accountId, "U123456") 1513 | self.assertEqual(instance.acctAlias, "Test Account") 1514 | self.assertEqual(instance.model, None) 1515 | self.assertEqual(instance.currency, "USD") 1516 | self.assertEqual(instance.assetCategory, enums.AssetClass.CASH) 1517 | self.assertEqual(instance.symbol, "EUR.USD") 1518 | self.assertEqual(instance.description, "EUR.USD") 1519 | self.assertEqual(instance.conid, "12087792") 1520 | self.assertEqual(instance.securityID, None) 1521 | self.assertEqual(instance.securityIDType, None) 1522 | self.assertEqual(instance.cusip, None) 1523 | self.assertEqual(instance.isin, None) 1524 | self.assertEqual(instance.listingExchange, None) 1525 | self.assertEqual(instance.underlyingConid, None) 1526 | self.assertEqual(instance.underlyingSymbol, None) 1527 | self.assertEqual(instance.underlyingSecurityID, None) 1528 | self.assertEqual(instance.underlyingListingExchange, None) 1529 | self.assertEqual(instance.issuer, None) 1530 | self.assertEqual(instance.multiplier, decimal.Decimal('1')) 1531 | self.assertEqual(instance.strike, None) 1532 | self.assertEqual(instance.expiry, None) 1533 | self.assertEqual(instance.putCall, None) 1534 | self.assertEqual(instance.principalAdjustFactor, None) 1535 | self.assertEqual(instance.transactionType, None) 1536 | self.assertEqual(instance.tradeID, None) 1537 | self.assertEqual(instance.orderID, decimal.Decimal('92965807')) 1538 | self.assertEqual(instance.execID, None) 1539 | self.assertEqual(instance.brokerageOrderID, None) 1540 | self.assertEqual(instance.orderReference, None) 1541 | self.assertEqual(instance.volatilityOrderLink, None) 1542 | self.assertEqual(instance.clearingFirmID, None) 1543 | self.assertEqual(instance.origTradePrice, None) 1544 | self.assertEqual(instance.origTradeDate, None) 1545 | self.assertEqual(instance.origTradeID, None) 1546 | # Despite the name, `orderTime` actually contains date/time data. 1547 | self.assertEqual(instance.orderTime, datetime.datetime(2021, 1, 11, 22, 16, 52)) 1548 | self.assertEqual(instance.dateTime, datetime.datetime(2021, 1, 12, 2, 16, 24)) 1549 | self.assertEqual(instance.reportDate, datetime.date(2021, 1, 12)) 1550 | self.assertEqual(instance.settleDate, datetime.date(2021, 1, 14)) 1551 | self.assertEqual(instance.tradeDate, datetime.date(2021, 1, 12)) 1552 | self.assertEqual(instance.exchange, None) 1553 | self.assertEqual(instance.buySell, enums.BuySell.BUY) 1554 | self.assertEqual(instance.quantity, decimal.Decimal("30000")) 1555 | self.assertEqual(instance.price, decimal.Decimal("1.21621")) 1556 | self.assertEqual(instance.amount, decimal.Decimal("36486.3")) 1557 | self.assertEqual(instance.proceeds, decimal.Decimal("-36486.3")) 1558 | self.assertEqual(instance.commission, decimal.Decimal("-2.557")) 1559 | self.assertEqual(instance.brokerExecutionCommission, None) 1560 | self.assertEqual(instance.brokerClearingCommission, None) 1561 | self.assertEqual(instance.thirdPartyExecutionCommission, None) 1562 | self.assertEqual(instance.thirdPartyClearingCommission, None) 1563 | self.assertEqual(instance.thirdPartyRegulatoryCommission, None) 1564 | self.assertEqual(instance.otherCommission, None) 1565 | self.assertEqual(instance.commissionCurrency, "CAD") 1566 | self.assertEqual(instance.tax, decimal.Decimal("0")) 1567 | self.assertEqual(instance.code, ()) 1568 | self.assertEqual(instance.orderType, enums.OrderType.LIMIT) 1569 | self.assertEqual(instance.levelOfDetail, "ORDER") 1570 | self.assertEqual(instance.traderID, None) 1571 | self.assertEqual(instance.isAPIOrder, None) 1572 | self.assertEqual(instance.allocatedTo, None) 1573 | self.assertEqual(instance.accruedInt, decimal.Decimal("0")) 1574 | 1575 | 1576 | class SymbolSummaryTestCase(unittest.TestCase): 1577 | data = ET.fromstring( 1578 | ('')) 1597 | 1598 | def testParse(self): 1599 | instance = parser.parse_data_element(self.data) 1600 | self.assertIsInstance(instance, Types.SymbolSummary) 1601 | self.assertEqual(instance.accountId, "U123456") 1602 | self.assertEqual(instance.acctAlias, "Test Account") 1603 | self.assertEqual(instance.model, None) 1604 | self.assertEqual(instance.currency, "USD") 1605 | self.assertEqual(instance.assetCategory, enums.AssetClass.CASH) 1606 | self.assertEqual(instance.symbol, "EUR.USD") 1607 | self.assertEqual(instance.description, "EUR.USD") 1608 | self.assertEqual(instance.conid, "12087792") 1609 | self.assertEqual(instance.securityID, None) 1610 | self.assertEqual(instance.securityIDType, None) 1611 | self.assertEqual(instance.cusip, None) 1612 | self.assertEqual(instance.isin, None) 1613 | self.assertEqual(instance.listingExchange, None) 1614 | self.assertEqual(instance.underlyingConid, None) 1615 | self.assertEqual(instance.underlyingSymbol, None) 1616 | self.assertEqual(instance.underlyingSecurityID, None) 1617 | self.assertEqual(instance.underlyingListingExchange, None) 1618 | self.assertEqual(instance.issuer, None) 1619 | self.assertEqual(instance.multiplier, decimal.Decimal('1')) 1620 | self.assertEqual(instance.strike, None) 1621 | self.assertEqual(instance.expiry, None) 1622 | self.assertEqual(instance.putCall, None) 1623 | self.assertEqual(instance.principalAdjustFactor, None) 1624 | self.assertEqual(instance.transactionType, None) 1625 | self.assertEqual(instance.tradeID, None) 1626 | self.assertEqual(instance.orderID, None) 1627 | self.assertEqual(instance.execID, None) 1628 | self.assertEqual(instance.brokerageOrderID, None) 1629 | self.assertEqual(instance.orderReference, None) 1630 | self.assertEqual(instance.volatilityOrderLink, None) 1631 | self.assertEqual(instance.clearingFirmID, None) 1632 | self.assertEqual(instance.origTradePrice, None) 1633 | self.assertEqual(instance.origTradeDate, None) 1634 | self.assertEqual(instance.origTradeID, None) 1635 | # Despite the name, `orderTime` actually contains date/time data. 1636 | self.assertEqual(instance.orderTime, None) 1637 | self.assertEqual(instance.dateTime, None) 1638 | self.assertEqual(instance.reportDate, datetime.date(2021, 1, 12)) 1639 | self.assertEqual(instance.settleDate, datetime.date(2021, 1, 14)) 1640 | self.assertEqual(instance.tradeDate, datetime.date(2021, 1, 12)) 1641 | self.assertEqual(instance.exchange, "IDEALFX") 1642 | self.assertEqual(instance.buySell, enums.BuySell.BUY) 1643 | self.assertEqual(instance.quantity, decimal.Decimal("30000")) 1644 | self.assertEqual(instance.price, decimal.Decimal("1.21621")) 1645 | self.assertEqual(instance.amount, decimal.Decimal("36486.3")) 1646 | self.assertEqual(instance.proceeds, decimal.Decimal("-36486.3")) 1647 | self.assertEqual(instance.commission, decimal.Decimal("-2.557")) 1648 | self.assertEqual(instance.brokerExecutionCommission, None) 1649 | self.assertEqual(instance.brokerClearingCommission, None) 1650 | self.assertEqual(instance.thirdPartyExecutionCommission, None) 1651 | self.assertEqual(instance.thirdPartyClearingCommission, None) 1652 | self.assertEqual(instance.thirdPartyRegulatoryCommission, None) 1653 | self.assertEqual(instance.otherCommission, None) 1654 | self.assertEqual(instance.commissionCurrency, "CAD") 1655 | self.assertEqual(instance.tax, decimal.Decimal("0")) 1656 | self.assertEqual(instance.code, ()) 1657 | self.assertEqual(instance.orderType, None) 1658 | self.assertEqual(instance.levelOfDetail, "SYMBOL_SUMMARY") 1659 | self.assertEqual(instance.traderID, None) 1660 | self.assertEqual(instance.isAPIOrder, None) 1661 | self.assertEqual(instance.allocatedTo, None) 1662 | self.assertEqual(instance.accruedInt, decimal.Decimal("0")) 1663 | 1664 | class AssetSummaryTestCase(unittest.TestCase): 1665 | data = ET.fromstring( 1666 | ('')) 1685 | 1686 | def testParse(self): 1687 | instance = parser.parse_data_element(self.data) 1688 | self.assertIsInstance(instance, Types.AssetSummary) 1689 | self.assertEqual(instance.accountId, "ABCDXYZ") 1690 | self.assertEqual(instance.acctAlias, None) 1691 | self.assertEqual(instance.model, None) 1692 | self.assertEqual(instance.currency, None) 1693 | self.assertEqual(instance.fxRateToBase, None) 1694 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1695 | self.assertEqual(instance.symbol, None) 1696 | self.assertEqual(instance.description, None) 1697 | self.assertEqual(instance.conid, None) 1698 | self.assertEqual(instance.securityID, None) 1699 | self.assertEqual(instance.securityIDType, None) 1700 | self.assertEqual(instance.cusip, None) 1701 | self.assertEqual(instance.isin, None) 1702 | self.assertEqual(instance.listingExchange, None) 1703 | self.assertEqual(instance.underlyingConid, None) 1704 | self.assertEqual(instance.underlyingSymbol, None) 1705 | self.assertEqual(instance.underlyingSecurityID, None) 1706 | self.assertEqual(instance.underlyingListingExchange, None) 1707 | self.assertEqual(instance.issuer, None) 1708 | self.assertEqual(instance.multiplier, None) 1709 | self.assertEqual(instance.strike, None) 1710 | self.assertEqual(instance.expiry, None) 1711 | self.assertEqual(instance.tradeID, None) 1712 | self.assertEqual(instance.putCall, None) 1713 | self.assertEqual(instance.reportDate, None) 1714 | self.assertEqual(instance.principalAdjustFactor, None) 1715 | self.assertEqual(instance.dateTime, None) 1716 | self.assertEqual(instance.tradeDate, None) 1717 | self.assertEqual(instance.settleDateTarget, None) 1718 | self.assertEqual(instance.transactionType, None) 1719 | self.assertEqual(instance.exchange, None) 1720 | self.assertEqual(instance.quantity, decimal.Decimal("123")) 1721 | self.assertEqual(instance.tradePrice, None) 1722 | self.assertEqual(instance.tradeMoney, None) 1723 | self.assertEqual(instance.orderID, None) 1724 | self.assertEqual(instance.execID, None) 1725 | self.assertEqual(instance.brokerageOrderID, None) 1726 | self.assertEqual(instance.orderReference, None) 1727 | self.assertEqual(instance.volatilityOrderLink, None) 1728 | self.assertEqual(instance.clearingFirmID, None) 1729 | self.assertEqual(instance.origTradePrice, None) 1730 | self.assertEqual(instance.origTradeDate, None) 1731 | self.assertEqual(instance.origTradeID, None) 1732 | # Despite the name, `orderTime` actually contains date/time data. 1733 | self.assertEqual(instance.orderTime, None) 1734 | self.assertEqual(instance.buySell, None) 1735 | self.assertEqual(instance.proceeds, decimal.Decimal("-123.456")) 1736 | self.assertEqual(instance.taxes, decimal.Decimal("-1.123")) 1737 | self.assertEqual(instance.ibCommission, decimal.Decimal("-1123.123")) 1738 | self.assertEqual(instance.ibCommissionCurrency, None) 1739 | self.assertEqual(instance.netCash, None) 1740 | self.assertEqual(instance.openCloseIndicator, None) 1741 | self.assertEqual(instance.notes, None) 1742 | self.assertEqual(instance.cost, None) 1743 | self.assertEqual(instance.fifoPnlRealized, None) 1744 | self.assertEqual(instance.fxPnl, None) 1745 | self.assertEqual(instance.mtmPnl, None) 1746 | self.assertEqual(instance.origTradePrice, None) 1747 | self.assertEqual(instance.origTradeDate, None) 1748 | self.assertEqual(instance.origTradeID, None) 1749 | self.assertEqual(instance.origOrderID, None) 1750 | self.assertEqual(instance.clearingFirmID, None) 1751 | self.assertEqual(instance.transactionID, None) 1752 | self.assertEqual(instance.buySell, None) 1753 | self.assertEqual(instance.ibOrderID, None) 1754 | self.assertEqual(instance.ibExecID, None) 1755 | self.assertEqual(instance.brokerageOrderID, None) 1756 | self.assertEqual(instance.orderReference, None) 1757 | self.assertEqual(instance.volatilityOrderLink, None) 1758 | self.assertEqual(instance.exchOrderId, None) 1759 | self.assertEqual(instance.extExecID, None) 1760 | self.assertEqual(instance.orderTime, None) 1761 | self.assertEqual(instance.openDateTime, None) 1762 | self.assertEqual(instance.holdingPeriodDateTime, None) 1763 | self.assertEqual(instance.whenRealized, None) 1764 | self.assertEqual(instance.whenReopened, None) 1765 | self.assertEqual(instance.levelOfDetail, "ASSET_SUMMARY") 1766 | self.assertEqual(instance.changeInPrice, None) 1767 | self.assertEqual(instance.changeInQuantity, None) 1768 | self.assertEqual(instance.orderType, None) 1769 | self.assertEqual(instance.traderID, None) 1770 | self.assertEqual(instance.isAPIOrder, None) 1771 | self.assertEqual(instance.accruedInt, None) 1772 | self.assertEqual(instance.serialNumber, None) 1773 | self.assertEqual(instance.deliveryType, None) 1774 | self.assertEqual(instance.commodityType, None) 1775 | self.assertEqual(instance.fineness, None) 1776 | self.assertEqual(instance.weight, None) 1777 | 1778 | class ChangeInNAVTestCase(unittest.TestCase): 1779 | 1780 | data = ET.fromstring( 1781 | ('')) 1790 | 1791 | def testParse(self): 1792 | instance = parser.parse_data_element(self.data) 1793 | self.assertIsInstance(instance, Types.ChangeInNAV) 1794 | self.assertEqual(instance.accountId, "myaccount") 1795 | self.assertEqual(instance.acctAlias, "myaccount") 1796 | self.assertEqual(instance.fromDate, datetime.date(2021, 2, 24)) 1797 | self.assertEqual(instance.toDate, datetime.date(2021, 2, 24)) 1798 | self.assertEqual(instance.startingValue, decimal.Decimal("234.567")) 1799 | self.assertEqual(instance.endingValue, decimal.Decimal("1234.56")) 1800 | self.assertEqual(instance.depositsWithdrawals, decimal.Decimal("0")) 1801 | self.assertEqual(instance.debitCardActivity, decimal.Decimal("0")) 1802 | self.assertEqual(instance.billPay, decimal.Decimal("0")) 1803 | self.assertEqual(instance.mtm, decimal.Decimal("11.11")) 1804 | self.assertEqual(instance.model, None) 1805 | self.assertEqual(instance.realized, decimal.Decimal("0")) 1806 | self.assertEqual(instance.changeInUnrealized, decimal.Decimal("0")) 1807 | self.assertEqual(instance.costAdjustments, decimal.Decimal("0")) 1808 | self.assertEqual(instance.transferredPnlAdjustments, decimal.Decimal("0")) 1809 | self.assertEqual(instance.internalCashTransfers, decimal.Decimal("0")) 1810 | self.assertEqual(instance.excessFundSweep, decimal.Decimal("0")) 1811 | self.assertEqual(instance.assetTransfers, decimal.Decimal("0")) 1812 | self.assertEqual(instance.grantActivity, decimal.Decimal("0")) 1813 | self.assertEqual(instance.dividends, decimal.Decimal("0")) 1814 | self.assertEqual(instance.withholdingTax, decimal.Decimal("0")) 1815 | self.assertEqual(instance.withholding871m, decimal.Decimal("0")) 1816 | self.assertEqual(instance.withholdingTaxCollected, decimal.Decimal("0")) 1817 | self.assertEqual(instance.changeInDividendAccruals, decimal.Decimal("0")) 1818 | self.assertEqual(instance.interest, decimal.Decimal("0")) 1819 | self.assertEqual(instance.changeInInterestAccruals, decimal.Decimal("0")) 1820 | self.assertEqual(instance.advisorFees, decimal.Decimal("0")) 1821 | self.assertEqual(instance.clientFees, decimal.Decimal("0")) 1822 | self.assertEqual(instance.otherFees, decimal.Decimal("0")) 1823 | self.assertEqual(instance.feesReceivables, decimal.Decimal("0")) 1824 | self.assertEqual(instance.commissions, decimal.Decimal("-7.5951887")) 1825 | self.assertEqual(instance.commissionCreditsRedemption, decimal.Decimal("0")) 1826 | self.assertEqual(instance.commissionReceivables, decimal.Decimal("0")) 1827 | self.assertEqual(instance.forexCommissions, decimal.Decimal("0")) 1828 | self.assertEqual(instance.transactionTax, decimal.Decimal("0")) 1829 | self.assertEqual(instance.taxReceivables, decimal.Decimal("0")) 1830 | self.assertEqual(instance.salesTax, decimal.Decimal("0")) 1831 | self.assertEqual(instance.billableSalesTax, decimal.Decimal("0")) 1832 | self.assertEqual(instance.softDollars, decimal.Decimal("0")) 1833 | self.assertEqual(instance.netFxTrading, decimal.Decimal("0")) 1834 | self.assertEqual(instance.fxTranslation, decimal.Decimal("0")) 1835 | self.assertEqual(instance.linkingAdjustments, decimal.Decimal("0")) 1836 | self.assertEqual(instance.other, decimal.Decimal("0")) 1837 | self.assertEqual(instance.twr, decimal.Decimal("0.30531605")) 1838 | 1839 | 1840 | class TradesOrderTestCase(unittest.TestCase): 1841 | """This example of Order comes from a flex report made by clicking Trades->Orders->Select All""" 1842 | 1843 | data = ET.fromstring( 1844 | ('')) 1857 | 1858 | def testParse(self): 1859 | instance = parser.parse_data_element(self.data) 1860 | self.assertIsInstance(instance, Types.Order) 1861 | 1862 | self.assertEqual(instance.buySell, enums.BuySell.BUY) 1863 | self.assertEqual(instance.quantity, decimal.Decimal("3")) 1864 | self.assertEqual(instance.netCash, decimal.Decimal("-876.9314")) 1865 | self.assertEqual(instance.dateTime, datetime.datetime(2021, 2, 3, 10, 1, 50)) 1866 | self.assertEqual(instance.tradePrice, decimal.Decimal("2.92")) 1867 | self.assertEqual(instance.acctAlias, "myaccount") 1868 | self.assertEqual(instance.assetCategory, enums.AssetClass.OPTION) 1869 | self.assertEqual(instance.description, "IWM 19MAR21 226.0 C") 1870 | self.assertEqual(instance.conid, "467957000") 1871 | self.assertEqual(instance.underlyingConid, "9579970") 1872 | self.assertEqual(instance.underlyingSymbol, "IWM") 1873 | self.assertEqual(instance.multiplier, decimal.Decimal("100")) 1874 | self.assertEqual(instance.strike, decimal.Decimal("226")) 1875 | self.assertEqual(instance.expiry, datetime.date(2021, 3, 19)) 1876 | self.assertEqual(instance.putCall, enums.PutCall.CALL) 1877 | self.assertEqual(instance.ibCommission, decimal.Decimal("-0.9314")) 1878 | self.assertEqual(instance.ibOrderID, "1722040385") 1879 | self.assertEqual(instance.accountId, "myaccount") 1880 | self.assertEqual(instance.model, "Independent") 1881 | self.assertEqual(instance.currency, "USD") 1882 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1883 | self.assertEqual(instance.symbol, "IWM 210319C00226000") 1884 | self.assertEqual(instance.securityID, None) 1885 | self.assertEqual(instance.securityIDType, None) 1886 | self.assertEqual(instance.cusip, None) 1887 | self.assertEqual(instance.isin, None) 1888 | self.assertEqual(instance.listingExchange, "CBOE") 1889 | self.assertEqual(instance.underlyingSecurityID, "US4642876555") 1890 | self.assertEqual(instance.underlyingListingExchange, "ARCA") 1891 | self.assertEqual(instance.issuer, None) 1892 | self.assertEqual(instance.tradeID, None) 1893 | self.assertEqual(instance.reportDate, datetime.date(2021, 2, 3)) 1894 | self.assertEqual(instance.principalAdjustFactor, None) 1895 | self.assertEqual(instance.tradeDate, datetime.date(2021, 2, 3)) 1896 | self.assertEqual(instance.settleDateTarget, datetime.date(2021, 2, 4)) 1897 | self.assertEqual(instance.transactionType, None) 1898 | self.assertEqual(instance.exchange, None) 1899 | self.assertEqual(instance.tradeMoney, decimal.Decimal("876")) 1900 | self.assertEqual(instance.proceeds, decimal.Decimal("-876")) 1901 | self.assertEqual(instance.taxes, decimal.Decimal("0")) 1902 | self.assertEqual(instance.ibCommissionCurrency, "USD") 1903 | self.assertEqual(instance.closePrice, decimal.Decimal("3.08")) 1904 | self.assertEqual(instance.openCloseIndicator, enums.OpenClose.UNKNOWN) 1905 | self.assertEqual(instance.notes, "P") 1906 | self.assertEqual(instance.cost, decimal.Decimal("876.9314")) 1907 | self.assertEqual(instance.fifoPnlRealized, decimal.Decimal("0")) 1908 | self.assertEqual(instance.fxPnl, decimal.Decimal("0")) 1909 | self.assertEqual(instance.mtmPnl, decimal.Decimal("48")) 1910 | self.assertEqual(instance.origTradePrice, None) 1911 | self.assertEqual(instance.origTradeDate, None) 1912 | self.assertEqual(instance.origTradeID, None) 1913 | self.assertEqual(instance.origOrderID, None) 1914 | self.assertEqual(instance.clearingFirmID, None) 1915 | self.assertEqual(instance.transactionID, None) 1916 | self.assertEqual(instance.ibExecID, None) 1917 | self.assertEqual(instance.brokerageOrderID, None) 1918 | self.assertEqual(instance.orderReference, None) 1919 | self.assertEqual(instance.volatilityOrderLink, None) 1920 | self.assertEqual(instance.exchOrderId, None) 1921 | self.assertEqual(instance.extExecID, None) 1922 | self.assertEqual(instance.orderTime, datetime.datetime(2021, 2, 3, 10, 1, 50)) 1923 | self.assertEqual(instance.openDateTime, None) 1924 | self.assertEqual(instance.holdingPeriodDateTime, None) 1925 | self.assertEqual(instance.whenRealized, None) 1926 | self.assertEqual(instance.whenReopened, None) 1927 | self.assertEqual(instance.levelOfDetail, "ORDER") 1928 | self.assertEqual(instance.changeInPrice, None) 1929 | self.assertEqual(instance.changeInQuantity, None) 1930 | self.assertEqual(instance.orderType, enums.OrderType.MULTIPLE) 1931 | self.assertEqual(instance.traderID, None) 1932 | self.assertEqual(instance.isAPIOrder, None) 1933 | self.assertEqual(instance.accruedInt, decimal.Decimal("0")) 1934 | 1935 | class OptionEAEBuyTestCase(unittest.TestCase): 1936 | data = ET.fromstring( 1937 | ('') 1948 | ) 1949 | 1950 | def testParse(self): 1951 | instance = parser.parse_data_element(self.data) 1952 | self.assertIsInstance(instance, Types.OptionEAE) 1953 | self.assertEqual(instance.accountId, "U123456") 1954 | self.assertEqual(instance.acctAlias, "ibflex testing") 1955 | self.assertEqual(instance.model, None) 1956 | self.assertEqual(instance.currency, "USD") 1957 | self.assertEqual(instance.fxRateToBase, decimal.Decimal("1")) 1958 | self.assertEqual(instance.assetCategory, enums.AssetClass.STOCK) 1959 | self.assertEqual(instance.symbol, "PSTH") 1960 | self.assertEqual(instance.description, "PERSHING SQUARE TONTINE -A") 1961 | self.assertEqual(instance.conid, "91900358") 1962 | self.assertEqual(instance.securityID, None) 1963 | self.assertEqual(instance.securityIDType, None) 1964 | self.assertEqual(instance.cusip, None) 1965 | self.assertEqual(instance.isin, None) 1966 | self.assertEqual(instance.underlyingConid, "80789235") 1967 | self.assertEqual(instance.underlyingSymbol, "PSTH") 1968 | self.assertEqual(instance.issuer, None) 1969 | self.assertEqual(instance.multiplier, None) 1970 | self.assertEqual(instance.strike, None) 1971 | self.assertEqual(instance.expiry, None) 1972 | self.assertEqual(instance.putCall, None) 1973 | self.assertEqual(instance.principalAdjustFactor, None) 1974 | self.assertEqual(instance.date, datetime.date(2011, 8, 5)) 1975 | self.assertEqual(instance.transactionType, enums.OptionAction.BUY) 1976 | self.assertEqual(instance.quantity, decimal.Decimal("100")) 1977 | self.assertEqual(instance.tradePrice, decimal.Decimal("25.0000")) 1978 | self.assertEqual(instance.markPrice, decimal.Decimal("0.0000")) 1979 | self.assertEqual(instance.proceeds, decimal.Decimal("-2500.00")) 1980 | self.assertEqual(instance.commisionsAndTax, decimal.Decimal("0.00")) 1981 | self.assertEqual(instance.costBasis, decimal.Decimal("2500.00")) 1982 | self.assertEqual(instance.realizedPnl, decimal.Decimal("0.00")) 1983 | self.assertEqual(instance.fxPnl, decimal.Decimal("0.00")) 1984 | self.assertEqual(instance.mtmPnl, decimal.Decimal("-118.00")) 1985 | self.assertEqual(instance.tradeID, None) 1986 | 1987 | 1988 | if __name__ == '__main__': 1989 | unittest.main(verbosity=3) 1990 | --------------------------------------------------------------------------------