├── ib_async ├── py.typed ├── version.py ├── connection.py ├── __init__.py ├── flexreport.py ├── ibcontroller.py ├── objects.py ├── ticker.py ├── util.py └── order.py ├── .github ├── FUNDING.yml └── workflows │ ├── test.yml │ └── docs.yml ├── .gitattributes ├── docs ├── readme.rst ├── images │ └── qt-tickertable.png ├── code.rst ├── index.rst ├── links.rst ├── api.rst ├── notebooks.rst ├── conf.py └── recipes.rst ├── .gitignore ├── tests ├── conftest.py ├── test_requests.py └── test_contract.py ├── examples ├── tk.py └── qt_ticker_table.py ├── LICENSE ├── pyproject.toml ├── notebooks ├── market_depth.ipynb ├── tick_data.ipynb ├── basics.ipynb ├── ordering.ipynb └── option_chain.ipynb └── README.md /ib_async/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | github: mattsta 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | notebooks/* linguist-documentation 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.md 2 | :parser: myst_parser.sphinx_ 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/images/qt-tickertable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ib-api-reloaded/ib_async/HEAD/docs/images/qt-tickertable.png -------------------------------------------------------------------------------- /docs/code.rst: -------------------------------------------------------------------------------- 1 | .. _code: 2 | 3 | Source code 4 | =========== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | Download 10 | Issue Tracker 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ib_async/__pycache__ 2 | dist 3 | build 4 | .vscode 5 | .idea 6 | .settings 7 | .spyproject 8 | .project 9 | .pydevproject 10 | .mypy_cache 11 | .eggs 12 | html 13 | doctrees 14 | ib_async.egg-info 15 | poetry.lock 16 | *.csv 17 | *.json 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | readme 8 | api 9 | notebooks 10 | recipes 11 | code 12 | changelog 13 | links 14 | 15 | .. include:: ../README.md 16 | :parser: myst_parser.sphinx_ 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import ib_async as ibi 4 | 5 | 6 | @pytest.fixture(scope="session") 7 | def event_loop(): 8 | loop = ibi.util.getLoop() 9 | yield loop 10 | loop.close() 11 | 12 | 13 | @pytest.fixture(scope="session") 14 | async def ib(): 15 | ib = ibi.IB() 16 | await ib.connectAsync() 17 | yield ib 18 | ib.disconnect() 19 | -------------------------------------------------------------------------------- /ib_async/version.py: -------------------------------------------------------------------------------- 1 | """Version info.""" 2 | 3 | from importlib.metadata import version 4 | 5 | __version__ = version("ib_async") 6 | 7 | # historically, ib_insync has provided __version_info__ as a 3-tuple of integers, 8 | # so we shouldn't use non-integer version components like "1.3b1" etc or else 9 | # anybody consuming __version_info__ may receive values outside of ranges they expect. 10 | __version_info__ = tuple([int(x) for x in __version__.split(".")]) 11 | -------------------------------------------------------------------------------- /docs/links.rst: -------------------------------------------------------------------------------- 1 | .. _links: 2 | 3 | Links 4 | ===== 5 | 6 | * `Interactive Brokers `_ 7 | * `Interactive Brokers Python API `_ 8 | * `TWSAPI documentation `_ 9 | * `TWSAPI user goup `_ 10 | * `Dmitry's TWS API FAQ `_ 11 | * `IBC `_ for hands-free operation of TWS or gateway 12 | -------------------------------------------------------------------------------- /tests/test_requests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import ib_async as ibi 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | async def test_request_error_raised(ib): 9 | contract = ibi.Forex("EURUSD") 10 | order = ibi.MarketOrder("BUY", 100) 11 | orderState = await ib.whatIfOrderAsync(contract, order) 12 | assert orderState.commission > 0 13 | 14 | ib.RaiseRequestErrors = True 15 | badContract = ibi.Stock("XXX") 16 | with pytest.raises(ibi.RequestError) as exc_info: 17 | await ib.whatIfOrderAsync(badContract, order) 18 | assert exc_info.value.code == 321 19 | 20 | 21 | async def test_account_summary(ib): 22 | summary = await ib.accountSummaryAsync() 23 | assert summary 24 | assert all(isinstance(value, ibi.AccountValue) for value in summary) 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: ib_async 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | # https://github.com/actions/runner-images 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10", "pypy3.11" ] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: Install dependencies of dependencies 22 | run: | 23 | pip install pip poetry uv setuptools wheel -U 24 | 25 | - name: Install dependencies 26 | run: | 27 | poetry install --with=dev 28 | 29 | - name: MyPy static code analysis 30 | run: | 31 | poetry run mypy --pretty ib_async 32 | 33 | - name: Ruff check 34 | run: | 35 | poetry run ruff check 36 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Adapted from https://tomasfarias.dev/articles/sphinx-docs-with-poetry-and-github-pages/ 2 | name: docs 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build-docs: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - uses: actions/setup-python@v5 23 | with: 24 | python-version: 3.12 25 | - uses: abatilo/actions-poetry@v2 26 | - name: install 27 | run: poetry install --with=docs 28 | - name: Build documentation 29 | run: | 30 | mkdir html 31 | touch html/.nojekyll 32 | poetry run sphinx-build -b html docs html 33 | - name: Deploy documentation 34 | if: ${{ github.event_name == 'push' }} 35 | uses: JamesIves/github-pages-deploy-action@v4 36 | with: 37 | branch: gh-pages 38 | folder: html 39 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API docs 4 | ================= 5 | 6 | Release |release|. 7 | 8 | .. toctree:: 9 | :maxdepth: 3 10 | :caption: Modules: 11 | 12 | Also see the official 13 | `Python API documentation `_ 14 | from IB. 15 | 16 | 17 | IB 18 | -- 19 | 20 | .. automodule:: ib_async.ib 21 | 22 | Client 23 | ------ 24 | 25 | .. automodule:: ib_async.client 26 | 27 | Order 28 | ----- 29 | 30 | .. automodule:: ib_async.order 31 | 32 | Contract 33 | -------- 34 | 35 | .. automodule:: ib_async.contract 36 | 37 | Ticker 38 | -------- 39 | 40 | .. automodule:: ib_async.ticker 41 | 42 | Objects 43 | ------- 44 | 45 | .. automodule:: ib_async.objects 46 | 47 | .. autoclass:: ib_async.wrapper.RequestError 48 | 49 | Utilities 50 | --------- 51 | 52 | .. automodule:: ib_async.util 53 | 54 | FlexReport 55 | ---------- 56 | 57 | .. automodule:: ib_async.flexreport 58 | 59 | IBC 60 | --- 61 | .. autoclass:: ib_async.ibcontroller.IBC 62 | 63 | Watchdog 64 | -------- 65 | .. autoclass:: ib_async.ibcontroller.Watchdog 66 | 67 | -------------------------------------------------------------------------------- /docs/notebooks.rst: -------------------------------------------------------------------------------- 1 | .. _notebooks: 2 | 3 | Notebooks 4 | ========= 5 | 6 | IB-insync can be used in a fully interactive, exploratory way with live data from 7 | within a `Jupyter `_ notebook. 8 | Here are some recipe notebooks: 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | Basics 14 | Contract details 15 | Option chain 16 | Bar data 17 | Tick data 18 | Market depth 19 | Ordering 20 | Scanners 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/tk.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | from ib_async import IB, util 4 | from ib_async.contract import * # noqa 5 | 6 | util.patchAsyncio() 7 | 8 | 9 | class TkApp: 10 | """ 11 | Example of integrating with Tkinter. 12 | """ 13 | 14 | def __init__(self): 15 | self.ib = IB().connect() 16 | self.root = tk.Tk() 17 | self.root.protocol("WM_DELETE_WINDOW", self._onDeleteWindow) 18 | self.entry = tk.Entry(self.root, width=50) 19 | self.entry.insert(0, "Stock('TSLA', 'SMART', 'USD')") 20 | self.entry.grid() 21 | self.button = tk.Button( 22 | self.root, text="Get details", command=self.onButtonClick 23 | ) 24 | self.button.grid() 25 | self.text = tk.Text(self.root) 26 | self.text.grid() 27 | self.loop = util.getLoop() 28 | 29 | def onButtonClick(self): 30 | contract = eval(self.entry.get()) 31 | cds = self.ib.reqContractDetails(contract) 32 | self.text.delete(1.0, tk.END) 33 | self.text.insert(tk.END, str(cds)) 34 | 35 | def run(self): 36 | self._onTimeout() 37 | self.loop.run_forever() 38 | 39 | def _onTimeout(self): 40 | self.root.update() 41 | self.loop.call_later(0.03, self._onTimeout) 42 | 43 | def _onDeleteWindow(self): 44 | self.loop.stop() 45 | 46 | 47 | app = TkApp() 48 | app.run() 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019 - 2023, Ewald de Wit 4 | Copyright (c) 2024, Matt Stancliff 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /tests/test_contract.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | import ib_async 4 | from ib_async import Stock, util 5 | 6 | 7 | def test_contract_format_data_pd(): 8 | """Simple smoketest to verify everything still works minimally.""" 9 | ib = ib_async.IB() 10 | ib.connect("127.0.0.1", 4001, clientId=90, readonly=True) 11 | 12 | symbols = ["AMZN", "TSLA"] 13 | 14 | # Method to get OHLCV 15 | def get_OHLCV( 16 | symbol, 17 | endDateTime="", 18 | durationStr="1 D", 19 | barSizeSetting="1 hour", 20 | whatToShow="TRADES", 21 | useRTH=False, 22 | formatDate=1, 23 | ): 24 | bars = ib.reqHistoricalData( 25 | symbol, 26 | endDateTime, 27 | durationStr, 28 | barSizeSetting, 29 | whatToShow, 30 | useRTH, 31 | formatDate, 32 | ) 33 | df = util.df(bars) 34 | df["date"] = df["date"].dt.tz_convert("America/New_York") 35 | df = df.drop(columns=["average", "barCount"]) 36 | # df.set_index("date", inplace=True) 37 | 38 | print("\n", df) 39 | 40 | # df = df.iloc[::-1] 41 | # df.to_csv("{}.csv".format(symbol.symbol)) 42 | # df = pd.read_csv("{}.csv".format(symbol.symbol)) 43 | df.columns = ["date", "open", "high", "low", "close", "volume"] 44 | 45 | df["date"] = pd.to_datetime(df["date"]) 46 | df.set_index("date", inplace=True) 47 | 48 | print(f"Data for {symbol.symbol} downloaded OK with OLD") 49 | return df 50 | 51 | for symbol_str in symbols: 52 | symbol = Stock(symbol_str, "SMART", "USD") 53 | get_OHLCV(symbol) 54 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | extensions = [ 2 | "sphinx.ext.autodoc", 3 | "sphinx.ext.viewcode", 4 | "sphinx.ext.napoleon", 5 | "sphinx_autodoc_typehints", 6 | "sphinx.ext.extlinks", 7 | "sphinx.ext.intersphinx", 8 | "myst_parser", 9 | ] 10 | 11 | templates_path = ["_templates"] 12 | source_suffix = { 13 | ".rst": "restructuredtext", 14 | ".txt": "markdown", 15 | ".md": "markdown", 16 | } 17 | suppress_warnings = ["myst.xref_missing", "myst.iref_ambiguous"] 18 | master_doc = "index" 19 | project = "ib_async" 20 | html_show_copyright = False 21 | author = "Originally Ewald de Wit; Currently Matt Stancliff" 22 | 23 | __version__ = "" 24 | exec(open("../ib_async/version.py").read()) 25 | version = ".".join(__version__.split(".")[:2]) 26 | release = __version__ 27 | 28 | language = "en" 29 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 30 | pygments_style = "sphinx" 31 | todo_include_todos = False 32 | html_theme = "sphinx_rtd_theme" 33 | html_theme_options = { 34 | "canonical_url": "https://ib-api-reloaded.github.io/ib_async", 35 | "logo_only": False, 36 | "display_version": True, 37 | "prev_next_buttons_location": "bottom", 38 | "style_external_links": False, 39 | # Toc options 40 | "collapse_navigation": True, 41 | "sticky_navigation": True, 42 | "navigation_depth": 4, 43 | "includehidden": True, 44 | "titles_only": False, 45 | } 46 | 47 | intersphinx_mapping = { 48 | "python": ("https://docs.python.org/3", None), 49 | "eventkit": ("https://eventkit.readthedocs.io/en/latest", None), 50 | } 51 | 52 | github_url = "https://github.com/ib-api-reloaded/ib_async" 53 | 54 | extlinks = { 55 | "issue": ("https://github.com/ib-api-reloaded/ib_async/issues/%s", "issue %s"), 56 | "pull": ("https://github.com/ib-api-reloaded/ib_async/pull/%s", "pull %s"), 57 | } 58 | 59 | autoclass_content = "both" 60 | autodoc_member_order = "bysource" 61 | autodoc_default_options = {"members": True, "undoc-members": True} 62 | 63 | 64 | def onDocstring(app, what, name, obj, options, lines): 65 | if not lines: 66 | return 67 | if lines[0].startswith("Alias for field number"): 68 | # strip useless namedtuple number fields 69 | del lines[:] 70 | 71 | 72 | def setup(app): 73 | (app.connect("autodoc-process-docstring", onDocstring),) 74 | -------------------------------------------------------------------------------- /ib_async/connection.py: -------------------------------------------------------------------------------- 1 | """Event-driven socket connection.""" 2 | 3 | import asyncio 4 | 5 | from eventkit import Event 6 | 7 | 8 | class Connection(asyncio.Protocol): 9 | """ 10 | Event-driven socket connection. 11 | 12 | Events: 13 | * ``hasData`` (data: bytes): 14 | Emits the received socket data. 15 | * ``disconnected`` (msg: str): 16 | Is emitted on socket disconnect, with an error message in case 17 | of error, or an empty string in case of a normal disconnect. 18 | """ 19 | 20 | def __init__(self): 21 | self.hasData = Event("hasData") 22 | self.disconnected = Event("disconnected") 23 | self.reset() 24 | 25 | def reset(self): 26 | self.transport = None 27 | self.numBytesSent = 0 28 | self.numMsgSent = 0 29 | 30 | async def connectAsync(self, host, port): 31 | if self.transport: 32 | # wait until a previous connection is finished closing 33 | self.disconnect() 34 | await self.disconnected 35 | self.reset() 36 | # Use get_running_loop() directly for optimal performance in async context 37 | loop = asyncio.get_running_loop() 38 | self.transport, _ = await loop.create_connection(lambda: self, host, port) 39 | 40 | def disconnect(self): 41 | if self.transport: 42 | self.transport.write_eof() 43 | try: 44 | # sometimes the loop is already closed, but we don't 45 | # want the user to get a runtime exception from deep in 46 | # this library, so we just hide any failure to double close 47 | # the event loop. 48 | self.transport.close() 49 | except RuntimeError: 50 | # prevents: raise RuntimeError('Event loop is closed') 51 | pass 52 | 53 | def isConnected(self): 54 | return self.transport is not None 55 | 56 | def sendMsg(self, msg): 57 | if self.transport: 58 | self.transport.write(msg) 59 | self.numBytesSent += len(msg) 60 | self.numMsgSent += 1 61 | 62 | def connection_lost(self, exc): 63 | self.transport = None 64 | msg = str(exc) if exc else "" 65 | self.disconnected.emit(msg) 66 | 67 | def data_received(self, data): 68 | self.hasData.emit(data) 69 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ib_async" 3 | requires-python = ">=3.10" 4 | version = "2.1.0" 5 | license = "BSD" 6 | license-files = ["LICENSE"] 7 | 8 | [tool.poetry] 9 | name = "ib_async" 10 | version = "2.1.0" 11 | description = "Python sync/async framework for Interactive Brokers API" 12 | authors = ["Ewald de Wit"] 13 | maintainers = ["Matt Stancliff "] 14 | license = "BSD" 15 | readme = "README.md" 16 | repository = "https://github.com/ib-api-reloaded/ib_async" 17 | include = ["ib_async/py.typed"] 18 | classifiers = [ 19 | "Development Status :: 5 - Production/Stable", 20 | "Intended Audience :: Developers", 21 | "Topic :: Office/Business :: Financial :: Investment", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Programming Language :: Python :: 3.13", 26 | "Programming Language :: Python :: 3 :: Only", 27 | ] 28 | keywords = ["ibapi", "tws", "asyncio", "jupyter", "interactive", "brokers", "async", "ib_async", "ib_insync"] 29 | 30 | [tool.poetry.dependencies] 31 | python = ">=3.10" 32 | aeventkit = "^2.1.0" 33 | # aeventkit = { path = "../eventkit", develop = true } 34 | nest_asyncio = "*" 35 | tzdata = "^2025.2" 36 | 37 | [tool.poetry.urls] 38 | "Bug Tracker" = "https://github.com/ib-api-reloaded/ib_async/issues" 39 | 40 | 41 | [tool.poetry.group.dev] 42 | optional = true 43 | 44 | [tool.poetry.group.dev.dependencies] 45 | mypy = ">=1.11.0" 46 | pytest = ">=8.0" 47 | pytest-asyncio = ">=0.23" 48 | pandas = "^2.2.1" 49 | ruff = "^0.11.13" 50 | 51 | 52 | [tool.poetry.group.docs] 53 | optional = true 54 | 55 | [tool.poetry.group.docs.dependencies] 56 | sphinx-autodoc-typehints = "^2.0.0" 57 | sphinx-rtd-theme = "^2.0.0" 58 | myst-parser = "^2.0.0" 59 | 60 | 61 | [tool.mypy] 62 | ignore_missing_imports = true 63 | check_untyped_defs = true 64 | 65 | 66 | [tool.pytest.ini_options] 67 | asyncio_mode="auto" 68 | 69 | 70 | [build-system] 71 | requires = ["poetry-core"] 72 | build-backend = "poetry.core.masonry.api" 73 | 74 | [tool.ruff] 75 | target-version = "py310" 76 | exclude = [ 77 | "notebooks/", 78 | "upstream_api_architecture/", 79 | "examples/", 80 | ] 81 | 82 | [tool.ruff.lint] 83 | extend-select = [ 84 | # convert legacy python syntax to modern syntax 85 | "UP", 86 | # isort imports 87 | "I", 88 | ] 89 | ignore = ["E402"] # Ignore Module level import not at top of file 90 | -------------------------------------------------------------------------------- /examples/qt_ticker_table.py: -------------------------------------------------------------------------------- 1 | import PyQt5.QtWidgets as qt 2 | 3 | # import PySide6.QtWidgets as qt 4 | from ib_async import IB, util 5 | from ib_async.contract import * # noqa 6 | 7 | 8 | class TickerTable(qt.QTableWidget): 9 | headers = [ 10 | "symbol", 11 | "bidSize", 12 | "bid", 13 | "ask", 14 | "askSize", 15 | "last", 16 | "lastSize", 17 | "close", 18 | ] 19 | 20 | def __init__(self, parent=None): 21 | qt.QTableWidget.__init__(self, parent) 22 | self.conId2Row = {} 23 | self.setColumnCount(len(self.headers)) 24 | self.setHorizontalHeaderLabels(self.headers) 25 | self.setAlternatingRowColors(True) 26 | 27 | def __contains__(self, contract): 28 | assert contract.conId 29 | return contract.conId in self.conId2Row 30 | 31 | def addTicker(self, ticker): 32 | row = self.rowCount() 33 | self.insertRow(row) 34 | self.conId2Row[ticker.contract.conId] = row 35 | for col in range(len(self.headers)): 36 | item = qt.QTableWidgetItem("-") 37 | self.setItem(row, col, item) 38 | item = self.item(row, 0) 39 | item.setText( 40 | ticker.contract.symbol 41 | + (ticker.contract.currency if ticker.contract.secType == "CASH" else "") 42 | ) 43 | self.resizeColumnsToContents() 44 | 45 | def clearTickers(self): 46 | self.setRowCount(0) 47 | self.conId2Row.clear() 48 | 49 | def onPendingTickers(self, tickers): 50 | for ticker in tickers: 51 | row = self.conId2Row[ticker.contract.conId] 52 | for col, header in enumerate(self.headers): 53 | if col == 0: 54 | continue 55 | item = self.item(row, col) 56 | val = getattr(ticker, header) 57 | item.setText(str(val)) 58 | 59 | 60 | class Window(qt.QWidget): 61 | def __init__(self, host, port, clientId): 62 | qt.QWidget.__init__(self) 63 | self.edit = qt.QLineEdit("", self) 64 | self.edit.editingFinished.connect(self.add) 65 | self.table = TickerTable() 66 | self.connectButton = qt.QPushButton("Connect") 67 | self.connectButton.clicked.connect(self.onConnectButtonClicked) 68 | layout = qt.QVBoxLayout(self) 69 | layout.addWidget(self.edit) 70 | layout.addWidget(self.table) 71 | layout.addWidget(self.connectButton) 72 | 73 | self.connectInfo = (host, port, clientId) 74 | self.ib = IB() 75 | self.ib.pendingTickersEvent += self.table.onPendingTickers 76 | 77 | def add(self, text=""): 78 | text = text or self.edit.text() 79 | if text: 80 | contract = eval(text) 81 | if ( 82 | contract 83 | and self.ib.qualifyContracts(contract) 84 | and contract not in self.table 85 | ): 86 | ticker = self.ib.reqMktData(contract, "", False, False) 87 | self.table.addTicker(ticker) 88 | self.edit.setText(text) 89 | 90 | def onConnectButtonClicked(self, _): 91 | if self.ib.isConnected(): 92 | self.ib.disconnect() 93 | self.table.clearTickers() 94 | self.connectButton.setText("Connect") 95 | else: 96 | self.ib.connect(*self.connectInfo) 97 | self.ib.reqMarketDataType(2) 98 | self.connectButton.setText("Disonnect") 99 | for symbol in ( 100 | "EURUSD", 101 | "USDJPY", 102 | "EURGBP", 103 | "USDCAD", 104 | "EURCHF", 105 | "AUDUSD", 106 | "NZDUSD", 107 | ): 108 | self.add(f"Forex('{symbol}')") 109 | self.add("Stock('TSLA', 'SMART', 'USD')") 110 | 111 | def closeEvent(self, ev): 112 | loop = util.getLoop() 113 | loop.stop() 114 | 115 | 116 | if __name__ == "__main__": 117 | util.patchAsyncio() 118 | util.useQt() 119 | # util.useQt('PySide6') 120 | window = Window("127.0.0.1", 7497, 1) 121 | window.resize(600, 400) 122 | window.show() 123 | IB.run() 124 | -------------------------------------------------------------------------------- /ib_async/__init__.py: -------------------------------------------------------------------------------- 1 | """Python sync/async framework for Interactive Brokers API""" 2 | 3 | import dataclasses 4 | import sys 5 | 6 | from eventkit import Event 7 | 8 | from . import util 9 | from .client import Client 10 | from .contract import ( 11 | CFD, 12 | Bag, 13 | Bond, 14 | ComboLeg, 15 | Commodity, 16 | ContFuture, 17 | Contract, 18 | ContractDescription, 19 | ContractDetails, 20 | Crypto, 21 | DeltaNeutralContract, 22 | Forex, 23 | Future, 24 | FuturesOption, 25 | Index, 26 | MutualFund, 27 | Option, 28 | ScanData, 29 | Stock, 30 | TagValue, 31 | Warrant, 32 | ) 33 | from .flexreport import FlexError, FlexReport 34 | from .ib import IB, StartupFetch, StartupFetchALL, StartupFetchNONE 35 | from .ibcontroller import IBC, Watchdog 36 | from .objects import ( 37 | AccountValue, 38 | BarData, 39 | BarDataList, 40 | CommissionReport, 41 | ConnectionStats, 42 | DepthMktDataDescription, 43 | Dividends, 44 | DOMLevel, 45 | Execution, 46 | ExecutionFilter, 47 | FamilyCode, 48 | Fill, 49 | FundamentalRatios, 50 | HistogramData, 51 | HistoricalNews, 52 | HistoricalSchedule, 53 | HistoricalSession, 54 | HistoricalTick, 55 | HistoricalTickBidAsk, 56 | HistoricalTickLast, 57 | IBDefaults, 58 | MktDepthData, 59 | NewsArticle, 60 | NewsBulletin, 61 | NewsProvider, 62 | NewsTick, 63 | OptionChain, 64 | OptionComputation, 65 | PnL, 66 | PnLSingle, 67 | PortfolioItem, 68 | Position, 69 | PriceIncrement, 70 | RealTimeBar, 71 | RealTimeBarList, 72 | ScanDataList, 73 | ScannerSubscription, 74 | SmartComponent, 75 | SoftDollarTier, 76 | TickAttrib, 77 | TickAttribBidAsk, 78 | TickAttribLast, 79 | TickByTickAllLast, 80 | TickByTickBidAsk, 81 | TickByTickMidPoint, 82 | TickData, 83 | TradeLogEntry, 84 | WshEventData, 85 | ) 86 | from .order import ( 87 | BracketOrder, 88 | ExecutionCondition, 89 | LimitOrder, 90 | MarginCondition, 91 | MarketOrder, 92 | Order, 93 | OrderComboLeg, 94 | OrderCondition, 95 | OrderState, 96 | OrderStateNumeric, 97 | OrderStatus, 98 | PercentChangeCondition, 99 | PriceCondition, 100 | StopLimitOrder, 101 | StopOrder, 102 | TimeCondition, 103 | Trade, 104 | VolumeCondition, 105 | ) 106 | from .ticker import Ticker 107 | from .version import __version__, __version_info__ 108 | from .wrapper import RequestError, Wrapper 109 | 110 | __all__ = [ 111 | "Event", 112 | "util", 113 | "Client", 114 | "Bag", 115 | "Bond", 116 | "CFD", 117 | "ComboLeg", 118 | "Commodity", 119 | "ContFuture", 120 | "Contract", 121 | "ContractDescription", 122 | "ContractDetails", 123 | "Crypto", 124 | "DeltaNeutralContract", 125 | "Forex", 126 | "Future", 127 | "FuturesOption", 128 | "Index", 129 | "MutualFund", 130 | "Option", 131 | "ScanData", 132 | "Stock", 133 | "TagValue", 134 | "Warrant", 135 | "FlexError", 136 | "FlexReport", 137 | "IB", 138 | "IBDefaults", 139 | "OrderStateNumeric", 140 | "IBC", 141 | "Watchdog", 142 | "AccountValue", 143 | "BarData", 144 | "BarDataList", 145 | "CommissionReport", 146 | "ConnectionStats", 147 | "DOMLevel", 148 | "DepthMktDataDescription", 149 | "Dividends", 150 | "Execution", 151 | "ExecutionFilter", 152 | "FamilyCode", 153 | "Fill", 154 | "FundamentalRatios", 155 | "HistogramData", 156 | "HistoricalNews", 157 | "HistoricalTick", 158 | "HistoricalTickBidAsk", 159 | "HistoricalTickLast", 160 | "HistoricalSchedule", 161 | "HistoricalSession", 162 | "MktDepthData", 163 | "NewsArticle", 164 | "NewsBulletin", 165 | "NewsProvider", 166 | "NewsTick", 167 | "OptionChain", 168 | "OptionComputation", 169 | "PnL", 170 | "PnLSingle", 171 | "PortfolioItem", 172 | "Position", 173 | "PriceIncrement", 174 | "RealTimeBar", 175 | "RealTimeBarList", 176 | "ScanDataList", 177 | "ScannerSubscription", 178 | "SmartComponent", 179 | "SoftDollarTier", 180 | "TickAttrib", 181 | "TickAttribBidAsk", 182 | "TickAttribLast", 183 | "TickByTickAllLast", 184 | "WshEventData", 185 | "TickByTickBidAsk", 186 | "TickByTickMidPoint", 187 | "TickData", 188 | "TradeLogEntry", 189 | "BracketOrder", 190 | "ExecutionCondition", 191 | "LimitOrder", 192 | "MarginCondition", 193 | "MarketOrder", 194 | "Order", 195 | "OrderComboLeg", 196 | "OrderCondition", 197 | "OrderState", 198 | "OrderStatus", 199 | "PercentChangeCondition", 200 | "PriceCondition", 201 | "StopLimitOrder", 202 | "StopOrder", 203 | "TimeCondition", 204 | "Trade", 205 | "VolumeCondition", 206 | "Ticker", 207 | "__version__", 208 | "__version_info__", 209 | "RequestError", 210 | "Wrapper", 211 | "StartupFetch", 212 | "StartupFetchALL", 213 | "StartupFetchNONE", 214 | ] 215 | 216 | 217 | # compatibility with old Object 218 | for obj in locals().copy().values(): 219 | if dataclasses.is_dataclass(obj): 220 | obj.dict = util.dataclassAsDict # type: ignore 221 | obj.tuple = util.dataclassAsTuple # type: ignore 222 | obj.update = util.dataclassUpdate # type: ignore 223 | obj.nonDefaults = util.dataclassNonDefaults # type: ignore 224 | 225 | del sys 226 | del dataclasses 227 | -------------------------------------------------------------------------------- /ib_async/flexreport.py: -------------------------------------------------------------------------------- 1 | """Access to account statement webservice.""" 2 | 3 | import logging 4 | import os 5 | import time 6 | import xml.etree.ElementTree as et 7 | from contextlib import suppress 8 | from typing import Final 9 | from urllib.parse import urlparse 10 | from urllib.request import urlopen 11 | 12 | from ib_async import util 13 | from ib_async.objects import DynamicObject 14 | 15 | _logger = logging.getLogger("ib_async.flexreport") 16 | 17 | FLEXREPORT_URL: Final = ( 18 | "https://ndcdyn.interactivebrokers.com/AccountManagement/" 19 | "FlexWebService/SendRequest?" 20 | ) 21 | """ 22 | https://www.interactivebrokers.com/campus/ibkr-api-page/flex-web-service/#flex-generate-report 23 | """ 24 | 25 | 26 | class FlexError(Exception): 27 | pass 28 | 29 | 30 | class FlexReport: 31 | """ 32 | To obtain a token: 33 | 34 | * Login to web portal 35 | * Go to Settings 36 | * Click on "Configure Flex Web Service" 37 | * Generate token 38 | """ 39 | 40 | data: bytes 41 | root: et.Element 42 | 43 | def __init__(self, token=None, queryId=None, path=None): 44 | """ 45 | Download a report by giving a valid ``token`` and ``queryId``, 46 | or load from file by giving a valid ``path``. 47 | 48 | To overwrite default URL, set env variable ``IB_FLEXREPORT_URL``. 49 | """ 50 | if token and queryId: 51 | self.download(token, queryId) 52 | elif path: 53 | self.load(path) 54 | 55 | def topics(self): 56 | """Get the set of topics that can be extracted from this report.""" 57 | return set(node.tag for node in self.root.iter() if node.attrib) 58 | 59 | def extract(self, topic: str, parseNumbers=True) -> list: 60 | """ 61 | Extract items of given topic and return as list of objects. 62 | 63 | The topic is a string like TradeConfirm, ChangeInDividendAccrual, 64 | Order, etc. 65 | """ 66 | cls = type(topic, (DynamicObject,), {}) 67 | results = [cls(**node.attrib) for node in self.root.iter(topic)] 68 | if parseNumbers: 69 | for obj in results: 70 | d = obj.__dict__ 71 | for k, v in d.items(): 72 | with suppress(ValueError): 73 | d[k] = float(v) 74 | d[k] = int(v) 75 | return results 76 | 77 | def df(self, topic: str, parseNumbers=True): 78 | """Same as extract but return the result as a pandas DataFrame.""" 79 | return util.df(self.extract(topic, parseNumbers)) 80 | 81 | def get_url(self): 82 | """Generate flexreport URL.""" 83 | 84 | def is_valid_url(url: str) -> bool: 85 | try: 86 | result = urlparse(url) 87 | # Must have scheme (http/https) and netloc (domain) 88 | return all([result.scheme, result.netloc]) 89 | except Exception: 90 | return False 91 | 92 | _url = os.getenv("IB_FLEXREPORT_URL", FLEXREPORT_URL) 93 | if is_valid_url(_url): 94 | return _url 95 | raise FlexError( 96 | "Invalid URL, please check that env variable IB_FLEXREPORT_URL is set correctly." 97 | ) 98 | 99 | def download(self, token, queryId): 100 | """Download report for the given ``token`` and ``queryId``.""" 101 | base_url = self.get_url() 102 | query = f"t={token}&q={queryId}&v=3" 103 | url = base_url + query 104 | 105 | resp = urlopen(url) 106 | data = resp.read() 107 | 108 | root = et.fromstring(data) 109 | elem = root.find("Status") 110 | if elem is not None and elem.text == "Success": 111 | elem = root.find("ReferenceCode") 112 | assert elem is not None 113 | code = elem.text 114 | elem = root.find("Url") 115 | assert elem is not None 116 | baseUrl = elem.text 117 | _logger.info("Statement is being prepared...") 118 | else: 119 | elem = root.find("ErrorCode") 120 | errorCode = elem.text if elem is not None else "" 121 | elem = root.find("ErrorMessage") 122 | errorMsg = elem.text if elem is not None else "" 123 | raise FlexError(f"{errorCode}: {errorMsg}") 124 | 125 | while True: 126 | time.sleep(1) 127 | url = f"{baseUrl}?q={code}&t={token}&v=3" 128 | resp = urlopen(url) 129 | self.data = resp.read() 130 | self.root = et.fromstring(self.data) 131 | if self.root[0].tag == "code": 132 | msg = self.root[0].text 133 | if msg and msg.startswith("Statement generation in progress"): 134 | _logger.info("still working...") 135 | continue 136 | else: 137 | raise FlexError(msg) 138 | break 139 | _logger.info("Statement retrieved.") 140 | 141 | def load(self, path): 142 | """Load report from XML file.""" 143 | with open(path, "rb") as f: 144 | self.data = f.read() 145 | self.root = et.fromstring(self.data) 146 | 147 | def save(self, path): 148 | """Save report to XML file.""" 149 | with open(path, "wb") as f: 150 | f.write(self.data) 151 | 152 | 153 | if __name__ == "__main__": 154 | util.logToConsole() 155 | report = FlexReport("945692423458902392892687", "272555") 156 | print(report.topics()) 157 | trades = report.extract("Trade") 158 | print(trades) 159 | -------------------------------------------------------------------------------- /notebooks/market_depth.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Market depth (order book)\n", 8 | "==============" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "metadata": {}, 15 | "outputs": [ 16 | { 17 | "data": { 18 | "text/plain": [ 19 | "" 20 | ] 21 | }, 22 | "execution_count": 1, 23 | "metadata": {}, 24 | "output_type": "execute_result" 25 | } 26 | ], 27 | "source": [ 28 | "from ib_async import *\n", 29 | "\n", 30 | "util.startLoop()\n", 31 | "\n", 32 | "ib = IB()\n", 33 | "ib.connect(\"127.0.0.1\", 7497, clientId=16)" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "To get a list of all exchanges that support market depth data and display the first five:" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": {}, 47 | "outputs": [ 48 | { 49 | "data": { 50 | "text/plain": [ 51 | "[DepthMktDataDescription(exchange='DTB', secType='OPT', listingExch='', serviceDataType='Deep', aggGroup=2147483647),\n", 52 | " DepthMktDataDescription(exchange='LSEETF', secType='STK', listingExch='', serviceDataType='Deep', aggGroup=2147483647),\n", 53 | " DepthMktDataDescription(exchange='SGX', secType='FUT', listingExch='', serviceDataType='Deep', aggGroup=2147483647),\n", 54 | " DepthMktDataDescription(exchange='IDEALPRO', secType='CASH', listingExch='', serviceDataType='Deep', aggGroup=4),\n", 55 | " DepthMktDataDescription(exchange='ARCA', secType='STK', listingExch='', serviceDataType='Deep', aggGroup=2147483647)]" 56 | ] 57 | }, 58 | "execution_count": 2, 59 | "metadata": {}, 60 | "output_type": "execute_result" 61 | } 62 | ], 63 | "source": [ 64 | "l = ib.reqMktDepthExchanges()\n", 65 | "l[:5]" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "Let's subscribe to market depth data for EURUSD:" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 3, 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "contract = Forex(\"EURUSD\")\n", 82 | "ib.qualifyContracts(contract)\n", 83 | "ticker = ib.reqMktDepth(contract)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "To see a live order book, an event handler for ticker updates is made that displays a dynamically updated dataframe:" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 4, 96 | "metadata": {}, 97 | "outputs": [ 98 | { 99 | "data": { 100 | "text/html": [ 101 | "
\n", 102 | "\n", 115 | "\n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | "
bidSizebidPriceaskPriceaskSize
0155000001.122651.1227521500000
1102000001.12261.12289000000
210000001.122551.122851000000
310000001.12251.123250000
40000
\n", 163 | "
" 164 | ], 165 | "text/plain": [ 166 | " bidSize bidPrice askPrice askSize\n", 167 | "0 15500000 1.12265 1.12275 21500000\n", 168 | "1 10200000 1.1226 1.1228 9000000\n", 169 | "2 1000000 1.12255 1.12285 1000000\n", 170 | "3 1000000 1.1225 1.1232 50000\n", 171 | "4 0 0 0 0" 172 | ] 173 | }, 174 | "metadata": {}, 175 | "output_type": "display_data" 176 | } 177 | ], 178 | "source": [ 179 | "import pandas as pd\n", 180 | "from IPython.display import clear_output, display\n", 181 | "\n", 182 | "df = pd.DataFrame(index=range(5), columns=\"bidSize bidPrice askPrice askSize\".split())\n", 183 | "\n", 184 | "\n", 185 | "def onTickerUpdate(ticker):\n", 186 | " bids = ticker.domBids\n", 187 | " for i in range(5):\n", 188 | " df.iloc[i, 0] = bids[i].size if i < len(bids) else 0\n", 189 | " df.iloc[i, 1] = bids[i].price if i < len(bids) else 0\n", 190 | " asks = ticker.domAsks\n", 191 | " for i in range(5):\n", 192 | " df.iloc[i, 2] = asks[i].price if i < len(asks) else 0\n", 193 | " df.iloc[i, 3] = asks[i].size if i < len(asks) else 0\n", 194 | " clear_output(wait=True)\n", 195 | " display(df)\n", 196 | "\n", 197 | "\n", 198 | "ticker.updateEvent += onTickerUpdate\n", 199 | "\n", 200 | "IB.sleep(15);" 201 | ] 202 | }, 203 | { 204 | "cell_type": "markdown", 205 | "metadata": {}, 206 | "source": [ 207 | "Stop the market depth subscription:" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 5, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "ib.cancelMktDepth(contract)" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": 6, 222 | "metadata": {}, 223 | "outputs": [], 224 | "source": [ 225 | "ib.disconnect()" 226 | ] 227 | } 228 | ], 229 | "metadata": { 230 | "kernelspec": { 231 | "display_name": "Python 3", 232 | "language": "python", 233 | "name": "python3" 234 | }, 235 | "language_info": { 236 | "codemirror_mode": { 237 | "name": "ipython", 238 | "version": 3 239 | }, 240 | "file_extension": ".py", 241 | "mimetype": "text/x-python", 242 | "name": "python", 243 | "nbconvert_exporter": "python", 244 | "pygments_lexer": "ipython3", 245 | "version": "3.7.5" 246 | } 247 | }, 248 | "nbformat": 4, 249 | "nbformat_minor": 4 250 | } 251 | -------------------------------------------------------------------------------- /docs/recipes.rst: -------------------------------------------------------------------------------- 1 | .. _recipes: 2 | 3 | 4 | Code recipes 5 | ============ 6 | 7 | Collection of useful patterns, snippets and recipes. 8 | 9 | When using the recipes in a notebook, don't forget to use ``util.startLoop()``. 10 | 11 | Fetching consecutive historical data 12 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | 14 | Suppose we want to get the 1 min bar data of Tesla since the very beginning 15 | up until now. The best way is to start with now and keep requesting further 16 | and further back in time until there is no more data returned. 17 | 18 | .. code-block:: python 19 | 20 | import datetime 21 | from ib_async import * 22 | 23 | ib = IB() 24 | ib.connect('127.0.0.1', 7497, clientId=1) 25 | 26 | contract = Stock('TSLA', 'SMART', 'USD') 27 | 28 | dt = '' 29 | barsList = [] 30 | while True: 31 | bars = ib.reqHistoricalData( 32 | contract, 33 | endDateTime=dt, 34 | durationStr='10 D', 35 | barSizeSetting='1 min', 36 | whatToShow='MIDPOINT', 37 | useRTH=True, 38 | formatDate=1) 39 | if not bars: 40 | break 41 | barsList.append(bars) 42 | dt = bars[0].date 43 | print(dt) 44 | 45 | # save to CSV file 46 | allBars = [b for bars in reversed(barsList) for b in bars] 47 | df = util.df(allBars) 48 | df.to_csv(contract.symbol + '.csv', index=False) 49 | 50 | Scanner data (blocking) 51 | ^^^^^^^^^^^^^^^^^^^^^^^ 52 | 53 | .. code-block:: python 54 | 55 | allParams = ib.reqScannerParameters() 56 | print(allParams) 57 | 58 | sub = ScannerSubscription( 59 | instrument='FUT.US', 60 | locationCode='FUT.GLOBEX', 61 | scanCode='TOP_PERC_GAIN') 62 | scanData = ib.reqScannerData(sub) 63 | print(scanData) 64 | 65 | Scanner data (streaming) 66 | ^^^^^^^^^^^^^^^^^^^^^^^^ 67 | 68 | .. code-block:: python 69 | 70 | def onScanData(scanData): 71 | print(scanData[0]) 72 | print(len(scanData)) 73 | 74 | sub = ScannerSubscription( 75 | instrument='FUT.US', 76 | locationCode='FUT.GLOBEX', 77 | scanCode='TOP_PERC_GAIN') 78 | scanData = ib.reqScannerSubscription(sub) 79 | scanData.updateEvent += onScanData 80 | ib.sleep(60) 81 | ib.cancelScannerSubscription(scanData) 82 | 83 | Option calculations 84 | ^^^^^^^^^^^^^^^^^^^ 85 | 86 | .. code-block:: python 87 | 88 | option = Option('EOE', '20171215', 490, 'P', 'FTA', multiplier=100) 89 | 90 | calc = ib.calculateImpliedVolatility( 91 | option, optionPrice=6.1, underPrice=525) 92 | print(calc) 93 | 94 | calc = ib.calculateOptionPrice( 95 | option, volatility=0.14, underPrice=525) 96 | print(calc) 97 | 98 | Order book 99 | ^^^^^^^^^^ 100 | 101 | .. code-block:: python 102 | 103 | eurusd = Forex('EURUSD') 104 | ticker = ib.reqMktDepth(eurusd) 105 | while ib.sleep(5): 106 | print( 107 | [d.price for d in ticker.domBids], 108 | [d.price for d in ticker.domAsks]) 109 | 110 | Minimum price increments 111 | ^^^^^^^^^^^^^^^^^^^^^^^^ 112 | 113 | .. code-block:: python 114 | 115 | usdjpy = Forex('USDJPY') 116 | cd = ib.reqContractDetails(usdjpy)[0] 117 | print(cd.marketRuleIds) 118 | 119 | rules = [ 120 | ib.reqMarketRule(ruleId) 121 | for ruleId in cd.marketRuleIds.split(',')] 122 | print(rules) 123 | 124 | News articles 125 | ^^^^^^^^^^^^^ 126 | 127 | .. code-block:: python 128 | 129 | newsProviders = ib.reqNewsProviders() 130 | print(newsProviders) 131 | codes = '+'.join(np.code for np in newsProviders) 132 | 133 | amd = Stock('AMD', 'SMART', 'USD') 134 | ib.qualifyContracts(amd) 135 | headlines = ib.reqHistoricalNews(amd.conId, codes, '', '', 10) 136 | latest = headlines[0] 137 | print(latest) 138 | article = ib.reqNewsArticle(latest.providerCode, latest.articleId) 139 | print(article) 140 | 141 | News bulletins 142 | ^^^^^^^^^^^^^^ 143 | 144 | .. code-block:: python 145 | 146 | ib.reqNewsBulletins(True) 147 | ib.sleep(5) 148 | print(ib.newsBulletins()) 149 | 150 | 151 | WSH Event Calendar 152 | ^^^^^^^^^^^^^^^^^^ 153 | 154 | A `Wall Street Horizon subscription `_ 155 | is needed to get corporate event data. 156 | 157 | .. code-block:: python 158 | 159 | from ib_async import * 160 | 161 | ib = IB() 162 | ib.connect('127.0.0.1', 7497, clientId=1) 163 | 164 | # Get the conId of an instrument (IBM in this case): 165 | ibm = Stock('IBM', 'SMART', 'USD') 166 | ib.qualifyContracts(ibm) 167 | print(ibm.conId) # is 8314 168 | 169 | # Get the list of available filters and event types: 170 | meta = ib.getWshMetaData() 171 | print(meta) 172 | 173 | # For IBM (with conId=8314) query the: 174 | # - Earnings Dates (wshe_ed) 175 | # - Board of Directors meetings (wshe_bod) 176 | data = WshEventData( 177 | filter = '''{ 178 | "country": "All", 179 | "watchlist": ["8314"], 180 | "limit_region": 10, 181 | "limit": 10, 182 | "wshe_ed": "true", 183 | "wshe_bod": "true" 184 | }''') 185 | events = ib.getWshEventData(data) 186 | print(events) 187 | 188 | Dividends 189 | ^^^^^^^^^ 190 | 191 | .. code-block:: python 192 | 193 | contract = Stock('INTC', 'SMART', 'USD') 194 | ticker = ib.reqMktData(contract, '456') 195 | ib.sleep(2) 196 | print(ticker.dividends) 197 | 198 | Output:: 199 | 200 | Dividends(past12Months=1.2, next12Months=1.2, nextDate=datetime.date(2019, 2, 6), nextAmount=0.3) 201 | 202 | Fundamental ratios 203 | ^^^^^^^^^^^^^^^^^^ 204 | 205 | .. code-block:: python 206 | 207 | contract = Stock('IBM', 'SMART', 'USD') 208 | ticker = ib.reqMktData(contract, '258') 209 | ib.sleep(2) 210 | print(ticker.fundamentalRatios) 211 | 212 | 213 | Short-lived connections 214 | ^^^^^^^^^^^^^^^^^^^^^^^ 215 | 216 | This IB socket protocol is designed to be used for a long-lived connection, 217 | lasting a day or so. For short connections, where for example just a few 218 | orders are fired of, it is best to add one second of delay before closing the 219 | connection. This gives the connection some time to flush 220 | the data that has not been sent yet. 221 | 222 | .. code-block:: python 223 | 224 | ib = IB() 225 | ib.connect() 226 | 227 | ... # create and submit some orders 228 | 229 | ib.sleep(1) # added delay 230 | ib.disconnect() 231 | 232 | 233 | Integration with PyQt5 or PySide2 234 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 235 | 236 | .. image:: images/qt-tickertable.png 237 | 238 | `This example `_ 239 | of a ticker table shows how to integrate both 240 | realtime streaming and synchronous API requests in a single-threaded 241 | Qt application. 242 | The API requests in this example are ``connect`` and 243 | ``ib.qualifyContracts()``; The latter is used 244 | to get the conId of a contract and use that as a unique key. 245 | 246 | The Qt interface will not freeze when a request is ongoing and it is even 247 | possible to have multiple outstanding requests at the same time. 248 | 249 | This example depends on PyQt5: 250 | 251 | ``pip3 install -U PyQt5``. 252 | 253 | It's also possible to use PySide2 instead; To do so uncomment the PySide2 254 | import and ``util.useQt`` lines in the example and comment out their PyQt5 255 | counterparts. 256 | 257 | Integration with Tkinter 258 | ^^^^^^^^^^^^^^^^^^^^^^^^ 259 | 260 | To integrate with the Tkinter event loop, take a look at 261 | `this example app `_. 262 | 263 | Integration with PyGame 264 | ^^^^^^^^^^^^^^^^^^^^^^^ 265 | 266 | By calling ``ib.sleep`` from within the PyGame run loop, ib_async can periodically 267 | run for short whiles and keep up to date: 268 | 269 | .. code-block:: python 270 | 271 | import ib_async as ibi 272 | import pygame 273 | 274 | 275 | def onTicker(ticker): 276 | screen.fill(bg_color) 277 | text = f'bid: {ticker.bid} ask: {ticker.ask}' 278 | quote = font.render(text, True, fg_color) 279 | screen.blit(quote, (40, 40)) 280 | pygame.display.flip() 281 | 282 | 283 | pygame.init() 284 | screen = pygame.display.set_mode((800, 600)) 285 | font = pygame.font.SysFont('arial', 48) 286 | bg_color = (255, 255, 255) 287 | fg_color = (0, 0, 0) 288 | 289 | ib = ibi.IB() 290 | ib.connect() 291 | contract = ibi.Forex('EURUSD') 292 | ticker = ib.reqMktData(contract) 293 | ticker.updateEvent += onTicker 294 | 295 | running = True 296 | while running: 297 | # This updates IB-insync: 298 | ib.sleep(0.03) 299 | 300 | # This updates PyGame: 301 | for event in pygame.event.get(): 302 | if event.type == pygame.QUIT: 303 | running = False 304 | pygame.quit() 305 | -------------------------------------------------------------------------------- /notebooks/tick_data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Tick data\n", 8 | "\n", 9 | "For optimum results this notebook should be run during the Forex trading session." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "text/plain": [ 20 | "" 21 | ] 22 | }, 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "output_type": "execute_result" 26 | } 27 | ], 28 | "source": [ 29 | "from ib_async import *\n", 30 | "\n", 31 | "util.startLoop()\n", 32 | "\n", 33 | "ib = IB()\n", 34 | "ib.connect(\"127.0.0.1\", 7497, clientId=15)" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "### Streaming tick data\n", 42 | "\n", 43 | "Create some Forex contracts:" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 2, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "contracts = [\n", 53 | " Forex(pair) for pair in (\"EURUSD\", \"USDJPY\", \"GBPUSD\", \"USDCHF\", \"USDCAD\", \"AUDUSD\")\n", 54 | "]\n", 55 | "ib.qualifyContracts(*contracts)\n", 56 | "\n", 57 | "eurusd = contracts[0]" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "Request streaming ticks for them:" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 3, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "for contract in contracts:\n", 74 | " ib.reqMktData(contract, \"\", False, False)" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "Wait a few seconds for the tickers to get filled." 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 4, 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "data": { 91 | "text/plain": [ 92 | "Ticker(contract=Forex('EURUSD', conId=12087792, exchange='IDEALPRO', localSymbol='EUR.USD', tradingClass='EUR.USD'), time=datetime.datetime(2019, 12, 31, 17, 5, 2, 127038, tzinfo=datetime.timezone.utc), bid=1.12245, bidSize=10700000, ask=1.1225, askSize=2000000, high=1.1239, low=1.11985, close=1.12, halted=0.0)" 93 | ] 94 | }, 95 | "execution_count": 4, 96 | "metadata": {}, 97 | "output_type": "execute_result" 98 | } 99 | ], 100 | "source": [ 101 | "ticker = ib.ticker(eurusd)\n", 102 | "ib.sleep(2)\n", 103 | "\n", 104 | "ticker" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "The price of Forex ticks is always nan. To get a midpoint price use ``midpoint()`` or ``marketPrice()``.\n", 112 | "\n", 113 | "The tickers are kept live updated, try this a few times to see if the price changes:" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 5, 119 | "metadata": {}, 120 | "outputs": [ 121 | { 122 | "data": { 123 | "text/plain": [ 124 | "1.1224750000000001" 125 | ] 126 | }, 127 | "execution_count": 5, 128 | "metadata": {}, 129 | "output_type": "execute_result" 130 | } 131 | ], 132 | "source": [ 133 | "ticker.marketPrice()" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "The following cell will start a 30 second loop that prints a live updated ticker table.\n", 141 | "It is updated on every ticker change." 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 6, 147 | "metadata": {}, 148 | "outputs": [ 149 | { 150 | "data": { 151 | "text/html": [ 152 | "
\n", 153 | "\n", 166 | "\n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | "
bidSizebidaskaskSizehighlowclose
EURUSD115000001.12251.1225510000001.12391.119851.12
USDJPY8000000108.665108.67516000000108.885108.475108.87
GBPUSD60000001.3271.32715000001.328451.31061.3111
USDCHF100000000.96770.967870000000.96980.964650.96935
USDCAD10000001.29661.2966520000001.306951.295151.3068
AUDUSD95000000.703050.703110000000.703250.699450.6995
\n", 242 | "
" 243 | ], 244 | "text/plain": [ 245 | " bidSize bid ask askSize high low close\n", 246 | "EURUSD 11500000 1.1225 1.12255 1000000 1.1239 1.11985 1.12\n", 247 | "USDJPY 8000000 108.665 108.675 16000000 108.885 108.475 108.87\n", 248 | "GBPUSD 6000000 1.327 1.3271 500000 1.32845 1.3106 1.3111\n", 249 | "USDCHF 10000000 0.9677 0.9678 7000000 0.9698 0.96465 0.96935\n", 250 | "USDCAD 1000000 1.2966 1.29665 2000000 1.30695 1.29515 1.3068\n", 251 | "AUDUSD 9500000 0.70305 0.7031 1000000 0.70325 0.69945 0.6995" 252 | ] 253 | }, 254 | "metadata": {}, 255 | "output_type": "display_data" 256 | } 257 | ], 258 | "source": [ 259 | "import pandas as pd\n", 260 | "from IPython.display import clear_output, display\n", 261 | "\n", 262 | "df = pd.DataFrame(\n", 263 | " index=[c.pair() for c in contracts],\n", 264 | " columns=[\"bidSize\", \"bid\", \"ask\", \"askSize\", \"high\", \"low\", \"close\"],\n", 265 | ")\n", 266 | "\n", 267 | "\n", 268 | "def onPendingTickers(tickers):\n", 269 | " for t in tickers:\n", 270 | " df.loc[t.contract.pair()] = (\n", 271 | " t.bidSize,\n", 272 | " t.bid,\n", 273 | " t.ask,\n", 274 | " t.askSize,\n", 275 | " t.high,\n", 276 | " t.low,\n", 277 | " t.close,\n", 278 | " )\n", 279 | " clear_output(wait=True)\n", 280 | " display(df)\n", 281 | "\n", 282 | "\n", 283 | "ib.pendingTickersEvent += onPendingTickers\n", 284 | "ib.sleep(30)\n", 285 | "ib.pendingTickersEvent -= onPendingTickers" 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "metadata": {}, 291 | "source": [ 292 | "New tick data is available in the 'ticks' attribute of the pending tickers.\n", 293 | "The tick data will be cleared before the next update." 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "To stop the live tick subscriptions:" 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": 7, 306 | "metadata": {}, 307 | "outputs": [], 308 | "source": [ 309 | "for contract in contracts:\n", 310 | " ib.cancelMktData(contract)" 311 | ] 312 | }, 313 | { 314 | "cell_type": "markdown", 315 | "metadata": {}, 316 | "source": [ 317 | "### Tick by Tick data ###\n", 318 | "\n", 319 | "The ticks in the previous section are time-sampled by IB in order to cut on bandwidth. So with ``reqMktdData`` not every tick from the exchanges is sent. The promise of ``reqTickByTickData`` is to send every tick, just how it appears in the TWS Time & Sales window. This functionality is severly nerfed by a total of just three simultaneous subscriptions, where bid-ask ticks and sale ticks also use up a subscription each.\n", 320 | "\n", 321 | "The tick-by-tick updates are available from ``ticker.tickByTicks`` and are signalled by ``ib.pendingTickersEvent`` or ``ticker.updateEvent``." 322 | ] 323 | }, 324 | { 325 | "cell_type": "code", 326 | "execution_count": 8, 327 | "metadata": {}, 328 | "outputs": [ 329 | { 330 | "name": "stdout", 331 | "output_type": "stream", 332 | "text": [ 333 | "Ticker(contract=Forex('EURUSD', conId=12087792, exchange='IDEALPRO', localSymbol='EUR.USD', tradingClass='EUR.USD'), time=datetime.datetime(2019, 12, 31, 17, 5, 35, 432737, tzinfo=datetime.timezone.utc), bid=1.1225, bidSize=11000000, ask=1.1226, askSize=11500000, prevBid=1.12255, prevBidSize=11500000, prevAsk=1.12255, prevAskSize=1000000, high=1.1239, low=1.11985, close=1.12, halted=0.0, tickByTicks=[TickByTickBidAsk(time=datetime.datetime(2019, 12, 31, 17, 5, 35, 432737, tzinfo=datetime.timezone.utc), bidPrice=1.1225, askPrice=1.12255, bidSize=11500000, askSize=1000000, tickAttribBidAsk=TickAttribBidAsk(bidPastLow=False, askPastHigh=False)), TickByTickBidAsk(time=datetime.datetime(2019, 12, 31, 17, 5, 35, 432737, tzinfo=datetime.timezone.utc), bidPrice=1.1225, askPrice=1.12255, bidSize=11000000, askSize=1000000, tickAttribBidAsk=TickAttribBidAsk(bidPastLow=False, askPastHigh=False)), TickByTickBidAsk(time=datetime.datetime(2019, 12, 31, 17, 5, 35, 432737, tzinfo=datetime.timezone.utc), bidPrice=1.1225, askPrice=1.1226, bidSize=11000000, askSize=11500000, tickAttribBidAsk=TickAttribBidAsk(bidPastLow=False, askPastHigh=False))])\n" 334 | ] 335 | } 336 | ], 337 | "source": [ 338 | "ticker = ib.reqTickByTickData(eurusd, \"BidAsk\")\n", 339 | "ib.sleep(2)\n", 340 | "print(ticker)\n", 341 | "\n", 342 | "ib.cancelTickByTickData(ticker.contract, \"BidAsk\")" 343 | ] 344 | }, 345 | { 346 | "cell_type": "markdown", 347 | "metadata": {}, 348 | "source": [ 349 | "### Historical tick data\n", 350 | "\n", 351 | "Historical tick data can be fetched with a maximum of 1000 ticks at a time. Either the start time or the end time must be given, and one of them must remain empty:" 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": 9, 357 | "metadata": {}, 358 | "outputs": [ 359 | { 360 | "data": { 361 | "text/plain": [ 362 | "HistoricalTickBidAsk(time=datetime.datetime(2019, 12, 31, 17, 5, 34, tzinfo=datetime.timezone.utc), tickAttribBidAsk=TickAttribBidAsk(bidPastLow=False, askPastHigh=False), priceBid=1.1225, priceAsk=1.1226, sizeBid=11000000, sizeAsk=11500000)" 363 | ] 364 | }, 365 | "execution_count": 9, 366 | "metadata": {}, 367 | "output_type": "execute_result" 368 | } 369 | ], 370 | "source": [ 371 | "import datetime\n", 372 | "\n", 373 | "start = \"\"\n", 374 | "end = datetime.datetime.now()\n", 375 | "ticks = ib.reqHistoricalTicks(eurusd, start, end, 1000, \"BID_ASK\", useRth=False)\n", 376 | "\n", 377 | "ticks[-1]" 378 | ] 379 | }, 380 | { 381 | "cell_type": "code", 382 | "execution_count": 10, 383 | "metadata": {}, 384 | "outputs": [], 385 | "source": [ 386 | "ib.disconnect()" 387 | ] 388 | } 389 | ], 390 | "metadata": { 391 | "kernelspec": { 392 | "display_name": "Python 3", 393 | "language": "python", 394 | "name": "python3" 395 | }, 396 | "language_info": { 397 | "codemirror_mode": { 398 | "name": "ipython", 399 | "version": 3 400 | }, 401 | "file_extension": ".py", 402 | "mimetype": "text/x-python", 403 | "name": "python", 404 | "nbconvert_exporter": "python", 405 | "pygments_lexer": "ipython3", 406 | "version": "3.7.5" 407 | } 408 | }, 409 | "nbformat": 4, 410 | "nbformat_minor": 4 411 | } 412 | -------------------------------------------------------------------------------- /notebooks/basics.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Basics\n", 8 | "\n", 9 | "Let's first take a look at what's inside the ``ib_async`` package:" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "['util', 'Event', 'OrderComboLeg', 'OrderState', 'SoftDollarTier', 'PriceIncrement', 'Execution', 'CommissionReport', 'BarList', 'BarDataList', 'RealTimeBarList', 'BarData', 'RealTimeBar', 'HistogramData', 'NewsProvider', 'DepthMktDataDescription', 'ScannerSubscription', 'ScanDataList', 'FundamentalRatios', 'ExecutionFilter', 'PnL', 'PnLSingle', 'AccountValue', 'TickData', 'TickByTickAllLast', 'TickByTickBidAsk', 'TickByTickMidPoint', 'HistoricalTick', 'HistoricalTickBidAsk', 'HistoricalTickLast', 'TickAttrib', 'TickAttribBidAsk', 'TickAttribLast', 'MktDepthData', 'DOMLevel', 'TradeLogEntry', 'TagValue', 'FamilyCode', 'SmartComponent', 'PortfolioItem', 'Position', 'Fill', 'OptionComputation', 'OptionChain', 'Dividends', 'NewsArticle', 'HistoricalNews', 'NewsTick', 'NewsBulletin', 'ConnectionStats', 'Contract', 'Stock', 'Option', 'Future', 'ContFuture', 'Forex', 'Index', 'CFD', 'Commodity', 'Bond', 'FuturesOption', 'MutualFund', 'Warrant', 'Bag', 'ComboLeg', 'DeltaNeutralContract', 'ContractDetails', 'ContractDescription', 'ScanData', 'Trade', 'OrderStatus', 'Order', 'LimitOrder', 'MarketOrder', 'StopOrder', 'StopLimitOrder', 'BracketOrder', 'OrderCondition', 'ExecutionCondition', 'MarginCondition', 'TimeCondition', 'PriceCondition', 'PercentChangeCondition', 'VolumeCondition', 'Ticker', 'IB', 'Client', 'Wrapper', 'FlexReport', 'FlexError', 'IBC', 'IBController', 'Watchdog']\n" 22 | ] 23 | } 24 | ], 25 | "source": [ 26 | "import ib_async\n", 27 | "\n", 28 | "print(ib_async.__all__)" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "### Importing\n", 36 | "The following two lines are used at the top of all notebooks. The first line imports everything and the second\n", 37 | "starts an event loop to keep the notebook live updated:" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from ib_async import *\n", 47 | "\n", 48 | "util.startLoop()" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "*Note that startLoop() only works in notebooks, not in regular Python programs.*" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "### Connecting\n", 63 | "The main player of the whole package is the \"IB\" class. Let's create an IB instance and connect to a running TWS/IBG application:" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 3, 69 | "metadata": { 70 | "scrolled": true 71 | }, 72 | "outputs": [ 73 | { 74 | "data": { 75 | "text/plain": [ 76 | "" 77 | ] 78 | }, 79 | "execution_count": 3, 80 | "metadata": {}, 81 | "output_type": "execute_result" 82 | } 83 | ], 84 | "source": [ 85 | "ib = IB()\n", 86 | "ib.connect(\"127.0.0.1\", 7497, clientId=10)" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": {}, 92 | "source": [ 93 | "If the connection failed, then verify that the application has the API port enabled and double-check the hostname and port. For IB Gateway the default port is 4002. Make sure the clientId is not already in use.\n", 94 | "\n", 95 | "If the connection succeeded, then ib will be synchronized with TWS/IBG. The \"current state\" is now available via methods such as ib.positions(), ib.trades(), ib.openTrades(), ib.accountValues() or ib.tickers(). Let's list the current positions:" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 4, 101 | "metadata": {}, 102 | "outputs": [ 103 | { 104 | "data": { 105 | "text/plain": [ 106 | "[]" 107 | ] 108 | }, 109 | "execution_count": 4, 110 | "metadata": {}, 111 | "output_type": "execute_result" 112 | } 113 | ], 114 | "source": [ 115 | "ib.positions()" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "Or filter the account values to get the liquidation value:" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 5, 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "data": { 132 | "text/plain": [ 133 | "[AccountValue(account='U172554', tag='NetLiquidationByCurrency', value='2039.4021', currency='BASE', modelCode='')]" 134 | ] 135 | }, 136 | "execution_count": 5, 137 | "metadata": {}, 138 | "output_type": "execute_result" 139 | } 140 | ], 141 | "source": [ 142 | "[\n", 143 | " v\n", 144 | " for v in ib.accountValues()\n", 145 | " if v.tag == \"NetLiquidationByCurrency\" and v.currency == \"BASE\"\n", 146 | "]" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "The \"current state\" will automatically be kept in sync with TWS/IBG. So an order fill will be added as soon as it is reported, or account values will be updated as soon as they change in TWS." 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "### Contracts\n", 161 | "\n", 162 | "Contracts can be specified in different ways:\n", 163 | "* The ibapi way, by creating an empty Contract object and setting its attributes one by one;\n", 164 | "* By using Contract and giving the attributes as keyword argument;\n", 165 | "* By using the specialized Stock, Option, Future, Forex, Index, CFD, Commodity,\n", 166 | " Bond, FuturesOption, MutualFund or Warrant contracts.\n", 167 | "\n", 168 | "Some examples:" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 6, 174 | "metadata": {}, 175 | "outputs": [], 176 | "source": [ 177 | "Contract(conId=270639)\n", 178 | "Stock(\"AMD\", \"SMART\", \"USD\")\n", 179 | "Stock(\"INTC\", \"SMART\", \"USD\", primaryExchange=\"NASDAQ\")\n", 180 | "Forex(\"EURUSD\")\n", 181 | "CFD(\"IBUS30\")\n", 182 | "Future(\"ES\", \"20180921\", \"GLOBEX\")\n", 183 | "Option(\"SPY\", \"20170721\", 240, \"C\", \"SMART\")\n", 184 | "Bond(secIdType=\"ISIN\", secId=\"US03076KAA60\");" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "### Sending a request\n", 192 | "\n", 193 | "The IB class has nearly all request methods that the IB API offers. The methods that return a result will block until finished and then return the result. Take for example reqContractDetails:" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 7, 199 | "metadata": {}, 200 | "outputs": [ 201 | { 202 | "data": { 203 | "text/plain": [ 204 | "[ContractDetails(contract=Contract(secType='STK', conId=76792991, symbol='TSLA', exchange='SMART', primaryExchange='NASDAQ', currency='USD', localSymbol='TSLA', tradingClass='NMS', comboLegs=[]), marketName='NMS', minTick=0.01, orderTypes='ACTIVETIM,ADJUST,ALERT,ALGO,ALLOC,AON,AVGCOST,BASKET,BENCHPX,COND,CONDORDER,DARKONLY,DARKPOLL,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,GAT,GTC,GTD,GTT,HID,IBKRATS,ICE,IMB,IOC,LIT,LMT,LOC,MIDPX,MIT,MKT,MOC,MTL,NGCOMB,NODARK,NONALGO,OCA,OPG,OPGREROUT,PEGBENCH,POSTONLY,PREOPGRTH,REL,RPI,RTH,SCALE,SCALEODD,SCALERST,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,TRAILMIT,WHATIF', validExchanges='SMART,AMEX,NYSE,CBOE,PHLX,ISE,CHX,ARCA,ISLAND,DRCTEDGE,BEX,BATS,EDGEA,CSFBALGO,JEFFALGO,BYX,IEX,EDGX,FOXRIVER,TPLUS1,NYSENAT,PSX', priceMagnifier=1, underConId=0, longName='TESLA INC', contractMonth='', industry='Consumer, Cyclical', category='Auto Manufacturers', subcategory='Auto-Cars/Light Trucks', timeZoneId='EST (Eastern Standard Time)', tradingHours='20191231:0400-20191231:2000;20200101:CLOSED;20200102:0400-20200102:2000;20200103:0400-20200103:2000;20200104:CLOSED;20200105:CLOSED;20200106:0400-20200106:2000;20200107:0400-20200107:2000;20200108:0400-20200108:2000;20200109:0400-20200109:2000;20200110:0400-20200110:2000;20200111:CLOSED;20200112:CLOSED;20200113:0400-20200113:2000;20200114:0400-20200114:2000;20200115:0400-20200115:2000;20200116:0400-20200116:2000;20200117:0400-20200117:2000;20200118:CLOSED;20200119:CLOSED;20200120:0400-20200120:2000;20200121:0400-20200121:2000;20200122:0400-20200122:2000;20200123:0400-20200123:2000;20200124:0400-20200124:2000;20200125:CLOSED;20200126:CLOSED;20200127:0400-20200127:2000;20200128:0400-20200128:2000;20200129:0400-20200129:2000;20200130:0400-20200130:2000;20200131:0400-20200131:2000;20200201:CLOSED;20200202:CLOSED;20200203:0400-20200203:2000', liquidHours='20191231:0930-20191231:1600;20200101:CLOSED;20200102:0930-20200102:1600;20200103:0930-20200103:1600;20200104:CLOSED;20200105:CLOSED;20200106:0930-20200106:1600;20200107:0930-20200107:1600;20200108:0930-20200108:1600;20200109:0930-20200109:1600;20200110:0930-20200110:1600;20200111:CLOSED;20200112:CLOSED;20200113:0930-20200113:1600;20200114:0930-20200114:1600;20200115:0930-20200115:1600;20200116:0930-20200116:1600;20200117:0930-20200117:1600;20200118:CLOSED;20200119:CLOSED;20200120:0930-20200120:1600;20200121:0930-20200121:1600;20200122:0930-20200122:1600;20200123:0930-20200123:1600;20200124:0930-20200124:1600;20200125:CLOSED;20200126:CLOSED;20200127:0930-20200127:1600;20200128:0930-20200128:1600;20200129:0930-20200129:1600;20200130:0930-20200130:1600;20200131:0930-20200131:1600;20200201:CLOSED;20200202:CLOSED;20200203:0930-20200203:1600', evRule='', evMultiplier=0, mdSizeMultiplier=100, aggGroup=1, underSymbol='', underSecType='', marketRuleIds='26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26', secIdList=[], realExpirationDate='', lastTradeTime='', stockType='COMMON', cusip='', ratings='', descAppend='', bondType='', couponType='', callable=False, putable=False, coupon=0, convertible=False, maturity='', issueDate='', nextOptionDate='', nextOptionType='', nextOptionPartial=False, notes='')]" 205 | ] 206 | }, 207 | "execution_count": 7, 208 | "metadata": {}, 209 | "output_type": "execute_result" 210 | } 211 | ], 212 | "source": [ 213 | "contract = Stock(\"TSLA\", \"SMART\", \"USD\")\n", 214 | "ib.reqContractDetails(contract)" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": {}, 220 | "source": [ 221 | "### Current state vs request\n", 222 | "\n", 223 | "Doing a request involves network traffic going up and down and can take considerable time. The current state on the other hand is always immediately available. So it is preferable to use the current state methods over requests. For example, use ``ib.openOrders()`` in preference over ``ib.reqOpenOrders()``, or ``ib.positions()`` over ``ib.reqPositions()``, etc:" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 8, 229 | "metadata": {}, 230 | "outputs": [ 231 | { 232 | "name": "stdout", 233 | "output_type": "stream", 234 | "text": [ 235 | "CPU times: user 5 µs, sys: 3 µs, total: 8 µs\n", 236 | "Wall time: 9.06 µs\n" 237 | ] 238 | } 239 | ], 240 | "source": [ 241 | "%time l = ib.positions()" 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": 9, 247 | "metadata": {}, 248 | "outputs": [ 249 | { 250 | "name": "stdout", 251 | "output_type": "stream", 252 | "text": [ 253 | "CPU times: user 0 ns, sys: 745 µs, total: 745 µs\n", 254 | "Wall time: 32.7 ms\n" 255 | ] 256 | } 257 | ], 258 | "source": [ 259 | "%time l = ib.reqPositions()" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "metadata": {}, 265 | "source": [ 266 | "### Logging\n", 267 | "\n", 268 | "The following will put log messages of INFO and higher level under the current active cell:" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": 10, 274 | "metadata": {}, 275 | "outputs": [], 276 | "source": [ 277 | "util.logToConsole()" 278 | ] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "metadata": {}, 283 | "source": [ 284 | "To see all debug messages (including network traffic):" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": 11, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "import logging\n", 294 | "\n", 295 | "util.logToConsole(logging.DEBUG)" 296 | ] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "metadata": {}, 301 | "source": [ 302 | "### Disconnecting\n", 303 | "\n", 304 | "The following will disconnect ``ib`` and clear all its state:" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": 12, 310 | "metadata": {}, 311 | "outputs": [ 312 | { 313 | "name": "stderr", 314 | "output_type": "stream", 315 | "text": [ 316 | "2019-12-31 13:28:29,252 ib_async.ib INFO Disconnecting from 127.0.0.1:7497, 160 B sent in 9 messages, 21.0 kB received in 418 messages, session time 920 ms.\n", 317 | "2019-12-31 13:28:29,255 ib_async.client INFO Disconnecting\n" 318 | ] 319 | } 320 | ], 321 | "source": [ 322 | "ib.disconnect()" 323 | ] 324 | } 325 | ], 326 | "metadata": { 327 | "kernelspec": { 328 | "display_name": "Python 3", 329 | "language": "python", 330 | "name": "python3" 331 | }, 332 | "language_info": { 333 | "codemirror_mode": { 334 | "name": "ipython", 335 | "version": 3 336 | }, 337 | "file_extension": ".py", 338 | "mimetype": "text/x-python", 339 | "name": "python", 340 | "nbconvert_exporter": "python", 341 | "pygments_lexer": "ipython3", 342 | "version": "3.7.5" 343 | } 344 | }, 345 | "nbformat": 4, 346 | "nbformat_minor": 4 347 | } 348 | -------------------------------------------------------------------------------- /ib_async/ibcontroller.py: -------------------------------------------------------------------------------- 1 | """Programmatic control over the TWS/gateway client software.""" 2 | 3 | import asyncio 4 | import logging 5 | import sys 6 | from contextlib import suppress 7 | from dataclasses import dataclass 8 | from typing import ClassVar 9 | 10 | from eventkit import Event 11 | 12 | import ib_async.util as util 13 | from ib_async.contract import Contract, Forex 14 | from ib_async.ib import IB 15 | 16 | 17 | @dataclass 18 | class IBC: 19 | r""" 20 | Programmatic control over starting and stopping TWS/Gateway 21 | using IBC (https://github.com/IbcAlpha/IBC). 22 | 23 | Args: 24 | twsVersion (int): (required) The major version number for 25 | TWS or gateway. 26 | gateway (bool): 27 | * True = gateway 28 | * False = TWS 29 | tradingMode (str): 'live' or 'paper'. 30 | userid (str): IB account username. It is recommended to set the real 31 | username/password in a secured IBC config file. 32 | password (str): IB account password. 33 | twsPath (str): Path to the TWS installation folder. 34 | Defaults: 35 | 36 | * Linux: ~/Jts 37 | * OS X: ~/Applications 38 | * Windows: C:\\Jts 39 | twsSettingsPath (str): Path to the TWS settings folder. 40 | Defaults: 41 | 42 | * Linux: ~/Jts 43 | * OS X: ~/Jts 44 | * Windows: Not available 45 | ibcPath (str): Path to the IBC installation folder. 46 | Defaults: 47 | 48 | * Linux: /opt/ibc 49 | * OS X: /opt/ibc 50 | * Windows: C:\\IBC 51 | ibcIni (str): Path to the IBC configuration file. 52 | Defaults: 53 | 54 | * Linux: ~/ibc/config.ini 55 | * OS X: ~/ibc/config.ini 56 | * Windows: %%HOMEPATH%%\\Documents\IBC\\config.ini 57 | javaPath (str): Path to Java executable. 58 | Default is to use the Java VM included with TWS/gateway. 59 | fixuserid (str): FIX account user id (gateway only). 60 | fixpassword (str): FIX account password (gateway only). 61 | on2fatimeout (str): What to do if 2-factor authentication times 62 | out; Can be 'restart' or 'exit'. 63 | 64 | This is not intended to be run in a notebook. 65 | 66 | To use IBC on Windows, the proactor (or quamash) event loop 67 | must have been set: 68 | 69 | .. code-block:: python 70 | 71 | import asyncio 72 | asyncio.set_event_loop(asyncio.ProactorEventLoop()) 73 | 74 | Example usage: 75 | 76 | .. code-block:: python 77 | 78 | ibc = IBC(976, gateway=True, tradingMode='live', 79 | userid='edemo', password='demouser') 80 | ibc.start() 81 | IB.run() 82 | """ 83 | 84 | IbcLogLevel: ClassVar = logging.DEBUG 85 | 86 | twsVersion: int = 0 87 | gateway: bool = False 88 | tradingMode: str = "" 89 | twsPath: str = "" 90 | twsSettingsPath: str = "" 91 | ibcPath: str = "" 92 | ibcIni: str = "" 93 | javaPath: str = "" 94 | userid: str = "" 95 | password: str = "" 96 | fixuserid: str = "" 97 | fixpassword: str = "" 98 | on2fatimeout: str = "" 99 | 100 | def __post_init__(self): 101 | self._isWindows = sys.platform == "win32" 102 | if not self.ibcPath: 103 | self.ibcPath = "/opt/ibc" if not self._isWindows else "C:\\IBC" 104 | self._proc = None 105 | self._monitor = None 106 | self._logger = logging.getLogger("ib_async.IBC") 107 | 108 | def __enter__(self): 109 | self.start() 110 | return self 111 | 112 | def __exit__(self, *_exc): 113 | self.terminate() 114 | 115 | def start(self): 116 | """Launch TWS/IBG.""" 117 | util.run(self.startAsync()) 118 | 119 | def terminate(self): 120 | """Terminate TWS/IBG.""" 121 | util.run(self.terminateAsync()) 122 | 123 | async def startAsync(self): 124 | if self._proc: 125 | return 126 | self._logger.info("Starting") 127 | 128 | # map from field names to cmd arguments; key=(UnixArg, WindowsArg) 129 | args = dict( 130 | twsVersion=("", ""), 131 | gateway=("--gateway", "/Gateway"), 132 | tradingMode=("--mode=", "/Mode:"), 133 | twsPath=("--tws-path=", "/TwsPath:"), 134 | twsSettingsPath=("--tws-settings-path=", ""), 135 | ibcPath=("--ibc-path=", "/IbcPath:"), 136 | ibcIni=("--ibc-ini=", "/Config:"), 137 | javaPath=("--java-path=", "/JavaPath:"), 138 | userid=("--user=", "/User:"), 139 | password=("--pw=", "/PW:"), 140 | fixuserid=("--fix-user=", "/FIXUser:"), 141 | fixpassword=("--fix-pw=", "/FIXPW:"), 142 | on2fatimeout=("--on2fatimeout=", "/On2FATimeout:"), 143 | ) 144 | 145 | # create shell command 146 | cmd = [ 147 | ( 148 | f"{self.ibcPath}\\scripts\\StartIBC.bat" 149 | if self._isWindows 150 | else f"{self.ibcPath}/scripts/ibcstart.sh" 151 | ) 152 | ] 153 | for k, v in util.dataclassAsDict(self).items(): 154 | arg = args[k][self._isWindows] 155 | if v: 156 | if arg.endswith("=") or arg.endswith(":"): 157 | cmd.append(f"{arg}{v}") 158 | elif arg: 159 | cmd.append(arg) 160 | else: 161 | cmd.append(str(v)) 162 | 163 | # run shell command 164 | self._proc = await asyncio.create_subprocess_exec( 165 | *cmd, stdout=asyncio.subprocess.PIPE 166 | ) 167 | self._monitor = asyncio.ensure_future(self.monitorAsync()) 168 | 169 | async def terminateAsync(self): 170 | if not self._proc: 171 | return 172 | self._logger.info("Terminating") 173 | if self._monitor: 174 | self._monitor.cancel() 175 | self._monitor = None 176 | if self._isWindows: 177 | import subprocess 178 | 179 | subprocess.call(["taskkill", "/F", "/T", "/PID", str(self._proc.pid)]) 180 | else: 181 | with suppress(ProcessLookupError): 182 | self._proc.terminate() 183 | await self._proc.wait() 184 | self._proc = None 185 | 186 | async def monitorAsync(self): 187 | while self._proc: 188 | line = await self._proc.stdout.readline() 189 | if not line: 190 | break 191 | self._logger.log(IBC.IbcLogLevel, line.strip().decode()) 192 | 193 | 194 | @dataclass 195 | class Watchdog: 196 | """ 197 | Start, connect and watch over the TWS or gateway app and try to keep it 198 | up and running. It is intended to be used in an event-driven 199 | application that properly initializes itself upon (re-)connect. 200 | 201 | It is not intended to be used in a notebook or in imperative-style code. 202 | Do not expect Watchdog to magically shield you from reality. Do not use 203 | Watchdog unless you understand what it does and doesn't do. 204 | 205 | Args: 206 | controller (IBC): (required) IBC instance. 207 | ib (IB): (required) IB instance to be used. Do not connect this 208 | instance as Watchdog takes care of that. 209 | host (str): Used for connecting IB instance. 210 | port (int): Used for connecting IB instance. 211 | clientId (int): Used for connecting IB instance. 212 | connectTimeout (float): Used for connecting IB instance. 213 | readonly (bool): Used for connecting IB instance. 214 | appStartupTime (float): Time (in seconds) that the app is given 215 | to start up. Make sure that it is given ample time. 216 | appTimeout (float): Timeout (in seconds) for network traffic idle time. 217 | retryDelay (float): Time (in seconds) to restart app after a 218 | previous failure. 219 | probeContract (Contract): Contract to use for historical data 220 | probe requests (default is EURUSD). 221 | probeTimeout (float); Timeout (in seconds) for the probe request. 222 | 223 | The idea is to wait until there is no traffic coming from the app for 224 | a certain amount of time (the ``appTimeout`` parameter). This triggers 225 | a historical request to be placed just to see if the app is still alive 226 | and well. If yes, then continue, if no then restart the whole app 227 | and reconnect. Restarting will also occur directly on errors 1100 and 100. 228 | 229 | Example usage: 230 | 231 | .. code-block:: python 232 | 233 | def onConnected(): 234 | print(ib.accountValues()) 235 | 236 | ibc = IBC(974, gateway=True, tradingMode='paper') 237 | ib = IB() 238 | ib.connectedEvent += onConnected 239 | watchdog = Watchdog(ibc, ib, port=4002) 240 | watchdog.start() 241 | ib.run() 242 | 243 | Events: 244 | * ``startingEvent`` (watchdog: :class:`.Watchdog`) 245 | * ``startedEvent`` (watchdog: :class:`.Watchdog`) 246 | * ``stoppingEvent`` (watchdog: :class:`.Watchdog`) 247 | * ``stoppedEvent`` (watchdog: :class:`.Watchdog`) 248 | * ``softTimeoutEvent`` (watchdog: :class:`.Watchdog`) 249 | * ``hardTimeoutEvent`` (watchdog: :class:`.Watchdog`) 250 | """ 251 | 252 | events = [ 253 | "startingEvent", 254 | "startedEvent", 255 | "stoppingEvent", 256 | "stoppedEvent", 257 | "softTimeoutEvent", 258 | "hardTimeoutEvent", 259 | ] 260 | 261 | controller: IBC 262 | ib: IB 263 | host: str = "127.0.0.1" 264 | port: int = 7497 265 | clientId: int = 1 266 | connectTimeout: float = 2 267 | appStartupTime: float = 30 268 | appTimeout: float = 20 269 | retryDelay: float = 2 270 | readonly: bool = False 271 | account: str = "" 272 | raiseSyncErrors: bool = False 273 | probeContract: Contract = Forex("EURUSD") 274 | probeTimeout: float = 4 275 | 276 | def __post_init__(self): 277 | self.startingEvent = Event("startingEvent") 278 | self.startedEvent = Event("startedEvent") 279 | self.stoppingEvent = Event("stoppingEvent") 280 | self.stoppedEvent = Event("stoppedEvent") 281 | self.softTimeoutEvent = Event("softTimeoutEvent") 282 | self.hardTimeoutEvent = Event("hardTimeoutEvent") 283 | if not self.controller: 284 | raise ValueError("No controller supplied") 285 | if not self.ib: 286 | raise ValueError("No IB instance supplied") 287 | if self.ib.isConnected(): 288 | raise ValueError("IB instance must not be connected") 289 | self._runner = None 290 | self._logger = logging.getLogger("ib_async.Watchdog") 291 | 292 | def start(self): 293 | self._logger.info("Starting") 294 | self.startingEvent.emit(self) 295 | self._runner = asyncio.ensure_future(self.runAsync()) 296 | return self._runner 297 | 298 | def stop(self): 299 | self._logger.info("Stopping") 300 | self.stoppingEvent.emit(self) 301 | self.ib.disconnect() 302 | self._runner = None 303 | 304 | async def runAsync(self): 305 | def onTimeout(idlePeriod): 306 | if not waiter.done(): 307 | waiter.set_result(None) 308 | 309 | def onError(reqId, errorCode, errorString, contract): 310 | if errorCode in {100, 1100} and not waiter.done(): 311 | waiter.set_exception(Warning(f"Error {errorCode}")) 312 | 313 | def onDisconnected(): 314 | if not waiter.done(): 315 | waiter.set_exception(Warning("Disconnected")) 316 | 317 | while self._runner: 318 | try: 319 | await self.controller.startAsync() 320 | await asyncio.sleep(self.appStartupTime) 321 | await self.ib.connectAsync( 322 | self.host, 323 | self.port, 324 | self.clientId, 325 | self.connectTimeout, 326 | self.readonly, 327 | self.account, 328 | self.raiseSyncErrors, 329 | ) 330 | self.startedEvent.emit(self) 331 | self.ib.setTimeout(self.appTimeout) 332 | self.ib.timeoutEvent += onTimeout 333 | self.ib.errorEvent += onError 334 | self.ib.disconnectedEvent += onDisconnected 335 | 336 | while self._runner: 337 | waiter: asyncio.Future = asyncio.Future() 338 | await waiter 339 | # soft timeout, probe the app with a historical request 340 | self._logger.debug("Soft timeout") 341 | self.softTimeoutEvent.emit(self) 342 | probe = self.ib.reqHistoricalDataAsync( 343 | self.probeContract, "", "30 S", "5 secs", "MIDPOINT", False 344 | ) 345 | bars = None 346 | with suppress(asyncio.TimeoutError): 347 | bars = await asyncio.wait_for(probe, self.probeTimeout) 348 | if not bars: 349 | self.hardTimeoutEvent.emit(self) 350 | raise Warning("Hard timeout") 351 | self.ib.setTimeout(self.appTimeout) 352 | 353 | except ConnectionRefusedError: 354 | pass 355 | except Warning as w: 356 | self._logger.warning(w) 357 | except Exception as e: 358 | self._logger.exception(e) 359 | finally: 360 | self.ib.timeoutEvent -= onTimeout 361 | self.ib.errorEvent -= onError 362 | self.ib.disconnectedEvent -= onDisconnected 363 | await self.controller.terminateAsync() 364 | self.stoppedEvent.emit(self) 365 | if self._runner: 366 | await asyncio.sleep(self.retryDelay) 367 | 368 | 369 | if __name__ == "__main__": 370 | ibc = IBC(1012, gateway=True, tradingMode="paper") 371 | ib = IB() 372 | app = Watchdog(ibc, ib, appStartupTime=15) 373 | app.start() 374 | IB.run() 375 | -------------------------------------------------------------------------------- /notebooks/ordering.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Ordering\n", 8 | "\n", 9 | "\n", 10 | "## Warning: This notebook will place live orders\n", 11 | "\n", 12 | "Use a paper trading account (during market hours).\n" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "metadata": { 19 | "scrolled": true 20 | }, 21 | "outputs": [ 22 | { 23 | "data": { 24 | "text/plain": [ 25 | "" 26 | ] 27 | }, 28 | "execution_count": 1, 29 | "metadata": {}, 30 | "output_type": "execute_result" 31 | } 32 | ], 33 | "source": [ 34 | "from ib_async import *\n", 35 | "\n", 36 | "util.startLoop()\n", 37 | "\n", 38 | "ib = IB()\n", 39 | "ib.connect(\"127.0.0.1\", 7497, clientId=13)\n", 40 | "# util.logToConsole()" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "Create a contract and a market order:" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 2, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "contract = Forex(\"EURUSD\")\n", 57 | "ib.qualifyContracts(contract)\n", 58 | "\n", 59 | "order = LimitOrder(\"SELL\", 20000, 1.11)" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "metadata": {}, 65 | "source": [ 66 | "placeOrder will place the order order and return a ``Trade`` object right away (non-blocking):" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 3, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "trade = ib.placeOrder(contract, order)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "``trade`` contains the order and everything related to it, such as order status, fills and a log.\n", 83 | "It will be live updated with every status change or fill of the order." 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 4, 89 | "metadata": { 90 | "scrolled": true 91 | }, 92 | "outputs": [ 93 | { 94 | "data": { 95 | "text/plain": [ 96 | "[TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 7, 96198, tzinfo=datetime.timezone.utc), status='PendingSubmit', message=''),\n", 97 | " TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 7, 283531, tzinfo=datetime.timezone.utc), status='PreSubmitted', message=''),\n", 98 | " TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 7, 323689, tzinfo=datetime.timezone.utc), status='PreSubmitted', message='Fill 20000.0@1.12245'),\n", 99 | " TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 7, 323689, tzinfo=datetime.timezone.utc), status='Filled', message='')]" 100 | ] 101 | }, 102 | "execution_count": 4, 103 | "metadata": {}, 104 | "output_type": "execute_result" 105 | } 106 | ], 107 | "source": [ 108 | "ib.sleep(1)\n", 109 | "trade.log" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "``trade`` will also available from ``ib.trades()``:" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 5, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "assert trade in ib.trades()" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "Likewise for ``order``:" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 6, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "assert order in ib.orders()" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "metadata": {}, 147 | "source": [ 148 | "Now let's create a limit order with an unrealistic limit:" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 7, 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "data": { 158 | "text/plain": [ 159 | "Trade(contract=Forex('EURUSD', conId=12087792, exchange='IDEALPRO', localSymbol='EUR.USD', tradingClass='EUR.USD'), order=LimitOrder(orderId=23, clientId=13, action='BUY', totalQuantity=20000, lmtPrice=0.05), orderStatus=OrderStatus(orderId=0, status='PendingSubmit', filled=0, remaining=0, avgFillPrice=0.0, permId=0, parentId=0, lastFillPrice=0.0, clientId=0, whyHeld='', mktCapPrice=0.0, lastLiquidity=0), fills=[], log=[TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 8, 149188, tzinfo=datetime.timezone.utc), status='PendingSubmit', message='')])" 160 | ] 161 | }, 162 | "execution_count": 7, 163 | "metadata": {}, 164 | "output_type": "execute_result" 165 | } 166 | ], 167 | "source": [ 168 | "limitOrder = LimitOrder(\"BUY\", 20000, 0.05)\n", 169 | "limitTrade = ib.placeOrder(contract, limitOrder)\n", 170 | "\n", 171 | "limitTrade" 172 | ] 173 | }, 174 | { 175 | "cell_type": "markdown", 176 | "metadata": {}, 177 | "source": [ 178 | "``status`` will change from \"PendingSubmit\" to \"Submitted\":" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 8, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "ib.sleep(1)\n", 188 | "assert limitTrade.orderStatus.status == \"Submitted\"" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": 9, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "assert limitTrade in ib.openTrades()" 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "metadata": {}, 203 | "source": [ 204 | "Let's modify the limit price and resubmit:" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": 10, 210 | "metadata": {}, 211 | "outputs": [ 212 | { 213 | "data": { 214 | "text/plain": [ 215 | "Trade(contract=Forex('EURUSD', conId=12087792, exchange='IDEALPRO', localSymbol='EUR.USD', tradingClass='EUR.USD'), order=LimitOrder(orderId=23, clientId=13, permId=1710981560, action='BUY', totalQuantity=20000.0, lmtPrice=0.1, auxPrice=0.0, tif='DAY', ocaType=3, trailStopPrice=1.05, volatilityType=0, deltaNeutralOrderType='None', referencePriceType=0, account='DU772802', clearingIntent='IB', adjustedOrderType='None', cashQty=0.0, dontUseAutoPriceForHedge=True), orderStatus=OrderStatus(orderId=0, status='Submitted', filled=0.0, remaining=20000.0, avgFillPrice=0.0, permId=1710981560, parentId=0, lastFillPrice=0.0, clientId=13, whyHeld='', mktCapPrice=0.0, lastLiquidity=0), fills=[], log=[TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 8, 149188, tzinfo=datetime.timezone.utc), status='PendingSubmit', message=''), TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 8, 249250, tzinfo=datetime.timezone.utc), status='Submitted', message=''), TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 9, 176924, tzinfo=datetime.timezone.utc), status='Submitted', message='Modify')])" 216 | ] 217 | }, 218 | "execution_count": 10, 219 | "metadata": {}, 220 | "output_type": "execute_result" 221 | } 222 | ], 223 | "source": [ 224 | "limitOrder.lmtPrice = 0.10\n", 225 | "\n", 226 | "ib.placeOrder(contract, limitOrder)" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": {}, 232 | "source": [ 233 | "And now cancel it:" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": 11, 239 | "metadata": {}, 240 | "outputs": [ 241 | { 242 | "data": { 243 | "text/plain": [ 244 | "Trade(contract=Forex('EURUSD', conId=12087792, exchange='IDEALPRO', localSymbol='EUR.USD', tradingClass='EUR.USD'), order=LimitOrder(orderId=23, clientId=13, permId=1710981560, action='BUY', totalQuantity=20000.0, lmtPrice=0.1, auxPrice=0.0, tif='DAY', ocaType=3, trailStopPrice=1.05, volatilityType=0, deltaNeutralOrderType='None', referencePriceType=0, account='DU772802', clearingIntent='IB', adjustedOrderType='None', cashQty=0.0, dontUseAutoPriceForHedge=True), orderStatus=OrderStatus(orderId=0, status='PendingCancel', filled=0.0, remaining=20000.0, avgFillPrice=0.0, permId=1710981560, parentId=0, lastFillPrice=0.0, clientId=13, whyHeld='', mktCapPrice=0.0, lastLiquidity=0), fills=[], log=[TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 8, 149188, tzinfo=datetime.timezone.utc), status='PendingSubmit', message=''), TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 8, 249250, tzinfo=datetime.timezone.utc), status='Submitted', message=''), TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 9, 176924, tzinfo=datetime.timezone.utc), status='Submitted', message='Modify'), TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 9, 183117, tzinfo=datetime.timezone.utc), status='PendingCancel', message='')])" 245 | ] 246 | }, 247 | "execution_count": 11, 248 | "metadata": {}, 249 | "output_type": "execute_result" 250 | } 251 | ], 252 | "source": [ 253 | "ib.cancelOrder(limitOrder)" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": 12, 259 | "metadata": {}, 260 | "outputs": [ 261 | { 262 | "data": { 263 | "text/plain": [ 264 | "[TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 8, 149188, tzinfo=datetime.timezone.utc), status='PendingSubmit', message=''),\n", 265 | " TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 8, 249250, tzinfo=datetime.timezone.utc), status='Submitted', message=''),\n", 266 | " TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 9, 176924, tzinfo=datetime.timezone.utc), status='Submitted', message='Modify'),\n", 267 | " TradeLogEntry(time=datetime.datetime(2019, 12, 31, 17, 19, 9, 183117, tzinfo=datetime.timezone.utc), status='PendingCancel', message='')]" 268 | ] 269 | }, 270 | "execution_count": 12, 271 | "metadata": {}, 272 | "output_type": "execute_result" 273 | } 274 | ], 275 | "source": [ 276 | "limitTrade.log" 277 | ] 278 | }, 279 | { 280 | "cell_type": "markdown", 281 | "metadata": {}, 282 | "source": [ 283 | "placeOrder is not blocking and will not wait on what happens with the order.\n", 284 | "To make the order placement blocking, that is to wait until the order is either\n", 285 | "filled or canceled, consider the following:" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 13, 291 | "metadata": {}, 292 | "outputs": [ 293 | { 294 | "name": "stdout", 295 | "output_type": "stream", 296 | "text": [ 297 | "CPU times: user 5.87 ms, sys: 486 µs, total: 6.35 ms\n", 298 | "Wall time: 163 ms\n" 299 | ] 300 | } 301 | ], 302 | "source": [ 303 | "%%time\n", 304 | "order = MarketOrder(\"BUY\", 100)\n", 305 | "\n", 306 | "trade = ib.placeOrder(contract, order)\n", 307 | "while not trade.isDone():\n", 308 | " ib.waitOnUpdate()" 309 | ] 310 | }, 311 | { 312 | "cell_type": "markdown", 313 | "metadata": {}, 314 | "source": [ 315 | "What are our positions?" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": 14, 321 | "metadata": { 322 | "scrolled": true 323 | }, 324 | "outputs": [ 325 | { 326 | "data": { 327 | "text/plain": [ 328 | "[Position(account='DU772802', contract=Stock(conId=9579970, symbol='IWM', exchange='ARCA', currency='USD', localSymbol='IWM', tradingClass='IWM'), position=600.0, avgCost=149.368),\n", 329 | " Position(account='DU772802', contract=Forex('USDCHF', conId=12087820, localSymbol='USD.CHF', tradingClass='USD.CHF'), position=20.0, avgCost=0.98545),\n", 330 | " Position(account='DU772802', contract=Forex('EURUSD', conId=12087792, localSymbol='EUR.USD', tradingClass='EUR.USD'), position=165700.0, avgCost=1.1247148664589277),\n", 331 | " Position(account='DU772802', contract=Stock(conId=265598, symbol='AAPL', exchange='NASDAQ', currency='USD', localSymbol='AAPL', tradingClass='NMS'), position=2551.0, avgCost=187.1202698)]" 332 | ] 333 | }, 334 | "execution_count": 14, 335 | "metadata": {}, 336 | "output_type": "execute_result" 337 | } 338 | ], 339 | "source": [ 340 | "ib.positions()" 341 | ] 342 | }, 343 | { 344 | "cell_type": "markdown", 345 | "metadata": {}, 346 | "source": [ 347 | "What's the total of commissions paid today?" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 15, 353 | "metadata": {}, 354 | "outputs": [ 355 | { 356 | "data": { 357 | "text/plain": [ 358 | "11.607050000000001" 359 | ] 360 | }, 361 | "execution_count": 15, 362 | "metadata": {}, 363 | "output_type": "execute_result" 364 | } 365 | ], 366 | "source": [ 367 | "sum(fill.commissionReport.commission for fill in ib.fills())" 368 | ] 369 | }, 370 | { 371 | "cell_type": "markdown", 372 | "metadata": {}, 373 | "source": [ 374 | "whatIfOrder can be used to see the commission and the margin impact of an order without actually sending the order:" 375 | ] 376 | }, 377 | { 378 | "cell_type": "code", 379 | "execution_count": 16, 380 | "metadata": {}, 381 | "outputs": [ 382 | { 383 | "data": { 384 | "text/plain": [ 385 | "OrderState(status='PreSubmitted', initMarginBefore='188130.0', maintMarginBefore='188130.0', equityWithLoanBefore='1126836.45', initMarginChange='0.0', maintMarginChange='0.0', equityWithLoanChange='-1.790000000037253', initMarginAfter='188130.0', maintMarginAfter='188130.0', equityWithLoanAfter='1126834.66', commission=1.7857, minCommission=1.7976931348623157e+308, maxCommission=1.7976931348623157e+308, commissionCurrency='EUR', warningText='', completedTime='', completedStatus='')" 386 | ] 387 | }, 388 | "execution_count": 16, 389 | "metadata": {}, 390 | "output_type": "execute_result" 391 | } 392 | ], 393 | "source": [ 394 | "order = MarketOrder(\"SELL\", 20000)\n", 395 | "ib.whatIfOrder(contract, order)" 396 | ] 397 | }, 398 | { 399 | "cell_type": "code", 400 | "execution_count": 17, 401 | "metadata": {}, 402 | "outputs": [], 403 | "source": [ 404 | "ib.disconnect()" 405 | ] 406 | } 407 | ], 408 | "metadata": { 409 | "kernelspec": { 410 | "display_name": "Python 3", 411 | "language": "python", 412 | "name": "python3" 413 | }, 414 | "language_info": { 415 | "codemirror_mode": { 416 | "name": "ipython", 417 | "version": 3 418 | }, 419 | "file_extension": ".py", 420 | "mimetype": "text/x-python", 421 | "name": "python", 422 | "nbconvert_exporter": "python", 423 | "pygments_lexer": "ipython3", 424 | "version": "3.7.5" 425 | } 426 | }, 427 | "nbformat": 4, 428 | "nbformat_minor": 4 429 | } 430 | -------------------------------------------------------------------------------- /ib_async/objects.py: -------------------------------------------------------------------------------- 1 | """Object hierarchy.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass, field 6 | from datetime import date as date_ 7 | from datetime import datetime, timezone, tzinfo 8 | from typing import Any, NamedTuple 9 | 10 | from eventkit import Event 11 | 12 | from .contract import Contract, ScanData, TagValue 13 | from .util import EPOCH, UNSET_DOUBLE, UNSET_INTEGER 14 | 15 | nan = float("nan") 16 | 17 | 18 | @dataclass 19 | class ScannerSubscription: 20 | numberOfRows: int = -1 21 | instrument: str = "" 22 | locationCode: str = "" 23 | scanCode: str = "" 24 | abovePrice: float = UNSET_DOUBLE 25 | belowPrice: float = UNSET_DOUBLE 26 | aboveVolume: int = UNSET_INTEGER 27 | marketCapAbove: float = UNSET_DOUBLE 28 | marketCapBelow: float = UNSET_DOUBLE 29 | moodyRatingAbove: str = "" 30 | moodyRatingBelow: str = "" 31 | spRatingAbove: str = "" 32 | spRatingBelow: str = "" 33 | maturityDateAbove: str = "" 34 | maturityDateBelow: str = "" 35 | couponRateAbove: float = UNSET_DOUBLE 36 | couponRateBelow: float = UNSET_DOUBLE 37 | excludeConvertible: bool = False 38 | averageOptionVolumeAbove: int = UNSET_INTEGER 39 | scannerSettingPairs: str = "" 40 | stockTypeFilter: str = "" 41 | 42 | 43 | @dataclass 44 | class SoftDollarTier: 45 | name: str = "" 46 | val: str = "" 47 | displayName: str = "" 48 | 49 | def __bool__(self): 50 | return bool(self.name or self.val or self.displayName) 51 | 52 | 53 | @dataclass 54 | class Execution: 55 | execId: str = "" 56 | time: datetime = field(default=EPOCH) 57 | acctNumber: str = "" 58 | exchange: str = "" 59 | side: str = "" 60 | shares: float = 0.0 61 | price: float = 0.0 62 | permId: int = 0 63 | clientId: int = 0 64 | orderId: int = 0 65 | liquidation: int = 0 66 | cumQty: float = 0.0 67 | avgPrice: float = 0.0 68 | orderRef: str = "" 69 | evRule: str = "" 70 | evMultiplier: float = 0.0 71 | modelCode: str = "" 72 | lastLiquidity: int = 0 73 | pendingPriceRevision: bool = False 74 | 75 | 76 | @dataclass 77 | class CommissionReport: 78 | execId: str = "" 79 | commission: float = 0.0 80 | currency: str = "" 81 | realizedPNL: float = 0.0 82 | yield_: float = 0.0 83 | yieldRedemptionDate: int = 0 84 | 85 | 86 | @dataclass 87 | class ExecutionFilter: 88 | clientId: int = 0 89 | acctCode: str = "" 90 | time: str = "" 91 | symbol: str = "" 92 | secType: str = "" 93 | exchange: str = "" 94 | side: str = "" 95 | 96 | 97 | @dataclass 98 | class BarData: 99 | date: date_ | datetime = EPOCH 100 | open: float = 0.0 101 | high: float = 0.0 102 | low: float = 0.0 103 | close: float = 0.0 104 | volume: float = 0 105 | average: float = 0.0 106 | barCount: int = 0 107 | 108 | 109 | @dataclass 110 | class RealTimeBar: 111 | time: datetime = EPOCH 112 | endTime: int = -1 113 | open_: float = 0.0 114 | high: float = 0.0 115 | low: float = 0.0 116 | close: float = 0.0 117 | volume: float = 0.0 118 | wap: float = 0.0 119 | count: int = 0 120 | 121 | 122 | @dataclass 123 | class TickAttrib: 124 | canAutoExecute: bool = False 125 | pastLimit: bool = False 126 | preOpen: bool = False 127 | 128 | 129 | @dataclass 130 | class TickAttribBidAsk: 131 | bidPastLow: bool = False 132 | askPastHigh: bool = False 133 | 134 | 135 | @dataclass 136 | class TickAttribLast: 137 | pastLimit: bool = False 138 | unreported: bool = False 139 | 140 | 141 | @dataclass 142 | class HistogramData: 143 | price: float = 0.0 144 | count: int = 0 145 | 146 | 147 | @dataclass 148 | class NewsProvider: 149 | code: str = "" 150 | name: str = "" 151 | 152 | 153 | @dataclass 154 | class DepthMktDataDescription: 155 | exchange: str = "" 156 | secType: str = "" 157 | listingExch: str = "" 158 | serviceDataType: str = "" 159 | aggGroup: int = UNSET_INTEGER 160 | 161 | 162 | @dataclass 163 | class PnL: 164 | account: str = "" 165 | modelCode: str = "" 166 | dailyPnL: float = nan 167 | unrealizedPnL: float = nan 168 | realizedPnL: float = nan 169 | 170 | 171 | @dataclass 172 | class TradeLogEntry: 173 | time: datetime 174 | status: str = "" 175 | message: str = "" 176 | errorCode: int = 0 177 | 178 | 179 | @dataclass 180 | class PnLSingle: 181 | account: str = "" 182 | modelCode: str = "" 183 | conId: int = 0 184 | dailyPnL: float = nan 185 | unrealizedPnL: float = nan 186 | realizedPnL: float = nan 187 | position: int = 0 188 | value: float = nan 189 | 190 | 191 | @dataclass 192 | class HistoricalSession: 193 | startDateTime: str = "" 194 | endDateTime: str = "" 195 | refDate: str = "" 196 | 197 | 198 | @dataclass 199 | class HistoricalSchedule: 200 | startDateTime: str = "" 201 | endDateTime: str = "" 202 | timeZone: str = "" 203 | sessions: list[HistoricalSession] = field(default_factory=list) 204 | 205 | 206 | @dataclass 207 | class WshEventData: 208 | conId: int = UNSET_INTEGER 209 | filter: str = "" 210 | fillWatchlist: bool = False 211 | fillPortfolio: bool = False 212 | fillCompetitors: bool = False 213 | startDate: str = "" 214 | endDate: str = "" 215 | totalLimit: int = UNSET_INTEGER 216 | 217 | 218 | class AccountValue(NamedTuple): 219 | account: str 220 | tag: str 221 | value: str 222 | currency: str 223 | modelCode: str 224 | 225 | 226 | class TickData(NamedTuple): 227 | time: datetime 228 | tickType: int 229 | price: float 230 | size: float 231 | 232 | 233 | class HistoricalTick(NamedTuple): 234 | time: datetime 235 | price: float 236 | size: float 237 | 238 | 239 | class HistoricalTickBidAsk(NamedTuple): 240 | time: datetime 241 | tickAttribBidAsk: TickAttribBidAsk 242 | priceBid: float 243 | priceAsk: float 244 | sizeBid: float 245 | sizeAsk: float 246 | 247 | 248 | class HistoricalTickLast(NamedTuple): 249 | time: datetime 250 | tickAttribLast: TickAttribLast 251 | price: float 252 | size: float 253 | exchange: str 254 | specialConditions: str 255 | 256 | 257 | class TickByTickAllLast(NamedTuple): 258 | tickType: int 259 | time: datetime 260 | price: float 261 | size: float 262 | tickAttribLast: TickAttribLast 263 | exchange: str 264 | specialConditions: str 265 | 266 | 267 | class TickByTickBidAsk(NamedTuple): 268 | time: datetime 269 | bidPrice: float 270 | askPrice: float 271 | bidSize: float 272 | askSize: float 273 | tickAttribBidAsk: TickAttribBidAsk 274 | 275 | 276 | class TickByTickMidPoint(NamedTuple): 277 | time: datetime 278 | midPoint: float 279 | 280 | 281 | class MktDepthData(NamedTuple): 282 | time: datetime 283 | position: int 284 | marketMaker: str 285 | operation: int 286 | side: int 287 | price: float 288 | size: float 289 | 290 | 291 | class DOMLevel(NamedTuple): 292 | price: float 293 | size: float 294 | marketMaker: str 295 | 296 | 297 | class PriceIncrement(NamedTuple): 298 | lowEdge: float 299 | increment: float 300 | 301 | 302 | class PortfolioItem(NamedTuple): 303 | contract: Contract 304 | position: float 305 | marketPrice: float 306 | marketValue: float 307 | averageCost: float 308 | unrealizedPNL: float 309 | realizedPNL: float 310 | account: str 311 | 312 | 313 | class Position(NamedTuple): 314 | account: str 315 | contract: Contract 316 | position: float 317 | avgCost: float 318 | 319 | 320 | class Fill(NamedTuple): 321 | contract: Contract 322 | execution: Execution 323 | commissionReport: CommissionReport 324 | time: datetime 325 | 326 | 327 | @dataclass(slots=True, frozen=True) 328 | class EfpData: 329 | """ 330 | Exchange for Physical (EFP) futures data. 331 | 332 | EFP allows trading a position in a single stock for a position 333 | in the corresponding single stock future. 334 | """ 335 | 336 | # Annualized basis points (financing rate comparable to broker rates) 337 | basisPoints: float 338 | 339 | # Basis points formatted as percentage string 340 | formattedBasisPoints: str 341 | 342 | # The implied Futures price 343 | impliedFuture: float 344 | 345 | # Number of days until the future's last trade date 346 | holdDays: int 347 | 348 | # Expiration date of the single stock future 349 | futureLastTradeDate: str 350 | 351 | # Dividend impact on the annualized basis points interest rate 352 | dividendImpact: float 353 | 354 | # Expected dividends until future expiration 355 | dividendsToLastTradeDate: float 356 | 357 | 358 | @dataclass(slots=True, frozen=True) 359 | class OptionComputation: 360 | tickAttrib: int 361 | impliedVol: float | None = None 362 | delta: float | None = None 363 | optPrice: float | None = None 364 | pvDividend: float | None = None 365 | gamma: float | None = None 366 | vega: float | None = None 367 | theta: float | None = None 368 | undPrice: float | None = None 369 | 370 | def __add__(self, other: OptionComputation) -> OptionComputation: 371 | if not isinstance(other, self.__class__): 372 | raise TypeError(f"Cannot add {type(self)} and {type(other)}") 373 | 374 | return self.__class__( 375 | tickAttrib=0, 376 | impliedVol=(self.impliedVol or 0) + (other.impliedVol or 0), 377 | delta=(self.delta or 0) + (other.delta or 0), 378 | optPrice=(self.optPrice or 0) + (other.optPrice or 0), 379 | gamma=(self.gamma or 0) + (other.gamma or 0), 380 | vega=(self.vega or 0) + (other.vega or 0), 381 | theta=(self.theta or 0) + (other.theta or 0), 382 | undPrice=self.undPrice, 383 | ) 384 | 385 | def __sub__(self, other: OptionComputation) -> OptionComputation: 386 | if not isinstance(other, self.__class__): 387 | raise TypeError(f"Cannot subtract {type(self)} and {type(other)}") 388 | 389 | return self.__class__( 390 | tickAttrib=0, 391 | impliedVol=(self.impliedVol or 0) - (other.impliedVol or 0), 392 | delta=(self.delta or 0) - (other.delta or 0), 393 | optPrice=(self.optPrice or 0) - (other.optPrice or 0), 394 | gamma=(self.gamma or 0) - (other.gamma or 0), 395 | vega=(self.vega or 0) - (other.vega or 0), 396 | theta=(self.theta or 0) - (other.theta or 0), 397 | undPrice=self.undPrice, 398 | ) 399 | 400 | def __mul__(self, other: int | float) -> OptionComputation: 401 | if not isinstance(other, int | float): 402 | raise TypeError(f"Cannot multiply {type(self)} and {type(other)}") 403 | 404 | return self.__class__( 405 | tickAttrib=0, 406 | impliedVol=(self.impliedVol or 0) * other, 407 | delta=(self.delta or 0) * other, 408 | optPrice=(self.optPrice or 0) * other, 409 | gamma=(self.gamma or 0) * other, 410 | vega=(self.vega or 0) * other, 411 | theta=(self.theta or 0) * other, 412 | undPrice=self.undPrice, 413 | ) 414 | 415 | 416 | @dataclass(slots=True, frozen=True) 417 | class OptionChain: 418 | exchange: str 419 | underlyingConId: int 420 | tradingClass: str 421 | multiplier: str 422 | expirations: list[str] 423 | strikes: list[float] 424 | 425 | 426 | @dataclass(slots=True, frozen=True) 427 | class Dividends: 428 | past12Months: float | None 429 | next12Months: float | None 430 | nextDate: date_ | None 431 | nextAmount: float | None 432 | 433 | 434 | @dataclass(slots=True, frozen=True) 435 | class NewsArticle: 436 | articleType: int 437 | articleText: str 438 | 439 | 440 | @dataclass(slots=True, frozen=True) 441 | class HistoricalNews: 442 | time: datetime 443 | providerCode: str 444 | articleId: str 445 | headline: str 446 | 447 | 448 | @dataclass(slots=True, frozen=True) 449 | class NewsTick: 450 | timeStamp: int 451 | providerCode: str 452 | articleId: str 453 | headline: str 454 | extraData: str 455 | 456 | 457 | @dataclass(slots=True, frozen=True) 458 | class NewsBulletin: 459 | msgId: int 460 | msgType: int 461 | message: str 462 | origExchange: str 463 | 464 | 465 | @dataclass(slots=True, frozen=True) 466 | class FamilyCode: 467 | accountID: str 468 | familyCodeStr: str 469 | 470 | 471 | @dataclass(slots=True, frozen=True) 472 | class SmartComponent: 473 | bitNumber: int 474 | exchange: str 475 | exchangeLetter: str 476 | 477 | 478 | @dataclass(slots=True, frozen=True) 479 | class ConnectionStats: 480 | startTime: float 481 | duration: float 482 | numBytesRecv: int 483 | numBytesSent: int 484 | numMsgRecv: int 485 | numMsgSent: int 486 | 487 | 488 | class BarDataList(list[BarData]): 489 | """ 490 | List of :class:`.BarData` that also stores all request parameters. 491 | 492 | Events: 493 | 494 | * ``updateEvent`` 495 | (bars: :class:`.BarDataList`, hasNewBar: bool) 496 | """ 497 | 498 | reqId: int 499 | contract: Contract 500 | endDateTime: datetime | date_ | str | None 501 | durationStr: str 502 | barSizeSetting: str 503 | whatToShow: str 504 | useRTH: bool 505 | formatDate: int 506 | keepUpToDate: bool 507 | chartOptions: list[TagValue] 508 | 509 | def __init__(self, *args): 510 | super().__init__(*args) 511 | self.updateEvent = Event("updateEvent") 512 | 513 | def __eq__(self, other) -> bool: 514 | return self is other 515 | 516 | 517 | class RealTimeBarList(list[RealTimeBar]): 518 | """ 519 | List of :class:`.RealTimeBar` that also stores all request parameters. 520 | 521 | Events: 522 | 523 | * ``updateEvent`` 524 | (bars: :class:`.RealTimeBarList`, hasNewBar: bool) 525 | """ 526 | 527 | reqId: int 528 | contract: Contract 529 | barSize: int 530 | whatToShow: str 531 | useRTH: bool 532 | realTimeBarsOptions: list[TagValue] 533 | 534 | def __init__(self, *args): 535 | super().__init__(*args) 536 | self.updateEvent = Event("updateEvent") 537 | 538 | def __eq__(self, other) -> bool: 539 | return self is other 540 | 541 | 542 | class ScanDataList(list[ScanData]): 543 | """ 544 | List of :class:`.ScanData` that also stores all request parameters. 545 | 546 | Events: 547 | * ``updateEvent`` (:class:`.ScanDataList`) 548 | """ 549 | 550 | reqId: int 551 | subscription: ScannerSubscription 552 | scannerSubscriptionOptions: list[TagValue] 553 | scannerSubscriptionFilterOptions: list[TagValue] 554 | 555 | def __init__(self, *args): 556 | super().__init__(*args) 557 | self.updateEvent = Event("updateEvent") 558 | 559 | def __eq__(self, other): 560 | return self is other 561 | 562 | 563 | class DynamicObject: 564 | def __init__(self, **kwargs): 565 | self.__dict__.update(kwargs) 566 | 567 | def __repr__(self): 568 | clsName = self.__class__.__name__ 569 | kwargs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items()) 570 | return f"{clsName}({kwargs})" 571 | 572 | 573 | class FundamentalRatios(DynamicObject): 574 | """ 575 | See: 576 | https://web.archive.org/web/20200725010343/https://interactivebrokers.github.io/tws-api/fundamental_ratios_tags.html 577 | """ 578 | 579 | pass 580 | 581 | 582 | @dataclass 583 | class IBDefaults: 584 | """A simple way to provide default values when populating API data.""" 585 | 586 | # optionally replace IBKR using -1 price and 0 size when quotes don't exist 587 | emptyPrice: Any = -1 588 | emptySize: Any = 0 589 | 590 | # optionally replace ib_async default for all instance variable values before popualted from API updates 591 | unset: Any = nan 592 | 593 | # optionally change the timezone used for log history events in objects (no impact on orders or data processing) 594 | timezone: tzinfo = timezone.utc 595 | -------------------------------------------------------------------------------- /notebooks/option_chain.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Option chains\n", 8 | "=======" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "metadata": { 15 | "scrolled": true 16 | }, 17 | "outputs": [ 18 | { 19 | "data": { 20 | "text/plain": [ 21 | "" 22 | ] 23 | }, 24 | "execution_count": 1, 25 | "metadata": {}, 26 | "output_type": "execute_result" 27 | } 28 | ], 29 | "source": [ 30 | "from ib_async import *\n", 31 | "\n", 32 | "util.startLoop()\n", 33 | "\n", 34 | "ib = IB()\n", 35 | "ib.connect(\"127.0.0.1\", 7497, clientId=12)" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "Suppose we want to find the options on the SPX, with the following conditions:\n", 43 | "\n", 44 | "* Use the next three monthly expiries;\n", 45 | "* Use strike prices within +- 20 dollar of the current SPX value;\n", 46 | "* Use strike prices that are a multitude of 5 dollar." 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "To get the current market value, first create a contract for the underlyer (the S&P 500 index):" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 2, 59 | "metadata": {}, 60 | "outputs": [ 61 | { 62 | "data": { 63 | "text/plain": [ 64 | "[Index(conId=416904, symbol='SPX', exchange='CBOE', currency='USD', localSymbol='SPX')]" 65 | ] 66 | }, 67 | "execution_count": 2, 68 | "metadata": {}, 69 | "output_type": "execute_result" 70 | } 71 | ], 72 | "source": [ 73 | "spx = Index(\"SPX\", \"CBOE\")\n", 74 | "ib.qualifyContracts(spx)" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "To avoid issues with market data permissions, we'll use delayed data:" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 3, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "ib.reqMarketDataType(4)" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "Then get the ticker. Requesting a ticker can take up to 11 seconds." 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 4, 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "data": { 107 | "text/plain": [ 108 | "Ticker(contract=Index(conId=416904, symbol='SPX', exchange='CBOE', currency='USD', localSymbol='SPX'), time=datetime.datetime(2019, 12, 31, 16, 58, 32, 252008, tzinfo=datetime.timezone.utc), last=3216.19, lastSize=0, high=3225.1, low=3212.03, close=3221.29, ticks=[TickData(time=datetime.datetime(2019, 12, 31, 16, 58, 32, 252008, tzinfo=datetime.timezone.utc), tickType=4, price=3216.19, size=0), TickData(time=datetime.datetime(2019, 12, 31, 16, 58, 32, 252008, tzinfo=datetime.timezone.utc), tickType=6, price=3225.1, size=0), TickData(time=datetime.datetime(2019, 12, 31, 16, 58, 32, 252008, tzinfo=datetime.timezone.utc), tickType=7, price=3212.03, size=0), TickData(time=datetime.datetime(2019, 12, 31, 16, 58, 32, 252008, tzinfo=datetime.timezone.utc), tickType=9, price=3221.29, size=0)])" 109 | ] 110 | }, 111 | "execution_count": 4, 112 | "metadata": {}, 113 | "output_type": "execute_result" 114 | } 115 | ], 116 | "source": [ 117 | "[ticker] = ib.reqTickers(spx)\n", 118 | "ticker" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "Take the current market value of the ticker:" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 5, 131 | "metadata": {}, 132 | "outputs": [ 133 | { 134 | "data": { 135 | "text/plain": [ 136 | "3221.29" 137 | ] 138 | }, 139 | "execution_count": 5, 140 | "metadata": {}, 141 | "output_type": "execute_result" 142 | } 143 | ], 144 | "source": [ 145 | "spxValue = ticker.marketPrice()\n", 146 | "spxValue" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "The following request fetches a list of option chains:" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 6, 159 | "metadata": {}, 160 | "outputs": [ 161 | { 162 | "data": { 163 | "text/html": [ 164 | "
\n", 165 | "\n", 178 | "\n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | "
exchangeunderlyingConIdtradingClassmultiplierexpirationsstrikes
0SMART416904SPX100[20200116, 20200220, 20200319, 20200416, 20200...[100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 650...
1CBOE416904SPXW100[20191231, 20200103, 20200106, 20200108, 20200...[1000.0, 1100.0, 1150.0, 1200.0, 1225.0, 1250....
2SMART416904SPXW100[20191231, 20200103, 20200106, 20200108, 20200...[1000.0, 1100.0, 1150.0, 1200.0, 1225.0, 1250....
3CBOE416904SPX100[20200116, 20200220, 20200319, 20200416, 20200...[100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 650...
\n", 229 | "
" 230 | ], 231 | "text/plain": [ 232 | " exchange underlyingConId tradingClass multiplier \\\n", 233 | "0 SMART 416904 SPX 100 \n", 234 | "1 CBOE 416904 SPXW 100 \n", 235 | "2 SMART 416904 SPXW 100 \n", 236 | "3 CBOE 416904 SPX 100 \n", 237 | "\n", 238 | " expirations \\\n", 239 | "0 [20200116, 20200220, 20200319, 20200416, 20200... \n", 240 | "1 [20191231, 20200103, 20200106, 20200108, 20200... \n", 241 | "2 [20191231, 20200103, 20200106, 20200108, 20200... \n", 242 | "3 [20200116, 20200220, 20200319, 20200416, 20200... \n", 243 | "\n", 244 | " strikes \n", 245 | "0 [100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 650... \n", 246 | "1 [1000.0, 1100.0, 1150.0, 1200.0, 1225.0, 1250.... \n", 247 | "2 [1000.0, 1100.0, 1150.0, 1200.0, 1225.0, 1250.... \n", 248 | "3 [100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 650... " 249 | ] 250 | }, 251 | "execution_count": 6, 252 | "metadata": {}, 253 | "output_type": "execute_result" 254 | } 255 | ], 256 | "source": [ 257 | "chains = ib.reqSecDefOptParams(spx.symbol, \"\", spx.secType, spx.conId)\n", 258 | "\n", 259 | "util.df(chains)" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "metadata": {}, 265 | "source": [ 266 | "These are four option chains that differ in ``exchange`` and ``tradingClass``. The latter is 'SPX' for the monthly and 'SPXW' for the weekly options. Note that the weekly expiries are disjoint from the monthly ones, so when interested in the weekly options the monthly options can be added as well.\n", 267 | "\n", 268 | "In this case we're only interested in the monthly options trading on SMART:" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": 7, 274 | "metadata": {}, 275 | "outputs": [ 276 | { 277 | "data": { 278 | "text/plain": [ 279 | "OptionChain(exchange='SMART', underlyingConId='416904', tradingClass='SPX', multiplier='100', expirations=['20200116', '20200220', '20200319', '20200416', '20200514', '20200618', '20200917', '20201015', '20201119', '20201217', '20210114', '20210318', '20210617', '20211216', '20221215'], strikes=[100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 650.0, 700.0, 750.0, 800.0, 850.0, 900.0, 950.0, 1000.0, 1050.0, 1100.0, 1150.0, 1200.0, 1225.0, 1250.0, 1275.0, 1300.0, 1325.0, 1350.0, 1375.0, 1400.0, 1425.0, 1450.0, 1475.0, 1500.0, 1525.0, 1550.0, 1575.0, 1600.0, 1625.0, 1650.0, 1675.0, 1700.0, 1725.0, 1750.0, 1775.0, 1800.0, 1825.0, 1850.0, 1875.0, 1900.0, 1925.0, 1950.0, 1975.0, 2000.0, 2025.0, 2050.0, 2075.0, 2090.0, 2100.0, 2110.0, 2120.0, 2125.0, 2130.0, 2140.0, 2150.0, 2160.0, 2170.0, 2175.0, 2180.0, 2190.0, 2200.0, 2210.0, 2220.0, 2225.0, 2230.0, 2240.0, 2250.0, 2260.0, 2270.0, 2275.0, 2280.0, 2290.0, 2300.0, 2310.0, 2320.0, 2325.0, 2330.0, 2340.0, 2350.0, 2360.0, 2370.0, 2375.0, 2380.0, 2390.0, 2395.0, 2400.0, 2405.0, 2410.0, 2415.0, 2420.0, 2425.0, 2430.0, 2435.0, 2440.0, 2445.0, 2450.0, 2455.0, 2460.0, 2465.0, 2470.0, 2475.0, 2480.0, 2485.0, 2490.0, 2495.0, 2500.0, 2505.0, 2510.0, 2515.0, 2520.0, 2525.0, 2530.0, 2535.0, 2540.0, 2545.0, 2550.0, 2555.0, 2560.0, 2565.0, 2570.0, 2575.0, 2580.0, 2585.0, 2590.0, 2595.0, 2600.0, 2605.0, 2610.0, 2615.0, 2620.0, 2625.0, 2630.0, 2635.0, 2640.0, 2645.0, 2650.0, 2655.0, 2660.0, 2665.0, 2670.0, 2675.0, 2680.0, 2685.0, 2690.0, 2695.0, 2700.0, 2705.0, 2710.0, 2715.0, 2720.0, 2725.0, 2730.0, 2735.0, 2740.0, 2745.0, 2750.0, 2755.0, 2760.0, 2765.0, 2770.0, 2775.0, 2780.0, 2785.0, 2790.0, 2795.0, 2800.0, 2805.0, 2810.0, 2815.0, 2820.0, 2825.0, 2830.0, 2835.0, 2840.0, 2845.0, 2850.0, 2855.0, 2860.0, 2865.0, 2870.0, 2875.0, 2880.0, 2885.0, 2890.0, 2895.0, 2900.0, 2905.0, 2910.0, 2915.0, 2920.0, 2925.0, 2930.0, 2935.0, 2940.0, 2945.0, 2950.0, 2955.0, 2960.0, 2965.0, 2970.0, 2975.0, 2980.0, 2985.0, 2990.0, 2995.0, 3000.0, 3005.0, 3010.0, 3015.0, 3020.0, 3025.0, 3030.0, 3035.0, 3040.0, 3045.0, 3050.0, 3055.0, 3060.0, 3065.0, 3070.0, 3075.0, 3080.0, 3085.0, 3090.0, 3095.0, 3100.0, 3105.0, 3110.0, 3115.0, 3120.0, 3125.0, 3130.0, 3135.0, 3140.0, 3145.0, 3150.0, 3155.0, 3160.0, 3165.0, 3170.0, 3175.0, 3180.0, 3185.0, 3190.0, 3195.0, 3200.0, 3205.0, 3210.0, 3215.0, 3220.0, 3225.0, 3230.0, 3235.0, 3240.0, 3245.0, 3250.0, 3255.0, 3260.0, 3265.0, 3270.0, 3275.0, 3280.0, 3285.0, 3290.0, 3295.0, 3300.0, 3305.0, 3310.0, 3315.0, 3320.0, 3325.0, 3330.0, 3335.0, 3340.0, 3345.0, 3350.0, 3355.0, 3360.0, 3365.0, 3370.0, 3375.0, 3380.0, 3385.0, 3390.0, 3395.0, 3400.0, 3405.0, 3410.0, 3415.0, 3420.0, 3425.0, 3430.0, 3435.0, 3440.0, 3445.0, 3450.0, 3455.0, 3460.0, 3465.0, 3470.0, 3475.0, 3480.0, 3490.0, 3500.0, 3510.0, 3520.0, 3525.0, 3530.0, 3540.0, 3550.0, 3560.0, 3575.0, 3600.0, 3625.0, 3650.0, 3700.0, 3750.0, 3800.0, 3850.0, 3900.0, 3950.0, 4000.0, 4050.0, 4100.0, 4200.0, 4300.0, 4400.0, 4500.0, 4600.0])" 280 | ] 281 | }, 282 | "execution_count": 7, 283 | "metadata": {}, 284 | "output_type": "execute_result" 285 | } 286 | ], 287 | "source": [ 288 | "chain = next(c for c in chains if c.tradingClass == \"SPX\" and c.exchange == \"SMART\")\n", 289 | "chain" 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "What we have here is the full matrix of expirations x strikes. From this we can build all the option contracts that meet our conditions:" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 8, 302 | "metadata": {}, 303 | "outputs": [ 304 | { 305 | "data": { 306 | "text/plain": [ 307 | "48" 308 | ] 309 | }, 310 | "execution_count": 8, 311 | "metadata": {}, 312 | "output_type": "execute_result" 313 | } 314 | ], 315 | "source": [ 316 | "strikes = [\n", 317 | " strike\n", 318 | " for strike in chain.strikes\n", 319 | " if strike % 5 == 0 and spxValue - 20 < strike < spxValue + 20\n", 320 | "]\n", 321 | "expirations = sorted(exp for exp in chain.expirations)[:3]\n", 322 | "rights = [\"P\", \"C\"]\n", 323 | "\n", 324 | "contracts = [\n", 325 | " Option(\"SPX\", expiration, strike, right, \"SMART\", tradingClass=\"SPX\")\n", 326 | " for right in rights\n", 327 | " for expiration in expirations\n", 328 | " for strike in strikes\n", 329 | "]\n", 330 | "\n", 331 | "contracts = ib.qualifyContracts(*contracts)\n", 332 | "len(contracts)" 333 | ] 334 | }, 335 | { 336 | "cell_type": "code", 337 | "execution_count": 9, 338 | "metadata": {}, 339 | "outputs": [ 340 | { 341 | "data": { 342 | "text/plain": [ 343 | "Option(conId=384688665, symbol='SPX', lastTradeDateOrContractMonth='20200116', strike=3205.0, right='P', multiplier='100', exchange='SMART', currency='USD', localSymbol='SPX 200117P03205000', tradingClass='SPX')" 344 | ] 345 | }, 346 | "execution_count": 9, 347 | "metadata": {}, 348 | "output_type": "execute_result" 349 | } 350 | ], 351 | "source": [ 352 | "contracts[0]" 353 | ] 354 | }, 355 | { 356 | "cell_type": "markdown", 357 | "metadata": {}, 358 | "source": [ 359 | "Now to get the market data for all options in one go:" 360 | ] 361 | }, 362 | { 363 | "cell_type": "code", 364 | "execution_count": 10, 365 | "metadata": {}, 366 | "outputs": [ 367 | { 368 | "data": { 369 | "text/plain": [ 370 | "Ticker(contract=Option(conId=384688665, symbol='SPX', lastTradeDateOrContractMonth='20200116', strike=3205.0, right='P', multiplier='100', exchange='SMART', currency='USD', localSymbol='SPX 200117P03205000', tradingClass='SPX'), time=datetime.datetime(2019, 12, 31, 16, 58, 41, 258764, tzinfo=datetime.timezone.utc), bid=28.1, bidSize=260, ask=28.5, askSize=75, last=26.2, lastSize=1, volume=17, high=29.15, low=26.2, close=26.65, bidGreeks=OptionComputation(impliedVol=0.12479434613887233, delta=-0.4467205699460659, optPrice=28.100000381469727, pvDividend=3.171605793435112, gamma=0.004541203481480199, vega=2.7584286390644355, theta=-0.9351184483257037, undPrice=3216.19), askGreeks=OptionComputation(impliedVol=0.1273924345982469, delta=-0.44758003325183715, optPrice=28.5, pvDividend=3.171605793435112, gamma=0.004449873325559049, vega=2.759225320523277, theta=-0.956080972911877, undPrice=3216.19), lastGreeks=OptionComputation(impliedVol=0.1121163044425008, delta=-0.441885782487882, optPrice=26.200000762939453, pvDividend=3.171605793435112, gamma=0.005046062232473765, vega=2.7537040141291262, theta=-0.8328175501618498, undPrice=3216.19), modelGreeks=OptionComputation(impliedVol=0.12597515955300287, delta=-0.44711612427890945, optPrice=30.020053965440823, pvDividend=3.171605793435112, gamma=0.004499237656712854, vega=2.7587969178159018, theta=-0.9446458451581966, undPrice=3216.19))" 371 | ] 372 | }, 373 | "execution_count": 10, 374 | "metadata": {}, 375 | "output_type": "execute_result" 376 | } 377 | ], 378 | "source": [ 379 | "tickers = ib.reqTickers(*contracts)\n", 380 | "\n", 381 | "tickers[0]" 382 | ] 383 | }, 384 | { 385 | "cell_type": "markdown", 386 | "metadata": {}, 387 | "source": [ 388 | "The option greeks are available from the ``modelGreeks`` attribute, and if there is a bid, ask resp. last price available also from ``bidGreeks``, ``askGreeks`` and ``lastGreeks``. For streaming ticks the greek values will be kept up to date to the current market situation." 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": 11, 394 | "metadata": {}, 395 | "outputs": [], 396 | "source": [ 397 | "ib.disconnect()" 398 | ] 399 | } 400 | ], 401 | "metadata": { 402 | "kernelspec": { 403 | "display_name": "Python 3", 404 | "language": "python", 405 | "name": "python3" 406 | }, 407 | "language_info": { 408 | "codemirror_mode": { 409 | "name": "ipython", 410 | "version": 3 411 | }, 412 | "file_extension": ".py", 413 | "mimetype": "text/x-python", 414 | "name": "python", 415 | "nbconvert_exporter": "python", 416 | "pygments_lexer": "ipython3", 417 | "version": "3.7.5" 418 | } 419 | }, 420 | "nbformat": 4, 421 | "nbformat_minor": 4 422 | } 423 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/ib-api-reloaded/ib_async/actions/workflows/test.yml/badge.svg?branch=next)](https://github.com/ib-api-reloaded/ib_async/actions) [![PyVersion](https://img.shields.io/badge/python-3.10+-blue.svg)](#) [![PyPiVersion](https://img.shields.io/pypi/v/ib_async.svg)](https://pypi.python.org/pypi/ib_async) [![License](https://img.shields.io/badge/license-BSD-blue.svg)](#) [![Docs](https://img.shields.io/badge/Documentation-green.svg)](https://ib-api-reloaded.github.io/ib_async/) 2 | 3 | # ib_async 4 | 5 | ## Update 6 | 7 | ## Introduction 8 | 9 | `ib_async` is a Python library that provides a clean, modern interface to Interactive Brokers' Trader Workstation (TWS) and IB Gateway. It handles the complexities of the [IBKR API](https://ibkrcampus.com/ibkr-api-page/twsapi-doc/) so you can focus on building trading applications, research tools, and market data analysis. 10 | 11 | ### What You Can Build 12 | 13 | * **Market Data Applications**: Stream live quotes, historical data, and market depth 14 | * **Trading Systems**: Place, modify, and monitor orders programmatically 15 | * **Portfolio Tools**: Track positions, account balances, and P&L in real-time 16 | * **Research Platforms**: Analyze contract details, option chains, and fundamental data 17 | * **Risk Management**: Monitor exposures and implement automated controls 18 | 19 | ### Key Features 20 | 21 | * **Simple and Intuitive**: Write straightforward Python code without dealing with callback complexity 22 | * **Automatic Synchronization**: The [IB component](https://ib-api-reloaded.github.io/ib_async/api.html#module-ib_async.ib) stays in sync with TWS/Gateway automatically 23 | * **Async-Ready**: Built on [asyncio](https://docs.python.org/3/library/asyncio.html) and [eventkit](https://github.com/erdewit/eventkit) for high-performance applications 24 | * **Jupyter-Friendly**: Interactive development with live data in notebooks 25 | * **Production-Ready**: Robust error handling, reconnection logic, and comprehensive logging 26 | 27 | Be sure to take a look at the 28 | [notebooks](https://ib-api-reloaded.github.io/ib_async/notebooks.html), 29 | the [recipes](https://ib-api-reloaded.github.io/ib_async/recipes.html) 30 | and the [API docs](https://ib-api-reloaded.github.io/ib_async/api.html). 31 | 32 | 33 | ## Installation 34 | 35 | ``` 36 | pip install ib_async 37 | ``` 38 | 39 | Requirements: 40 | 41 | - Python 3.10 or higher 42 | - We plan to support Python releases [2 years back](https://devguide.python.org/versions/) which allows us to continue adding newer features and performance improvements over time. 43 | - A running IB Gateway application (or TWS with API mode enabled) 44 | - [stable gateway](https://www.interactivebrokers.com/en/trading/ibgateway-stable.php) — updated every few months 45 | - [latest gateway](https://www.interactivebrokers.com/en/trading/ibgateway-latest.php) — updated weekly 46 | - Make sure the [API port is enabled](https://ibkrcampus.com/ibkr-api-page/twsapi-doc/#tws-download) and 'Download open orders on connection' is checked. 47 | - You may also want to increase the Java memory usage under `Configure->Settings->Memory Allocation` to 4096 MB minimum to prevent gateway crashes when loading bulk data. 48 | 49 | The ibapi package from IB is not needed. `ib_async` implements the full IBKR API binary protocol internally. 50 | 51 | ## Build Manually 52 | 53 | First, install poetry: 54 | 55 | ``` 56 | pip install poetry -U 57 | ``` 58 | 59 | ### Installing Only Library 60 | 61 | ``` 62 | poetry install 63 | ``` 64 | 65 | ### Install Everything (enable docs + dev testing) 66 | 67 | ``` 68 | poetry install --with=docs,dev 69 | ``` 70 | 71 | ## Generate Docs 72 | 73 | ``` 74 | poetry install --with=docs 75 | poetry run sphinx-build -b html docs html 76 | ``` 77 | 78 | ## Check Types 79 | 80 | ``` 81 | poetry run mypy ib_async 82 | ``` 83 | 84 | ## Build Package 85 | 86 | ``` 87 | poetry build 88 | ``` 89 | 90 | ## Upload Package (if maintaining) 91 | 92 | ``` 93 | poetry install 94 | poetry config pypi-token.pypi your-api-token 95 | poetry publish --build 96 | ``` 97 | 98 | ## Setup Interactive Brokers 99 | 100 | ### 1. Download IB Gateway or TWS 101 | 102 | - [IB Gateway (Stable)](https://www.interactivebrokers.com/en/trading/ibgateway-stable.php) — Updated every few months, more stable 103 | - [IB Gateway (Latest)](https://www.interactivebrokers.com/en/trading/ibgateway-latest.php) — Updated weekly, newest features 104 | - [Trader Workstation (TWS)](https://www.interactivebrokers.com/en/trading/tws.php) — Full trading platform 105 | 106 | ### 2. Configure API Access 107 | 108 | 1. **Enable API**: Go to `Configure → API → Settings` and check "Enable ActiveX and Socket Clients" 109 | 2. **Set Port**: Default ports are 7497 (TWS) and 4001 (Gateway). You can change these if needed. 110 | 3. **Allow Connections**: Add `127.0.0.1` to "Trusted IPs" if connecting locally 111 | 4. **Download Orders**: Check "Download open orders on connection" to see existing orders 112 | 113 | ### 3. Performance Settings 114 | 115 | - **Memory**: Go to `Configure → Settings → Memory Allocation` and set to 4096 MB minimum to prevent crashes with bulk data 116 | - **Timeouts**: Increase API timeout settings if you experience disconnections during large data requests 117 | 118 | ### 4. Common Connection Issues 119 | 120 | **Connection Refused** 121 | ```python 122 | # Make sure TWS/Gateway is running and API is enabled 123 | # Check that ports match (7497 for TWS, 4001 for Gateway) 124 | ib.connect('127.0.0.1', 7497, clientId=1) # TWS 125 | ib.connect('127.0.0.1', 4001, clientId=1) # Gateway 126 | ``` 127 | 128 | **Client ID Conflicts** 129 | ```python 130 | # Each connection needs a unique client ID 131 | ib.connect('127.0.0.1', 7497, clientId=1) # Use different numbers for multiple connections 132 | ``` 133 | 134 | **Market Data Issues** 135 | ```python 136 | # For free delayed data (no subscription required) 137 | ib.reqMarketDataType(3) # Delayed 138 | ib.reqMarketDataType(4) # Delayed frozen 139 | 140 | # For real-time data (requires subscription) 141 | ib.reqMarketDataType(1) # Real-time 142 | ``` 143 | 144 | ## Connection Patterns 145 | 146 | ### Basic Script Usage 147 | ```python 148 | from ib_async import * 149 | 150 | ib = IB() 151 | ib.connect('127.0.0.1', 7497, clientId=1) 152 | # Your code here 153 | ib.disconnect() 154 | ``` 155 | 156 | ### Jupyter Notebook Usage 157 | ```python 158 | from ib_async import * 159 | util.startLoop() # Required for notebooks 160 | 161 | ib = IB() 162 | ib.connect('127.0.0.1', 7497, clientId=1) 163 | # Your code here - no need to call ib.run() 164 | ``` 165 | 166 | ### Async Application 167 | ```python 168 | import asyncio 169 | from ib_async import * 170 | 171 | async def main(): 172 | ib = IB() 173 | await ib.connectAsync('127.0.0.1', 7497, clientId=1) 174 | # Your async code here 175 | ib.disconnect() 176 | 177 | asyncio.run(main()) 178 | ``` 179 | 180 | ## Quick Start 181 | 182 | ### Basic Connection 183 | 184 | ```python 185 | from ib_async import * 186 | 187 | # Connect to TWS or IB Gateway 188 | ib = IB() 189 | ib.connect('127.0.0.1', 7497, clientId=1) 190 | print("Connected") 191 | 192 | # Disconnect when done 193 | ib.disconnect() 194 | ``` 195 | 196 | ### Get Account Information 197 | 198 | ```python 199 | from ib_async import * 200 | 201 | ib = IB() 202 | ib.connect('127.0.0.1', 7497, clientId=1) 203 | 204 | # Get account summary 205 | account = ib.managedAccounts()[0] 206 | summary = ib.accountSummary(account) 207 | for item in summary: 208 | print(f"{item.tag}: {item.value}") 209 | 210 | ib.disconnect() 211 | ``` 212 | 213 | ### Historical Data 214 | 215 | ```python 216 | from ib_async import * 217 | # util.startLoop() # uncomment this line when in a notebook 218 | 219 | ib = IB() 220 | ib.connect('127.0.0.1', 7497, clientId=1) 221 | 222 | # Request historical data 223 | contract = Forex('EURUSD') 224 | bars = ib.reqHistoricalData( 225 | contract, endDateTime='', durationStr='30 D', 226 | barSizeSetting='1 hour', whatToShow='MIDPOINT', useRTH=True) 227 | 228 | # Convert to pandas dataframe (pandas needs to be installed): 229 | df = util.df(bars) 230 | print(df.head()) 231 | 232 | ib.disconnect() 233 | ``` 234 | 235 | ### Live Market Data 236 | 237 | ```python 238 | from ib_async import * 239 | import time 240 | 241 | ib = IB() 242 | ib.connect('127.0.0.1', 7497, clientId=1) 243 | 244 | # Subscribe to live market data 245 | contract = Stock('AAPL', 'SMART', 'USD') 246 | ticker = ib.reqMktData(contract, '', False, False) 247 | 248 | # Print live quotes for 30 seconds 249 | for i in range(30): 250 | ib.sleep(1) # Wait 1 second 251 | if ticker.last: 252 | print(f"AAPL: ${ticker.last} (bid: ${ticker.bid}, ask: ${ticker.ask})") 253 | 254 | ib.disconnect() 255 | ``` 256 | 257 | ### Place a Simple Order 258 | 259 | ```python 260 | from ib_async import * 261 | 262 | ib = IB() 263 | ib.connect('127.0.0.1', 7497, clientId=1) 264 | 265 | # Create a contract and order 266 | contract = Stock('AAPL', 'SMART', 'USD') 267 | order = MarketOrder('BUY', 100) 268 | 269 | # Place the order 270 | trade = ib.placeOrder(contract, order) 271 | print(f"Order placed: {trade}") 272 | 273 | # Monitor order status 274 | while not trade.isDone(): 275 | ib.sleep(1) 276 | print(f"Order status: {trade.orderStatus.status}") 277 | 278 | ib.disconnect() 279 | ``` 280 | 281 | ## More Complete Examples 282 | 283 | ### Portfolio Monitoring 284 | 285 | ```python 286 | from ib_async import * 287 | 288 | ib = IB() 289 | ib.connect('127.0.0.1', 7497, clientId=1) 290 | 291 | # Get current positions 292 | positions = ib.positions() 293 | print("Current Positions:") 294 | for pos in positions: 295 | print(f"{pos.contract.symbol}: {pos.position} @ {pos.avgCost}") 296 | 297 | # Get open orders 298 | orders = ib.openTrades() 299 | print(f"\nOpen Orders: {len(orders)}") 300 | for trade in orders: 301 | print(f"{trade.contract.symbol}: {trade.order.action} {trade.order.totalQuantity}") 302 | 303 | ib.disconnect() 304 | ``` 305 | 306 | ### Real-time P&L Tracking 307 | 308 | ```python 309 | from ib_async import * 310 | 311 | def onPnL(pnl): 312 | print(f"P&L Update: Unrealized: ${pnl.unrealizedPnL:.2f}, Realized: ${pnl.realizedPnL:.2f}") 313 | 314 | ib = IB() 315 | ib.connect('127.0.0.1', 7497, clientId=1) 316 | 317 | # Subscribe to P&L updates 318 | account = ib.managedAccounts()[0] 319 | pnl = ib.reqPnL(account) 320 | pnl.updateEvent += onPnL 321 | 322 | # Keep running to receive updates 323 | try: 324 | ib.run() # Run until interrupted 325 | except KeyboardInterrupt: 326 | ib.disconnect() 327 | ``` 328 | 329 | ### Advanced Order Management 330 | 331 | ```python 332 | from ib_async import * 333 | 334 | ib = IB() 335 | ib.connect('127.0.0.1', 7497, clientId=1) 336 | 337 | # Create a bracket order (entry + stop loss + take profit) 338 | contract = Stock('TSLA', 'SMART', 'USD') 339 | 340 | # Parent order 341 | parent = LimitOrder('BUY', 100, 250.00) 342 | parent.orderId = ib.client.getReqId() 343 | parent.transmit = False 344 | 345 | # Stop loss 346 | stopLoss = StopOrder('SELL', 100, 240.00) 347 | stopLoss.orderId = ib.client.getReqId() 348 | stopLoss.parentId = parent.orderId 349 | stopLoss.transmit = False 350 | 351 | # Take profit 352 | takeProfit = LimitOrder('SELL', 100, 260.00) 353 | takeProfit.orderId = ib.client.getReqId() 354 | takeProfit.parentId = parent.orderId 355 | takeProfit.transmit = True 356 | 357 | # Place bracket order 358 | trades = [] 359 | trades.append(ib.placeOrder(contract, parent)) 360 | trades.append(ib.placeOrder(contract, stopLoss)) 361 | trades.append(ib.placeOrder(contract, takeProfit)) 362 | 363 | print(f"Bracket order placed: {len(trades)} orders") 364 | ib.disconnect() 365 | ``` 366 | 367 | ### Historical Data Analysis 368 | 369 | ```python 370 | from ib_async import * 371 | import pandas as pd 372 | 373 | ib = IB() 374 | ib.connect('127.0.0.1', 7497, clientId=1) 375 | 376 | # Get multiple timeframes 377 | contract = Stock('SPY', 'SMART', 'USD') 378 | 379 | # Daily bars for 1 year 380 | daily_bars = ib.reqHistoricalData( 381 | contract, endDateTime='', durationStr='1 Y', 382 | barSizeSetting='1 day', whatToShow='TRADES', useRTH=True) 383 | 384 | # 5-minute bars for last 5 days 385 | intraday_bars = ib.reqHistoricalData( 386 | contract, endDateTime='', durationStr='5 D', 387 | barSizeSetting='5 mins', whatToShow='TRADES', useRTH=True) 388 | 389 | # Convert to DataFrames 390 | daily_df = util.df(daily_bars) 391 | intraday_df = util.df(intraday_bars) 392 | 393 | print(f"Daily bars: {len(daily_df)} rows") 394 | print(f"Intraday bars: {len(intraday_df)} rows") 395 | 396 | # Calculate simple moving average 397 | daily_df['SMA_20'] = daily_df['close'].rolling(20).mean() 398 | print(daily_df[['date', 'close', 'SMA_20']].tail()) 399 | 400 | ib.disconnect() 401 | ``` 402 | 403 | ## Library Structure 404 | 405 | ### Core Components 406 | 407 | **`ib_async.ib.IB`** - Main interface class 408 | - Connection management (`connect()`, `disconnect()`, `connectAsync()`) 409 | - Market data requests (`reqMktData()`, `reqHistoricalData()`) 410 | - Order management (`placeOrder()`, `cancelOrder()`) 411 | - Account data (`positions()`, `accountSummary()`, `reqPnL()`) 412 | 413 | **`ib_async.contract`** - Financial instruments 414 | - `Stock`, `Option`, `Future`, `Forex`, `Index`, `Bond` 415 | - `Contract` - Base class for all instruments 416 | - `ComboLeg`, `DeltaNeutralContract` - Complex instruments 417 | 418 | **`ib_async.order`** - Order types and management 419 | - `MarketOrder`, `LimitOrder`, `StopOrder`, `StopLimitOrder` 420 | - `Order` - Base order class with all parameters 421 | - `OrderStatus`, `OrderState` - Order execution tracking 422 | - `Trade` - Complete order lifecycle tracking 423 | 424 | **`ib_async.ticker`** - Real-time market data 425 | - `Ticker` - Live quotes, trades, and market data 426 | - Automatic field updates (bid, ask, last, volume, etc.) 427 | - Event-driven updates via `updateEvent` 428 | 429 | **`ib_async.objects`** - Data structures 430 | - `BarData` - Historical price bars 431 | - `Position` - Portfolio positions 432 | - `PortfolioItem` - Portfolio details with P&L 433 | - `AccountValue` - Account metrics 434 | 435 | ### Key Patterns 436 | 437 | **Synchronous vs Asynchronous** 438 | ```python 439 | # Synchronous (blocks until complete) 440 | bars = ib.reqHistoricalData(contract, ...) 441 | 442 | # Asynchronous (yields to event loop) 443 | bars = await ib.reqHistoricalDataAsync(contract, ...) 444 | ``` 445 | 446 | **Event Handling** 447 | ```python 448 | # Subscribe to events 449 | def onOrderUpdate(trade): 450 | print(f"Order update: {trade.orderStatus.status}") 451 | 452 | ib.orderStatusEvent += onOrderUpdate 453 | 454 | # Or with async 455 | async def onTicker(ticker): 456 | print(f"Price update: {ticker.last}") 457 | 458 | ticker.updateEvent += onTicker 459 | ``` 460 | 461 | **Error Handling** 462 | ```python 463 | try: 464 | ib.connect('127.0.0.1', 7497, clientId=1) 465 | except ConnectionRefusedError: 466 | print("TWS/Gateway not running or API not enabled") 467 | except Exception as e: 468 | print(f"Connection error: {e}") 469 | ``` 470 | 471 | ## Documentation 472 | 473 | The complete [API documentation](https://ib-api-reloaded.github.io/ib_async/api.html). 474 | 475 | [Changelog](https://ib-api-reloaded.github.io/ib_async/changelog.html). 476 | 477 | ## Development 478 | 479 | ### Running Tests 480 | 481 | ```bash 482 | poetry install --with=dev 483 | poetry run pytest 484 | ``` 485 | 486 | ### Type Checking 487 | 488 | ```bash 489 | poetry run mypy ib_async 490 | ``` 491 | 492 | ### Code Formatting 493 | 494 | ```bash 495 | poetry run ruff format 496 | poetry run ruff check --fix 497 | ``` 498 | 499 | ### Local Development 500 | 501 | 1. Clone the repository: 502 | ```bash 503 | git clone https://github.com/ib-api-reloaded/ib_async.git 504 | cd ib_async 505 | ``` 506 | 507 | 2. Install dependencies: 508 | ```bash 509 | poetry install --with=dev,docs 510 | ``` 511 | 512 | 3. Make your changes and run tests: 513 | ```bash 514 | poetry run pytest 515 | poetry run mypy ib_async 516 | ``` 517 | 518 | 4. Submit a pull request with: 519 | - Clear description of changes 520 | - Tests for new functionality 521 | - Updated documentation if needed 522 | 523 | ### Contributing Guidelines 524 | 525 | - Follow existing code style (enforced by ruff) 526 | - Add tests for new features 527 | - Update documentation for user-facing changes 528 | - Keep commits focused and well-described 529 | - Be responsive to code review feedback 530 | 531 | ## Community Resources 532 | 533 | If you have other public work related to `ib_async` or `ib_insync` open an issue and we can keep an active list here. 534 | 535 | Projects below are not endorsed by any entity and are purely for reference or entertainment purposes. 536 | 537 | - Adi's livestream VODs about using IBKR APIs: [Interactive Brokers API in Python](https://www.youtube.com/playlist?list=PLCZZtBmmgxn8CFKysCkcl-B1tqRgCCNIX) 538 | - Matt's IBKR python CLI: [icli](http://github.com/mattsta/icli) 539 | - Corporate data parsing via IBKR API: [ib_fundamental](https://github.com/quantbelt/ib_fundamental) 540 | 541 | ## Disclaimer 542 | 543 | The software is provided on the conditions of the simplified BSD license. 544 | 545 | This project is not affiliated with Interactive Brokers Group, Inc. 546 | 547 | [Official Interactive Brokers API Docs](https://ibkrcampus.com/ibkr-api-page/twsapi-doc/) 548 | 549 | ## History 550 | 551 | This library was originally created by [Ewald de Wit](https://github.com/erdewit) as [`tws_async` in early-2017](https://github.com/erdewit/tws_async) then became the more prominent [`ib_insync` library in mid-2017](https://github.com/erdewit/ib_insync). He maintained and improved the library for the world to use for free until his unexpected passing in early 2024. Afterward, we decided to rename the project to `ib_async` under a new github organization since we lost access to modify anything in the original repos and packaging and docs infrastructure. 552 | 553 | The library is currently maintained by [Matt Stancliff](https://github.com/mattsta) and we are open to adding more committers and org contributors if people show interest in helping out. 554 | -------------------------------------------------------------------------------- /ib_async/ticker.py: -------------------------------------------------------------------------------- 1 | """Access to realtime market information.""" 2 | 3 | from dataclasses import dataclass, field 4 | from datetime import datetime 5 | from typing import ClassVar 6 | 7 | from eventkit import Event, Op 8 | 9 | from ib_async.contract import Contract 10 | from ib_async.objects import ( 11 | Dividends, 12 | DOMLevel, 13 | EfpData, 14 | FundamentalRatios, 15 | IBDefaults, 16 | MktDepthData, 17 | OptionComputation, 18 | TickByTickAllLast, 19 | TickByTickBidAsk, 20 | TickByTickMidPoint, 21 | TickData, 22 | ) 23 | from ib_async.util import dataclassRepr, isNan 24 | 25 | nan = float("nan") 26 | 27 | 28 | @dataclass 29 | class Ticker: 30 | """ 31 | Current market data such as bid, ask, last price, etc. for a contract. 32 | 33 | Streaming level-1 ticks of type :class:`.TickData` are stored in 34 | the ``ticks`` list. 35 | 36 | Streaming level-2 ticks of type :class:`.MktDepthData` are stored in the 37 | ``domTicks`` list. The order book (DOM) is available as lists of 38 | :class:`.DOMLevel` in ``domBids`` and ``domAsks``. 39 | 40 | Streaming tick-by-tick ticks are stored in ``tickByTicks``. 41 | 42 | For options the :class:`.OptionComputation` values for the bid, ask, resp. 43 | last price are stored in the ``bidGreeks``, ``askGreeks`` resp. 44 | ``lastGreeks`` attributes. There is also ``modelGreeks`` that conveys 45 | the greeks as calculated by Interactive Brokers' option model. 46 | 47 | Events: 48 | * ``updateEvent`` (ticker: :class:`.Ticker`) 49 | """ 50 | 51 | events: ClassVar = ("updateEvent",) 52 | 53 | contract: Contract | None = None 54 | time: datetime | None = None 55 | timestamp: float | None = None 56 | marketDataType: int = 1 57 | minTick: float = nan 58 | bid: float = nan 59 | bidSize: float = nan 60 | bidExchange: str = "" 61 | ask: float = nan 62 | askSize: float = nan 63 | askExchange: str = "" 64 | last: float = nan 65 | lastSize: float = nan 66 | lastExchange: str = "" 67 | lastTimestamp: datetime | None = None 68 | prevBid: float = nan 69 | prevBidSize: float = nan 70 | prevAsk: float = nan 71 | prevAskSize: float = nan 72 | prevLast: float = nan 73 | prevLastSize: float = nan 74 | volume: float = nan 75 | open: float = nan 76 | high: float = nan 77 | low: float = nan 78 | close: float = nan 79 | vwap: float = nan 80 | low13week: float = nan 81 | high13week: float = nan 82 | low26week: float = nan 83 | high26week: float = nan 84 | low52week: float = nan 85 | high52week: float = nan 86 | bidYield: float = nan 87 | askYield: float = nan 88 | lastYield: float = nan 89 | markPrice: float = nan 90 | halted: float = nan 91 | rtHistVolatility: float = nan 92 | rtVolume: float = nan 93 | rtTradeVolume: float = nan 94 | rtTime: datetime | None = None 95 | avVolume: float = nan 96 | tradeCount: float = nan 97 | tradeRate: float = nan 98 | volumeRate: float = nan 99 | volumeRate3Min: float = nan 100 | volumeRate5Min: float = nan 101 | volumeRate10Min: float = nan 102 | shortable: float = nan 103 | shortableShares: float = nan 104 | indexFuturePremium: float = nan 105 | futuresOpenInterest: float = nan 106 | putOpenInterest: float = nan 107 | callOpenInterest: float = nan 108 | putVolume: float = nan 109 | callVolume: float = nan 110 | avOptionVolume: float = nan 111 | histVolatility: float = nan 112 | impliedVolatility: float = nan 113 | openInterest: float = nan 114 | lastRthTrade: float = nan 115 | lastRegTime: str = "" 116 | optionBidExch: str = "" 117 | optionAskExch: str = "" 118 | bondFactorMultiplier: float = nan 119 | creditmanMarkPrice: float = nan 120 | creditmanSlowMarkPrice: float = nan 121 | delayedLastTimestamp: datetime | None = None 122 | delayedHalted: float = nan 123 | reutersMutualFunds: str = "" 124 | etfNavClose: float = nan 125 | etfNavPriorClose: float = nan 126 | etfNavBid: float = nan 127 | etfNavAsk: float = nan 128 | etfNavLast: float = nan 129 | etfFrozenNavLast: float = nan 130 | etfNavHigh: float = nan 131 | etfNavLow: float = nan 132 | socialMarketAnalytics: str = "" 133 | estimatedIpoMidpoint: float = nan 134 | finalIpoLast: float = nan 135 | dividends: Dividends | None = None 136 | fundamentalRatios: FundamentalRatios | None = None 137 | ticks: list[TickData] = field(default_factory=list) 138 | tickByTicks: list[TickByTickAllLast | TickByTickBidAsk | TickByTickMidPoint] = ( 139 | field(default_factory=list) 140 | ) 141 | domBids: list[DOMLevel] = field(default_factory=list) 142 | domBidsDict: dict[int, DOMLevel] = field(default_factory=dict) 143 | domAsks: list[DOMLevel] = field(default_factory=list) 144 | domAsksDict: dict[int, DOMLevel] = field(default_factory=dict) 145 | domTicks: list[MktDepthData] = field(default_factory=list) 146 | bidGreeks: OptionComputation | None = None 147 | askGreeks: OptionComputation | None = None 148 | lastGreeks: OptionComputation | None = None 149 | modelGreeks: OptionComputation | None = None 150 | custGreeks: OptionComputation | None = None 151 | bidEfp: EfpData | None = None 152 | askEfp: EfpData | None = None 153 | lastEfp: EfpData | None = None 154 | openEfp: EfpData | None = None 155 | highEfp: EfpData | None = None 156 | lowEfp: EfpData | None = None 157 | closeEfp: EfpData | None = None 158 | auctionVolume: float = nan 159 | auctionPrice: float = nan 160 | auctionImbalance: float = nan 161 | regulatoryImbalance: float = nan 162 | bboExchange: str = "" 163 | snapshotPermissions: int = 0 164 | 165 | defaults: IBDefaults = field(default_factory=IBDefaults, repr=False) 166 | created: bool = False 167 | 168 | def __post_init__(self): 169 | # when copying a dataclass, the __post_init__ runs again, so we 170 | # want to make sure if this was _already_ created, we don't overwrite 171 | # everything with _another_ post_init clear. 172 | if not self.created: 173 | self.updateEvent = TickerUpdateEvent("updateEvent") 174 | self.minTick = self.defaults.unset 175 | self.bid = self.defaults.unset 176 | self.bidSize = self.defaults.unset 177 | self.ask = self.defaults.unset 178 | self.askSize = self.defaults.unset 179 | self.last = self.defaults.unset 180 | self.lastSize = self.defaults.unset 181 | self.prevBid = self.defaults.unset 182 | self.prevBidSize = self.defaults.unset 183 | self.prevAsk = self.defaults.unset 184 | self.prevAskSize = self.defaults.unset 185 | self.prevLast = self.defaults.unset 186 | self.prevLastSize = self.defaults.unset 187 | self.volume = self.defaults.unset 188 | self.open = self.defaults.unset 189 | self.high = self.defaults.unset 190 | self.low = self.defaults.unset 191 | self.close = self.defaults.unset 192 | self.vwap = self.defaults.unset 193 | self.low13week = self.defaults.unset 194 | self.high13week = self.defaults.unset 195 | self.low26week = self.defaults.unset 196 | self.high26week = self.defaults.unset 197 | self.low52week = self.defaults.unset 198 | self.high52week = self.defaults.unset 199 | self.bidYield = self.defaults.unset 200 | self.askYield = self.defaults.unset 201 | self.lastYield = self.defaults.unset 202 | self.markPrice = self.defaults.unset 203 | self.halted = self.defaults.unset 204 | self.rtHistVolatility = self.defaults.unset 205 | self.rtVolume = self.defaults.unset 206 | self.rtTradeVolume = self.defaults.unset 207 | self.avVolume = self.defaults.unset 208 | self.tradeCount = self.defaults.unset 209 | self.tradeRate = self.defaults.unset 210 | self.volumeRate = self.defaults.unset 211 | self.volumeRate3Min = self.defaults.unset 212 | self.volumeRate5Min = self.defaults.unset 213 | self.volumeRate10Min = self.defaults.unset 214 | self.shortable = self.defaults.unset 215 | self.shortableShares = self.defaults.unset 216 | self.indexFuturePremium = self.defaults.unset 217 | self.futuresOpenInterest = self.defaults.unset 218 | self.putOpenInterest = self.defaults.unset 219 | self.callOpenInterest = self.defaults.unset 220 | self.putVolume = self.defaults.unset 221 | self.callVolume = self.defaults.unset 222 | self.avOptionVolume = self.defaults.unset 223 | self.histVolatility = self.defaults.unset 224 | self.impliedVolatility = self.defaults.unset 225 | self.auctionVolume = self.defaults.unset 226 | self.auctionPrice = self.defaults.unset 227 | self.auctionImbalance = self.defaults.unset 228 | self.regulatoryImbalance = self.defaults.unset 229 | self.openInterest = self.defaults.unset 230 | self.lastRthTrade = self.defaults.unset 231 | self.bondFactorMultiplier = self.defaults.unset 232 | self.creditmanMarkPrice = self.defaults.unset 233 | self.creditmanSlowMarkPrice = self.defaults.unset 234 | self.delayedHalted = self.defaults.unset 235 | self.etfNavClose = self.defaults.unset 236 | self.etfNavPriorClose = self.defaults.unset 237 | self.etfNavBid = self.defaults.unset 238 | self.etfNavAsk = self.defaults.unset 239 | self.etfNavLast = self.defaults.unset 240 | self.etfFrozenNavLast = self.defaults.unset 241 | self.etfNavHigh = self.defaults.unset 242 | self.etfNavLow = self.defaults.unset 243 | self.estimatedIpoMidpoint = self.defaults.unset 244 | self.finalIpoLast = self.defaults.unset 245 | 246 | self.created = True 247 | 248 | def __eq__(self, other): 249 | return self is other 250 | 251 | def __hash__(self): 252 | return id(self) 253 | 254 | __repr__ = dataclassRepr 255 | __str__ = dataclassRepr 256 | 257 | def isUnset(self, value) -> bool: 258 | # if default value is nan and value is nan, it is unset. 259 | # else, if value matches default value, it is unset. 260 | dev = self.defaults.unset 261 | return (dev != dev and value != value) or (value == dev) 262 | 263 | def hasBidAsk(self) -> bool: 264 | """See if this ticker has a valid bid and ask.""" 265 | return ( 266 | self.bid != -1 267 | and not self.isUnset(self.bid) 268 | and self.bidSize > 0 269 | and self.ask != -1 270 | and not self.isUnset(self.ask) 271 | and self.askSize > 0 272 | ) 273 | 274 | def midpoint(self) -> float: 275 | """ 276 | Return average of bid and ask, or defaults.unset if no valid bid and ask 277 | are available. 278 | """ 279 | return (self.bid + self.ask) * 0.5 if self.hasBidAsk() else self.defaults.unset 280 | 281 | def marketPrice(self) -> float: 282 | """ 283 | Return the first available one of 284 | 285 | * last price if within current bid/ask or no bid/ask available; 286 | * average of bid and ask (midpoint). 287 | """ 288 | if self.hasBidAsk(): 289 | if self.bid <= self.last <= self.ask: 290 | price = self.last 291 | else: 292 | price = self.midpoint() 293 | else: 294 | price = self.last 295 | 296 | return price 297 | 298 | 299 | class TickerUpdateEvent(Event): 300 | __slots__ = () 301 | 302 | def trades(self) -> "Tickfilter": 303 | """Emit trade ticks.""" 304 | return Tickfilter((4, 5, 48, 68, 71), self) 305 | 306 | def bids(self) -> "Tickfilter": 307 | """Emit bid ticks.""" 308 | return Tickfilter((0, 1, 66, 69), self) 309 | 310 | def asks(self) -> "Tickfilter": 311 | """Emit ask ticks.""" 312 | return Tickfilter((2, 3, 67, 70), self) 313 | 314 | def bidasks(self) -> "Tickfilter": 315 | """Emit bid and ask ticks.""" 316 | return Tickfilter((0, 1, 66, 69, 2, 3, 67, 70), self) 317 | 318 | def midpoints(self) -> "Tickfilter": 319 | """Emit midpoint ticks.""" 320 | return Midpoints((), self) 321 | 322 | 323 | class Tickfilter(Op): 324 | """Tick filtering event operators that ``emit(time, price, size)``.""" 325 | 326 | __slots__ = ("_tickTypes",) 327 | 328 | def __init__(self, tickTypes, source=None): 329 | Op.__init__(self, source) 330 | self._tickTypes = set(tickTypes) 331 | 332 | def on_source(self, ticker): 333 | for t in ticker.ticks: 334 | if t.tickType in self._tickTypes: 335 | self.emit(t.time, t.price, t.size) 336 | 337 | def timebars(self, timer: Event) -> "TimeBars": 338 | """ 339 | Aggregate ticks into time bars, where the timing of new bars 340 | is derived from a timer event. 341 | Emits a completed :class:`Bar`. 342 | 343 | This event stores a :class:`BarList` of all created bars in the 344 | ``bars`` property. 345 | 346 | Args: 347 | timer: Event for timing when a new bar starts. 348 | """ 349 | return TimeBars(timer, self) 350 | 351 | def tickbars(self, count: int) -> "TickBars": 352 | """ 353 | Aggregate ticks into bars that have the same number of ticks. 354 | Emits a completed :class:`Bar`. 355 | 356 | This event stores a :class:`BarList` of all created bars in the 357 | ``bars`` property. 358 | 359 | Args: 360 | count: Number of ticks to use to form one bar. 361 | """ 362 | return TickBars(count, self) 363 | 364 | def volumebars(self, volume: int) -> "VolumeBars": 365 | """ 366 | Aggregate ticks into bars that have the same volume. 367 | Emits a completed :class:`Bar`. 368 | 369 | This event stores a :class:`BarList` of all created bars in the 370 | ``bars`` property. 371 | 372 | Args: 373 | count: Number of ticks to use to form one bar. 374 | """ 375 | return VolumeBars(volume, self) 376 | 377 | 378 | class Midpoints(Tickfilter): 379 | __slots__ = () 380 | 381 | def on_source(self, ticker): 382 | if ticker.ticks: 383 | self.emit(ticker.time, ticker.midpoint(), 0) 384 | 385 | 386 | @dataclass 387 | class Bar: 388 | time: datetime | None 389 | open: float = nan 390 | high: float = nan 391 | low: float = nan 392 | close: float = nan 393 | volume: int = 0 394 | count: int = 0 395 | 396 | 397 | class BarList(list[Bar]): 398 | def __init__(self, *args): 399 | super().__init__(*args) 400 | self.updateEvent = Event("updateEvent") 401 | 402 | def __eq__(self, other) -> bool: 403 | return self is other 404 | 405 | 406 | class TimeBars(Op): 407 | __slots__ = ( 408 | "_timer", 409 | "bars", 410 | ) 411 | __doc__ = Tickfilter.timebars.__doc__ 412 | 413 | bars: BarList 414 | 415 | def __init__(self, timer, source=None): 416 | Op.__init__(self, source) 417 | self._timer = timer 418 | self._timer.connect(self._on_timer, None, self._on_timer_done) 419 | self.bars = BarList() 420 | 421 | def on_source(self, time, price, size): 422 | if not self.bars: 423 | return 424 | bar = self.bars[-1] 425 | 426 | if isNan(bar.open): 427 | bar.open = bar.high = bar.low = price 428 | 429 | bar.high = max(bar.high, price) 430 | bar.low = min(bar.low, price) 431 | bar.close = price 432 | bar.volume += size 433 | bar.count += 1 434 | self.bars.updateEvent.emit(self.bars, False) 435 | 436 | def _on_timer(self, time): 437 | if self.bars: 438 | bar = self.bars[-1] 439 | if isNan(bar.close) and len(self.bars) > 1: 440 | bar.open = bar.high = bar.low = bar.close = self.bars[-2].close 441 | 442 | self.bars.updateEvent.emit(self.bars, True) 443 | self.emit(bar) 444 | 445 | self.bars.append(Bar(time)) 446 | 447 | def _on_timer_done(self, timer): 448 | self._timer = None 449 | self.set_done() 450 | 451 | 452 | class TickBars(Op): 453 | __slots__ = ("_count", "bars") 454 | __doc__ = Tickfilter.tickbars.__doc__ 455 | 456 | bars: BarList 457 | 458 | def __init__(self, count, source=None): 459 | Op.__init__(self, source) 460 | self._count = count 461 | self.bars = BarList() 462 | 463 | def on_source(self, time, price, size): 464 | if not self.bars or self.bars[-1].count == self._count: 465 | bar = Bar(time, price, price, price, price, size, 1) 466 | self.bars.append(bar) 467 | else: 468 | bar = self.bars[-1] 469 | bar.high = max(bar.high, price) 470 | bar.low = min(bar.low, price) 471 | bar.close = price 472 | bar.volume += size 473 | bar.count += 1 474 | if bar.count == self._count: 475 | self.bars.updateEvent.emit(self.bars, True) 476 | self.emit(self.bars) 477 | 478 | 479 | class VolumeBars(Op): 480 | __slots__ = ("_volume", "bars") 481 | __doc__ = Tickfilter.volumebars.__doc__ 482 | 483 | bars: BarList 484 | 485 | def __init__(self, volume, source=None): 486 | Op.__init__(self, source) 487 | self._volume = volume 488 | self.bars = BarList() 489 | 490 | def on_source(self, time, price, size): 491 | if not self.bars or self.bars[-1].volume >= self._volume: 492 | bar = Bar(time, price, price, price, price, size, 1) 493 | self.bars.append(bar) 494 | else: 495 | bar = self.bars[-1] 496 | bar.high = max(bar.high, price) 497 | bar.low = min(bar.low, price) 498 | bar.close = price 499 | bar.volume += size 500 | bar.count += 1 501 | if bar.volume >= self._volume: 502 | self.bars.updateEvent.emit(self.bars, True) 503 | self.emit(self.bars) 504 | -------------------------------------------------------------------------------- /ib_async/util.py: -------------------------------------------------------------------------------- 1 | """Utilities.""" 2 | 3 | import asyncio 4 | import datetime as dt 5 | import logging 6 | import math 7 | import signal 8 | import sys 9 | import time 10 | from collections.abc import AsyncIterator, Awaitable, Callable, Iterator 11 | from dataclasses import fields, is_dataclass 12 | from typing import ( 13 | Any, 14 | Final, 15 | TypeAlias, 16 | ) 17 | from zoneinfo import ZoneInfo 18 | 19 | import eventkit as ev 20 | 21 | globalErrorEvent = ev.Event() 22 | """ 23 | Event to emit global exceptions. 24 | """ 25 | 26 | EPOCH: Final = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc) 27 | UNSET_INTEGER: Final = 2**31 - 1 28 | UNSET_DOUBLE: Final = sys.float_info.max 29 | 30 | Time_t: TypeAlias = dt.time | dt.datetime 31 | 32 | 33 | def df(objs, labels: list[str] | None = None): 34 | """ 35 | Create pandas DataFrame from the sequence of same-type objects. 36 | 37 | Args: 38 | labels: If supplied, retain only the given labels and drop the rest. 39 | """ 40 | import pandas as pd 41 | 42 | from .objects import DynamicObject 43 | 44 | if objs: 45 | objs = list(objs) 46 | obj = objs[0] 47 | if is_dataclass(obj): 48 | df = pd.DataFrame.from_records(dataclassAsTuple(o) for o in objs) 49 | df.columns = [field.name for field in fields(obj)] 50 | elif isinstance(obj, DynamicObject): 51 | df = pd.DataFrame.from_records(o.__dict__ for o in objs) 52 | else: 53 | df = pd.DataFrame.from_records(objs) 54 | 55 | if isinstance(obj, tuple): 56 | _fields = getattr(obj, "_fields", None) 57 | if _fields: 58 | # assume it's a namedtuple 59 | df.columns = _fields 60 | else: 61 | df = None 62 | 63 | if labels: 64 | exclude = [label for label in df if label not in labels] 65 | df = df.drop(exclude, axis=1) 66 | 67 | return df 68 | 69 | 70 | def dataclassAsDict(obj) -> dict: 71 | """ 72 | Return dataclass values as ``dict``. 73 | This is a non-recursive variant of ``dataclasses.asdict``. 74 | """ 75 | if not is_dataclass(obj): 76 | raise TypeError(f"Object {obj} is not a dataclass") 77 | 78 | return {field.name: getattr(obj, field.name) for field in fields(obj)} 79 | 80 | 81 | def dataclassAsTuple(obj) -> tuple[Any, ...]: 82 | """ 83 | Return dataclass values as ``tuple``. 84 | This is a non-recursive variant of ``dataclasses.astuple``. 85 | """ 86 | if not is_dataclass(obj): 87 | raise TypeError(f"Object {obj} is not a dataclass") 88 | 89 | return tuple(getattr(obj, field.name) for field in fields(obj)) 90 | 91 | 92 | def dataclassNonDefaults(obj) -> dict[str, Any]: 93 | """ 94 | For a ``dataclass`` instance get the fields that are different from the 95 | default values and return as ``dict``. 96 | """ 97 | if not is_dataclass(obj): 98 | raise TypeError(f"Object {obj} is not a dataclass") 99 | 100 | values = [getattr(obj, field.name) for field in fields(obj)] 101 | 102 | return { 103 | field.name: value 104 | for field, value in zip(fields(obj), values) 105 | if value is not None 106 | and value != field.default 107 | and value == value 108 | and not ( 109 | (isinstance(value, list) and value == []) 110 | or (isinstance(value, dict) and value == {}) 111 | ) 112 | } 113 | 114 | 115 | def dataclassUpdate(obj, *srcObjs, **kwargs) -> object: 116 | """ 117 | Update fields of the given ``dataclass`` object from zero or more 118 | ``dataclass`` source objects and/or from keyword arguments. 119 | """ 120 | if not is_dataclass(obj): 121 | raise TypeError(f"Object {obj} is not a dataclass") 122 | 123 | for srcObj in srcObjs: 124 | obj.__dict__.update(dataclassAsDict(srcObj)) # type: ignore 125 | 126 | obj.__dict__.update(**kwargs) # type: ignore 127 | return obj 128 | 129 | 130 | def dataclassRepr(obj) -> str: 131 | """ 132 | Provide a culled representation of the given ``dataclass`` instance, 133 | showing only the fields with a non-default value. 134 | """ 135 | attrs = dataclassNonDefaults(obj) 136 | clsName = obj.__class__.__qualname__ 137 | kwargs = ", ".join(f"{k}={v!r}" for k, v in attrs.items()) 138 | return f"{clsName}({kwargs})" 139 | 140 | 141 | def isnamedtupleinstance(x): 142 | """From https://stackoverflow.com/a/2166841/6067848""" 143 | t = type(x) 144 | b = t.__bases__ 145 | if len(b) != 1 or not isinstance(x, tuple): 146 | return False 147 | 148 | f = getattr(t, "_fields", None) 149 | if not isinstance(f, tuple): 150 | return False 151 | 152 | return all(isinstance(n, str) for n in f) 153 | 154 | 155 | def tree(obj): 156 | """ 157 | Convert object to a tree of lists, dicts and simple values. 158 | The result can be serialized to JSON. 159 | """ 160 | if isinstance(obj, bool | int | float | str | bytes): 161 | return obj 162 | 163 | if isinstance(obj, dt.date | dt.time): 164 | return obj.isoformat() 165 | 166 | if isinstance(obj, dict): 167 | return {k: tree(v) for k, v in obj.items()} 168 | 169 | if isnamedtupleinstance(obj): 170 | return {f: tree(getattr(obj, f)) for f in obj._fields} 171 | 172 | if isinstance(obj, list | tuple | set): 173 | return [tree(i) for i in obj] 174 | 175 | if is_dataclass(obj): 176 | return {obj.__class__.__qualname__: tree(dataclassNonDefaults(obj))} 177 | 178 | return str(obj) 179 | 180 | 181 | def barplot(bars, title="", upColor="blue", downColor="red"): 182 | """ 183 | Create candlestick plot for the given bars. The bars can be given as 184 | a DataFrame or as a list of bar objects. 185 | """ 186 | import matplotlib.pyplot as plt 187 | import pandas as pd 188 | from matplotlib.lines import Line2D 189 | from matplotlib.patches import Rectangle 190 | 191 | if isinstance(bars, pd.DataFrame): 192 | ohlcTups = [tuple(v) for v in bars[["open", "high", "low", "close"]].values] 193 | elif bars and hasattr(bars[0], "open_"): 194 | ohlcTups = [(b.open_, b.high, b.low, b.close) for b in bars] 195 | else: 196 | ohlcTups = [(b.open, b.high, b.low, b.close) for b in bars] 197 | 198 | fig, ax = plt.subplots() 199 | ax.set_title(title) 200 | ax.grid(True) 201 | fig.set_size_inches(10, 6) 202 | for n, (open_, high, low, close) in enumerate(ohlcTups): 203 | if close >= open_: 204 | color = upColor 205 | bodyHi, bodyLo = close, open_ 206 | else: 207 | color = downColor 208 | bodyHi, bodyLo = open_, close 209 | line = Line2D(xdata=(n, n), ydata=(low, bodyLo), color=color, linewidth=1) 210 | ax.add_line(line) 211 | line = Line2D(xdata=(n, n), ydata=(high, bodyHi), color=color, linewidth=1) 212 | ax.add_line(line) 213 | rect = Rectangle( 214 | xy=(n - 0.3, bodyLo), 215 | width=0.6, 216 | height=bodyHi - bodyLo, 217 | edgecolor=color, 218 | facecolor=color, 219 | alpha=0.4, 220 | antialiased=True, 221 | ) 222 | ax.add_patch(rect) 223 | 224 | ax.autoscale_view() 225 | return fig 226 | 227 | 228 | def allowCtrlC(): 229 | """Allow Control-C to end program.""" 230 | signal.signal(signal.SIGINT, signal.SIG_DFL) 231 | 232 | 233 | def logToFile(path, level=logging.INFO): 234 | """Create a log handler that logs to the given file.""" 235 | logger = logging.getLogger() 236 | if logger.handlers: 237 | logging.getLogger("ib_async").setLevel(level) 238 | else: 239 | logger.setLevel(level) 240 | 241 | formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s") 242 | handler = logging.FileHandler(path) 243 | handler.setFormatter(formatter) 244 | logger.addHandler(handler) 245 | 246 | 247 | def logToConsole(level=logging.INFO): 248 | """Create a log handler that logs to the console.""" 249 | logger = logging.getLogger() 250 | stdHandlers = [ 251 | h 252 | for h in logger.handlers 253 | if type(h) is logging.StreamHandler and h.stream is sys.stderr 254 | ] 255 | 256 | if stdHandlers: 257 | # if a standard stream handler already exists, use it and 258 | # set the log level for the ib_async namespace only 259 | logging.getLogger("ib_async").setLevel(level) 260 | else: 261 | # else create a new handler 262 | logger.setLevel(level) 263 | formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s") 264 | handler = logging.StreamHandler() 265 | handler.setFormatter(formatter) 266 | logger.addHandler(handler) 267 | 268 | 269 | def isNan(x: float) -> bool: 270 | """Not a number test.""" 271 | return x != x 272 | 273 | 274 | def formatSI(n: float) -> str: 275 | """Format the integer or float n to 3 significant digits + SI prefix.""" 276 | s = "" 277 | if n < 0: 278 | n = -n 279 | s += "-" 280 | 281 | if isinstance(n, int) and n < 1000: 282 | s = str(n) + " " 283 | elif n < 1e-22: 284 | s = "0.00 " 285 | else: 286 | assert n < 9.99e26 287 | log = int(math.floor(math.log10(n))) 288 | i, j = divmod(log, 3) 289 | for _try in range(2): 290 | templ = f"%.{2 - j}f" 291 | val = templ % (n * 10 ** (-3 * i)) 292 | if val != "1000": 293 | break 294 | i += 1 295 | j = 0 296 | s += val + " " 297 | if i != 0: 298 | s += "yzafpnum kMGTPEZY"[i + 8] 299 | 300 | return s 301 | 302 | 303 | class timeit: 304 | """Context manager for timing.""" 305 | 306 | def __init__(self, title="Run"): 307 | self.title = title 308 | 309 | def __enter__(self): 310 | self.t0 = time.time() 311 | 312 | def __exit__(self, *_args): 313 | print(self.title + " took " + formatSI(time.time() - self.t0) + "s") 314 | 315 | 316 | def run(*awaitables: Awaitable, timeout: float | None = None): 317 | """ 318 | By default run the event loop forever. 319 | 320 | When awaitables (like Tasks, Futures or coroutines) are given then 321 | run the event loop until each has completed and return their results. 322 | 323 | An optional timeout (in seconds) can be given that will raise 324 | asyncio.TimeoutError if the awaitables are not ready within the 325 | timeout period. 326 | """ 327 | loop = getLoop() 328 | if not awaitables: 329 | if loop.is_running(): 330 | return 331 | 332 | loop.run_forever() 333 | result = None 334 | all_tasks = asyncio.all_tasks(loop) # type: ignore 335 | 336 | if all_tasks: 337 | # cancel pending tasks 338 | f = asyncio.gather(*all_tasks) 339 | f.cancel() 340 | try: 341 | loop.run_until_complete(f) 342 | except asyncio.CancelledError: 343 | pass 344 | else: 345 | if len(awaitables) == 1: 346 | future = awaitables[0] 347 | else: 348 | future = asyncio.gather(*awaitables) 349 | 350 | if timeout: 351 | future = asyncio.wait_for(future, timeout) 352 | # Pass loop explicitly to avoid deprecation warnings in Python 3.10+ 353 | task = asyncio.ensure_future(future, loop=loop) 354 | 355 | def onError(_): 356 | task.cancel() 357 | 358 | globalErrorEvent.connect(onError) 359 | try: 360 | result = loop.run_until_complete(task) 361 | except asyncio.CancelledError as e: 362 | raise globalErrorEvent.value() or e 363 | finally: 364 | globalErrorEvent.disconnect(onError) 365 | 366 | return result 367 | 368 | 369 | def _fillDate(time: Time_t) -> dt.datetime: 370 | # use today if date is absent 371 | if isinstance(time, dt.time): 372 | t = dt.datetime.combine(dt.date.today(), time) 373 | else: 374 | t = time 375 | return t 376 | 377 | 378 | def schedule(time: Time_t, callback: Callable, *args): 379 | """ 380 | Schedule the callback to be run at the given time with 381 | the given arguments. 382 | This will return the Event Handle. 383 | 384 | Args: 385 | time: Time to run callback. If given as :py:class:`datetime.time` 386 | then use today as date. 387 | callback: Callable scheduled to run. 388 | args: Arguments for to call callback with. 389 | """ 390 | t = _fillDate(time) 391 | now = dt.datetime.now(t.tzinfo) 392 | delay = (t - now).total_seconds() 393 | loop = getLoop() 394 | return loop.call_later(delay, callback, *args) 395 | 396 | 397 | def sleep(secs: float = 0.02) -> bool: 398 | """ 399 | Wait for the given amount of seconds while everything still keeps 400 | processing in the background. Never use time.sleep(). 401 | 402 | Args: 403 | secs (float): Time in seconds to wait. 404 | """ 405 | run(asyncio.sleep(secs)) 406 | return True 407 | 408 | 409 | def timeRange(start: Time_t, end: Time_t, step: float) -> Iterator[dt.datetime]: 410 | """ 411 | Iterator that waits periodically until certain time points are 412 | reached while yielding those time points. 413 | 414 | Args: 415 | start: Start time, can be specified as datetime.datetime, 416 | or as datetime.time in which case today is used as the date 417 | end: End time, can be specified as datetime.datetime, 418 | or as datetime.time in which case today is used as the date 419 | step (float): The number of seconds of each period 420 | """ 421 | assert step > 0 422 | delta = dt.timedelta(seconds=step) 423 | t = _fillDate(start) 424 | tz = dt.timezone.utc if t.tzinfo else None 425 | now = dt.datetime.now(tz) 426 | while t < now: 427 | t += delta 428 | 429 | while t <= _fillDate(end): 430 | waitUntil(t) 431 | yield t 432 | t += delta 433 | 434 | 435 | def waitUntil(t: Time_t) -> bool: 436 | """ 437 | Wait until the given time t is reached. 438 | 439 | Args: 440 | t: The time t can be specified as datetime.datetime, 441 | or as datetime.time in which case today is used as the date. 442 | """ 443 | now = dt.datetime.now(t.tzinfo) 444 | secs = (_fillDate(t) - now).total_seconds() 445 | run(asyncio.sleep(secs)) 446 | return True 447 | 448 | 449 | async def timeRangeAsync( 450 | start: Time_t, end: Time_t, step: float 451 | ) -> AsyncIterator[dt.datetime]: 452 | """Async version of :meth:`timeRange`.""" 453 | assert step > 0 454 | 455 | delta = dt.timedelta(seconds=step) 456 | t = _fillDate(start) 457 | tz = dt.timezone.utc if t.tzinfo else None 458 | now = dt.datetime.now(tz) 459 | while t < now: 460 | t += delta 461 | 462 | while t <= _fillDate(end): 463 | await waitUntilAsync(t) 464 | yield t 465 | t += delta 466 | 467 | 468 | async def waitUntilAsync(t: Time_t) -> bool: 469 | """Async version of :meth:`waitUntil`.""" 470 | now = dt.datetime.now(t.tzinfo) 471 | secs = (_fillDate(t) - now).total_seconds() 472 | await asyncio.sleep(secs) 473 | 474 | return True 475 | 476 | 477 | def patchAsyncio(): 478 | """Patch asyncio to allow nested event loops.""" 479 | import nest_asyncio 480 | 481 | nest_asyncio.apply() 482 | 483 | 484 | def getLoop(): 485 | """ 486 | Get asyncio event loop with smart fallback handling. 487 | 488 | This function is designed for use in synchronous contexts or when the 489 | execution context is unknown. It will: 490 | 1. Try to get the currently running event loop (if in async context) 491 | 2. Fall back to getting the current thread's event loop via policy 492 | 3. Create a new event loop if none exists or if the existing one is closed 493 | 494 | For performance-critical async code paths, prefer using 495 | asyncio.get_running_loop() directly instead of this function. 496 | 497 | Note: This function does NOT cache the loop to avoid stale loop bugs 498 | when loops are closed and recreated (e.g., in testing, Jupyter notebooks). 499 | """ 500 | try: 501 | # Fast path: we're in an async context (coroutine or callback) 502 | loop = asyncio.get_running_loop() 503 | return loop 504 | except RuntimeError: 505 | pass 506 | 507 | # We're in a sync context or no loop is running 508 | # Use the event loop policy to get the loop for this thread 509 | # This avoids deprecation warnings from get_event_loop() in Python 3.10+ 510 | try: 511 | loop = asyncio.get_event_loop_policy().get_event_loop() 512 | except RuntimeError: 513 | # No event loop exists for this thread, create one 514 | loop = asyncio.new_event_loop() 515 | asyncio.set_event_loop(loop) 516 | return loop 517 | 518 | # Check if the loop we got is closed - if so, create a new one 519 | if loop.is_closed(): 520 | loop = asyncio.new_event_loop() 521 | asyncio.set_event_loop(loop) 522 | 523 | return loop 524 | 525 | 526 | def startLoop(): 527 | """Use nested asyncio event loop for Jupyter notebooks.""" 528 | patchAsyncio() 529 | 530 | 531 | def useQt(qtLib: str = "PyQt5", period: float = 0.01): 532 | """ 533 | Run combined Qt5/asyncio event loop. 534 | 535 | Args: 536 | qtLib: Name of Qt library to use: 537 | 538 | * PyQt5 539 | * PyQt6 540 | * PySide2 541 | * PySide6 542 | period: Period in seconds to poll Qt. 543 | """ 544 | 545 | def qt_step(): 546 | loop.call_later(period, qt_step) 547 | if not stack: 548 | qloop = qc.QEventLoop() 549 | timer = qc.QTimer() 550 | timer.timeout.connect(qloop.quit) 551 | stack.append((qloop, timer)) 552 | qloop, timer = stack.pop() 553 | timer.start(0) 554 | qloop.exec() if qtLib == "PyQt6" else qloop.exec_() 555 | timer.stop() 556 | stack.append((qloop, timer)) 557 | qApp.processEvents() # type: ignore 558 | 559 | if qtLib not in {"PyQt5", "PyQt6", "PySide2", "PySide6"}: 560 | raise RuntimeError(f"Unknown Qt library: {qtLib}") 561 | from importlib import import_module 562 | 563 | qc = import_module(qtLib + ".QtCore") 564 | qw = import_module(qtLib + ".QtWidgets") 565 | global qApp 566 | qApp = ( # type: ignore 567 | qw.QApplication.instance() or qw.QApplication(sys.argv) # type: ignore 568 | ) # type: ignore 569 | loop = getLoop() 570 | stack: list = [] 571 | qt_step() 572 | 573 | 574 | def formatIBDatetime(t: dt.date | dt.datetime | str | None) -> str: 575 | """Format date or datetime to string that IB uses.""" 576 | if not t: 577 | s = "" 578 | elif isinstance(t, dt.datetime): 579 | # convert to UTC timezone 580 | t = t.astimezone(tz=dt.timezone.utc) 581 | s = t.strftime("%Y%m%d %H:%M:%S UTC") 582 | elif isinstance(t, dt.date): 583 | t = dt.datetime(t.year, t.month, t.day, 23, 59, 59).astimezone( 584 | tz=dt.timezone.utc 585 | ) 586 | s = t.strftime("%Y%m%d %H:%M:%S UTC") 587 | else: 588 | s = t 589 | 590 | return s 591 | 592 | 593 | def parseIBDatetime(s: str) -> dt.date | dt.datetime: 594 | """Parse string in IB date or datetime format to datetime.""" 595 | if len(s) == 8: 596 | # YYYYmmdd 597 | y = int(s[0:4]) 598 | m = int(s[4:6]) 599 | d = int(s[6:8]) 600 | t = dt.date(y, m, d) 601 | elif s.isdigit(): 602 | t = dt.datetime.fromtimestamp(int(s), dt.timezone.utc) 603 | elif s.count(" ") >= 2 and " " not in s: 604 | # 20221125 10:00:00 Europe/Amsterdam 605 | s0, s1, s2 = s.split(" ", 2) 606 | t = dt.datetime.strptime(s0 + s1, "%Y%m%d%H:%M:%S") 607 | t = t.replace(tzinfo=ZoneInfo(s2)) 608 | else: 609 | # YYYYmmdd HH:MM:SS 610 | # or 611 | # YYYY-mm-dd HH:MM:SS.0 612 | ss = s.replace(" ", "").replace("-", "")[:16] 613 | t = dt.datetime.strptime(ss, "%Y%m%d%H:%M:%S") 614 | 615 | return t 616 | -------------------------------------------------------------------------------- /ib_async/order.py: -------------------------------------------------------------------------------- 1 | """Order types used by Interactive Brokers.""" 2 | 3 | from __future__ import annotations 4 | 5 | import dataclasses 6 | from dataclasses import dataclass, field 7 | from decimal import Decimal 8 | from typing import ClassVar, NamedTuple 9 | 10 | from eventkit import Event 11 | 12 | from .contract import Contract, TagValue 13 | from .objects import Fill, SoftDollarTier, TradeLogEntry 14 | from .util import UNSET_DOUBLE, UNSET_INTEGER, dataclassNonDefaults 15 | 16 | 17 | @dataclass 18 | class Order: 19 | """ 20 | Order for trading contracts. 21 | 22 | https://interactivebrokers.github.io/tws-api/available_orders.html 23 | """ 24 | 25 | orderId: int = 0 26 | clientId: int = 0 27 | permId: int = 0 28 | action: str = "" 29 | totalQuantity: float = 0.0 30 | orderType: str = "" 31 | lmtPrice: float | Decimal | None = UNSET_DOUBLE 32 | auxPrice: float | Decimal | None = UNSET_DOUBLE 33 | tif: str = "" 34 | activeStartTime: str = "" 35 | activeStopTime: str = "" 36 | ocaGroup: str = "" 37 | ocaType: int = 0 38 | orderRef: str = "" 39 | transmit: bool = True 40 | parentId: int = 0 41 | blockOrder: bool = False 42 | sweepToFill: bool = False 43 | displaySize: int = 0 44 | triggerMethod: int = 0 45 | outsideRth: bool = False 46 | hidden: bool = False 47 | goodAfterTime: str = "" 48 | goodTillDate: str = "" 49 | rule80A: str = "" 50 | allOrNone: bool = False 51 | minQty: int = UNSET_INTEGER 52 | percentOffset: float | Decimal = UNSET_DOUBLE 53 | overridePercentageConstraints: bool = False 54 | trailStopPrice: float | Decimal = UNSET_DOUBLE 55 | trailingPercent: float | Decimal = UNSET_DOUBLE 56 | faGroup: str = "" 57 | faProfile: str = "" # obsolete 58 | faMethod: str = "" 59 | faPercentage: str = "" 60 | designatedLocation: str = "" 61 | openClose: str = "O" 62 | origin: int = 0 63 | shortSaleSlot: int = 0 64 | exemptCode: int = -1 65 | discretionaryAmt: float = 0.0 66 | eTradeOnly: bool = False 67 | firmQuoteOnly: bool = False 68 | nbboPriceCap: float | Decimal = UNSET_DOUBLE 69 | optOutSmartRouting: bool = False 70 | auctionStrategy: int = 0 71 | startingPrice: float | Decimal = UNSET_DOUBLE 72 | stockRefPrice: float | Decimal = UNSET_DOUBLE 73 | delta: float | Decimal = UNSET_DOUBLE 74 | stockRangeLower: float | Decimal = UNSET_DOUBLE 75 | stockRangeUpper: float | Decimal = UNSET_DOUBLE 76 | randomizePrice: bool = False 77 | randomizeSize: bool = False 78 | volatility: float | Decimal = UNSET_DOUBLE 79 | volatilityType: int = UNSET_INTEGER 80 | deltaNeutralOrderType: str = "" 81 | deltaNeutralAuxPrice: float | Decimal = UNSET_DOUBLE 82 | deltaNeutralConId: int = 0 83 | deltaNeutralSettlingFirm: str = "" 84 | deltaNeutralClearingAccount: str = "" 85 | deltaNeutralClearingIntent: str = "" 86 | deltaNeutralOpenClose: str = "" 87 | deltaNeutralShortSale: bool = False 88 | deltaNeutralShortSaleSlot: int = 0 89 | deltaNeutralDesignatedLocation: str = "" 90 | continuousUpdate: bool = False 91 | referencePriceType: int = UNSET_INTEGER 92 | basisPoints: float | Decimal = UNSET_DOUBLE 93 | basisPointsType: int = UNSET_INTEGER 94 | scaleInitLevelSize: int = UNSET_INTEGER 95 | scaleSubsLevelSize: int = UNSET_INTEGER 96 | scalePriceIncrement: float | Decimal = UNSET_DOUBLE 97 | scalePriceAdjustValue: float | Decimal = UNSET_DOUBLE 98 | scalePriceAdjustInterval: int = UNSET_INTEGER 99 | scaleProfitOffset: float | Decimal = UNSET_DOUBLE 100 | scaleAutoReset: bool = False 101 | scaleInitPosition: int = UNSET_INTEGER 102 | scaleInitFillQty: int = UNSET_INTEGER 103 | scaleRandomPercent: bool = False 104 | scaleTable: str = "" 105 | hedgeType: str = "" 106 | hedgeParam: str = "" 107 | account: str = "" 108 | settlingFirm: str = "" 109 | clearingAccount: str = "" 110 | clearingIntent: str = "" 111 | algoStrategy: str = "" 112 | algoParams: list[TagValue] = field(default_factory=list) 113 | smartComboRoutingParams: list[TagValue] = field(default_factory=list) 114 | algoId: str = "" 115 | whatIf: bool = False 116 | notHeld: bool = False 117 | solicited: bool = False 118 | modelCode: str = "" 119 | orderComboLegs: list[OrderComboLeg] = field(default_factory=list) 120 | orderMiscOptions: list[TagValue] = field(default_factory=list) 121 | referenceContractId: int = 0 122 | peggedChangeAmount: float = 0.0 123 | isPeggedChangeAmountDecrease: bool = False 124 | referenceChangeAmount: float = 0.0 125 | referenceExchangeId: str = "" 126 | adjustedOrderType: str = "" 127 | triggerPrice: float | Decimal | None = UNSET_DOUBLE 128 | adjustedStopPrice: float | Decimal = UNSET_DOUBLE 129 | adjustedStopLimitPrice: float | Decimal = UNSET_DOUBLE 130 | adjustedTrailingAmount: float | Decimal = UNSET_DOUBLE 131 | adjustableTrailingUnit: int = 0 132 | lmtPriceOffset: float | Decimal = UNSET_DOUBLE 133 | conditions: list[OrderCondition] = field(default_factory=list) 134 | conditionsCancelOrder: bool = False 135 | conditionsIgnoreRth: bool = False 136 | extOperator: str = "" 137 | softDollarTier: SoftDollarTier = field(default_factory=SoftDollarTier) 138 | cashQty: float | Decimal = UNSET_DOUBLE 139 | mifid2DecisionMaker: str = "" 140 | mifid2DecisionAlgo: str = "" 141 | mifid2ExecutionTrader: str = "" 142 | mifid2ExecutionAlgo: str = "" 143 | dontUseAutoPriceForHedge: bool = False 144 | isOmsContainer: bool = False 145 | discretionaryUpToLimitPrice: bool = False 146 | autoCancelDate: str = "" 147 | filledQuantity: float | Decimal = UNSET_DOUBLE 148 | refFuturesConId: int = 0 149 | autoCancelParent: bool = False 150 | shareholder: str = "" 151 | imbalanceOnly: bool = False 152 | routeMarketableToBbo: bool = False 153 | parentPermId: int = 0 154 | usePriceMgmtAlgo: bool = False 155 | duration: int = UNSET_INTEGER 156 | postToAts: int = UNSET_INTEGER 157 | advancedErrorOverride: str = "" 158 | manualOrderTime: str = "" 159 | minTradeQty: int = UNSET_INTEGER 160 | minCompeteSize: int = UNSET_INTEGER 161 | competeAgainstBestOffset: float | Decimal = UNSET_DOUBLE 162 | midOffsetAtWhole: float | Decimal = UNSET_DOUBLE 163 | midOffsetAtHalf: float | Decimal = UNSET_DOUBLE 164 | 165 | def __repr__(self): 166 | attrs = dataclassNonDefaults(self) 167 | if self.__class__ is not Order: 168 | attrs.pop("orderType", None) 169 | 170 | if not self.softDollarTier: 171 | attrs.pop("softDollarTier") 172 | 173 | clsName = self.__class__.__qualname__ 174 | kwargs = ", ".join(f"{k}={v!r}" for k, v in attrs.items()) 175 | return f"{clsName}({kwargs})" 176 | 177 | __str__ = __repr__ 178 | 179 | def __eq__(self, other): 180 | return self is other 181 | 182 | def __hash__(self): 183 | return id(self) 184 | 185 | 186 | class LimitOrder(Order): 187 | def __init__(self, action: str, totalQuantity: float, lmtPrice: float, **kwargs): 188 | Order.__init__( 189 | self, 190 | orderType="LMT", 191 | action=action, 192 | totalQuantity=totalQuantity, 193 | lmtPrice=lmtPrice, 194 | **kwargs, 195 | ) 196 | 197 | 198 | class MarketOrder(Order): 199 | def __init__(self, action: str, totalQuantity: float, **kwargs): 200 | Order.__init__( 201 | self, orderType="MKT", action=action, totalQuantity=totalQuantity, **kwargs 202 | ) 203 | 204 | 205 | class StopOrder(Order): 206 | def __init__(self, action: str, totalQuantity: float, stopPrice: float, **kwargs): 207 | Order.__init__( 208 | self, 209 | orderType="STP", 210 | action=action, 211 | totalQuantity=totalQuantity, 212 | auxPrice=stopPrice, 213 | **kwargs, 214 | ) 215 | 216 | 217 | class StopLimitOrder(Order): 218 | def __init__( 219 | self, 220 | action: str, 221 | totalQuantity: float, 222 | lmtPrice: float, 223 | stopPrice: float, 224 | **kwargs, 225 | ): 226 | Order.__init__( 227 | self, 228 | orderType="STP LMT", 229 | action=action, 230 | totalQuantity=totalQuantity, 231 | lmtPrice=lmtPrice, 232 | auxPrice=stopPrice, 233 | **kwargs, 234 | ) 235 | 236 | 237 | @dataclass 238 | class OrderStatus: 239 | orderId: int = 0 240 | status: str = "" 241 | filled: float = 0.0 242 | remaining: float = 0.0 243 | avgFillPrice: float = 0.0 244 | permId: int = 0 245 | parentId: int = 0 246 | lastFillPrice: float = 0.0 247 | clientId: int = 0 248 | whyHeld: str = "" 249 | mktCapPrice: float = 0.0 250 | 251 | @property 252 | def total(self) -> float: 253 | """Helper property to return the total size of this requested order.""" 254 | return self.filled + self.remaining 255 | 256 | PendingSubmit: ClassVar[str] = "PendingSubmit" 257 | PendingCancel: ClassVar[str] = "PendingCancel" 258 | PreSubmitted: ClassVar[str] = "PreSubmitted" 259 | Submitted: ClassVar[str] = "Submitted" 260 | ApiPending: ClassVar[str] = "ApiPending" 261 | ApiCancelled: ClassVar[str] = "ApiCancelled" 262 | ApiUpdate: ClassVar[str] = "ApiUpdate" 263 | Cancelled: ClassVar[str] = "Cancelled" 264 | Filled: ClassVar[str] = "Filled" 265 | Inactive: ClassVar[str] = "Inactive" 266 | ValidationError: ClassVar[str] = "ValidationError" 267 | 268 | # order has either been completed, cancelled, or destroyed by IBKR's risk management 269 | DoneStates: ClassVar[frozenset[str]] = frozenset( 270 | ["Filled", "Cancelled", "ApiCancelled", "Inactive"] 271 | ) 272 | 273 | # order is capable of executing at sometime in the future 274 | ActiveStates: ClassVar[frozenset[str]] = frozenset( 275 | [ 276 | "PendingSubmit", 277 | "ApiPending", 278 | "PreSubmitted", 279 | "Submitted", 280 | "ValidationError", 281 | "ApiUpdate", 282 | ] 283 | ) 284 | 285 | # order hasn't triggered "live" yet (but it could become live and execute before we receive a notice) 286 | WaitingStates: ClassVar[frozenset[str]] = frozenset( 287 | [ 288 | "PendingSubmit", 289 | "ApiPending", 290 | "PreSubmitted", 291 | ] 292 | ) 293 | 294 | # order is live and "working" at the broker against public exchanges 295 | WorkingStates: ClassVar[frozenset[str]] = frozenset( 296 | [ 297 | "Submitted", 298 | # ValidationError can happen on submit or modify. 299 | # If ValidationError happens on submit, the states go PreSubmitted -> ValidationError -> Submitted (if it can be ignored automatically), so order is still live. 300 | # If ValidationError happens on modify, the update is just ValidationError with no new Submitted, so the previous order state remains active. 301 | "ValidationError", 302 | "ApiUpdate", 303 | ] 304 | ) 305 | 306 | 307 | @dataclass 308 | class OrderState: 309 | status: str = "" 310 | initMarginBefore: str = "" 311 | maintMarginBefore: str = "" 312 | equityWithLoanBefore: str = "" 313 | initMarginChange: str = "" 314 | maintMarginChange: str = "" 315 | equityWithLoanChange: str = "" 316 | initMarginAfter: str = "" 317 | maintMarginAfter: str = "" 318 | equityWithLoanAfter: str = "" 319 | commission: float = UNSET_DOUBLE 320 | minCommission: float = UNSET_DOUBLE 321 | maxCommission: float = UNSET_DOUBLE 322 | commissionCurrency: str = "" 323 | warningText: str = "" 324 | completedTime: str = "" 325 | completedStatus: str = "" 326 | 327 | def transform(self, transformer): 328 | """Convert the numeric values of this OrderState into a new OrderState transformed by 'using'""" 329 | return dataclasses.replace( 330 | self, 331 | initMarginBefore=transformer(self.initMarginBefore), 332 | maintMarginBefore=transformer(self.maintMarginBefore), 333 | equityWithLoanBefore=transformer(self.equityWithLoanBefore), 334 | initMarginChange=transformer(self.initMarginChange), 335 | maintMarginChange=transformer(self.maintMarginChange), 336 | equityWithLoanChange=transformer(self.equityWithLoanChange), 337 | initMarginAfter=transformer(self.initMarginAfter), 338 | maintMarginAfter=transformer(self.maintMarginAfter), 339 | equityWithLoanAfter=transformer(self.equityWithLoanAfter), 340 | commission=transformer(self.commission), 341 | minCommission=transformer(self.minCommission), 342 | maxCommission=transformer(self.maxCommission), 343 | ) 344 | 345 | def numeric(self, digits: int = 2) -> OrderStateNumeric: 346 | """Return a new OrderState with the current values values to floats instead of strings as returned from IBKR directly.""" 347 | 348 | def floatOrNone(what, precision) -> float | None: 349 | """Attempt to convert input to a float, but if we fail (value is just empty string) return None""" 350 | try: 351 | # convert 352 | floated = float(what) 353 | 354 | # if the conversion is IBKR speak for "this value is not set" then give us None 355 | if floated == UNSET_DOUBLE: 356 | return None 357 | 358 | # else, round to the requested precision 359 | return round(floated, precision) 360 | except Exception as _: 361 | # initial conversion failed so just return None in its place 362 | return None 363 | 364 | return self.transform(lambda x: floatOrNone(x, digits)) 365 | 366 | def formatted(self, digits: int = 2): 367 | """Return a new OrderState with the current values as formatted strings.""" 368 | return self.numeric(8).transform( 369 | # 300000.21 -> 300,000.21 370 | # 0.0 -> 0.00 371 | # 431.342000000001 -> 431.34 372 | # Note: we need 'is not None' here because 'x=0' is a valid numeric input too 373 | lambda x: f"{x:,.{digits}f}" if x is not None else None 374 | ) 375 | 376 | 377 | @dataclass 378 | class OrderStateNumeric(OrderState): 379 | """Just a type helper for mypy to check against if you convert OrderState to .numeric(). 380 | 381 | Usage: 382 | 383 | state_numeric: OrderStateNumeric = state.numeric(digits=2)""" 384 | 385 | initMarginBefore: float = float("nan") # type: ignore 386 | maintMarginBefore: float = float("nan") # type: ignore 387 | equityWithLoanBefore: float = float("nan") # type: ignore 388 | initMarginChange: float = float("nan") # type: ignore 389 | maintMarginChange: float = float("nan") # type: ignore 390 | equityWithLoanChange: float = float("nan") # type: ignore 391 | initMarginAfter: float = float("nan") # type: ignore 392 | maintMarginAfter: float = float("nan") # type: ignore 393 | equityWithLoanAfter: float = float("nan") # type: ignore 394 | 395 | 396 | @dataclass 397 | class OrderComboLeg: 398 | price: float | Decimal = UNSET_DOUBLE 399 | 400 | 401 | @dataclass 402 | class Trade: 403 | """ 404 | Trade keeps track of an order, its status and all its fills. 405 | 406 | Events: 407 | * ``statusEvent`` (trade: :class:`.Trade`) 408 | * ``modifyEvent`` (trade: :class:`.Trade`) 409 | * ``fillEvent`` (trade: :class:`.Trade`, fill: :class:`.Fill`) 410 | * ``commissionReportEvent`` (trade: :class:`.Trade`, 411 | fill: :class:`.Fill`, commissionReport: :class:`.CommissionReport`) 412 | * ``filledEvent`` (trade: :class:`.Trade`) 413 | * ``cancelEvent`` (trade: :class:`.Trade`) 414 | * ``cancelledEvent`` (trade: :class:`.Trade`) 415 | """ 416 | 417 | contract: Contract = field(default_factory=Contract) 418 | order: Order = field(default_factory=Order) 419 | orderStatus: OrderStatus = field(default_factory=OrderStatus) 420 | fills: list[Fill] = field(default_factory=list) 421 | log: list[TradeLogEntry] = field(default_factory=list) 422 | advancedError: str = "" 423 | 424 | # TODO: replace these with an enum? 425 | events: ClassVar = ( 426 | "statusEvent", 427 | "modifyEvent", 428 | "fillEvent", 429 | "commissionReportEvent", 430 | "filledEvent", 431 | "cancelEvent", 432 | "cancelledEvent", 433 | ) 434 | 435 | def __post_init__(self): 436 | self.statusEvent = Event("statusEvent") 437 | self.modifyEvent = Event("modifyEvent") 438 | self.fillEvent = Event("fillEvent") 439 | self.commissionReportEvent = Event("commissionReportEvent") 440 | self.filledEvent = Event("filledEvent") 441 | self.cancelEvent = Event("cancelEvent") 442 | self.cancelledEvent = Event("cancelledEvent") 443 | 444 | def isWaiting(self) -> bool: 445 | """True if sent to IBKR but not "Submitted" for live execution yet.""" 446 | return self.orderStatus.status in OrderStatus.WaitingStates 447 | 448 | def isWorking(self) -> bool: 449 | """True if sent to IBKR but not "Submitted" for live execution yet.""" 450 | return self.orderStatus.status in OrderStatus.WorkingStates 451 | 452 | def isActive(self) -> bool: 453 | """True if eligible for execution, false otherwise.""" 454 | return self.orderStatus.status in OrderStatus.ActiveStates 455 | 456 | def isDone(self) -> bool: 457 | """True if completely filled or cancelled, false otherwise.""" 458 | return self.orderStatus.status in OrderStatus.DoneStates 459 | 460 | def filled(self) -> float: 461 | """Number of shares filled.""" 462 | fills = self.fills 463 | if self.contract.secType == "BAG": 464 | # don't count fills for the leg contracts 465 | fills = [f for f in fills if f.contract.secType == "BAG"] 466 | 467 | return sum([f.execution.shares for f in fills]) 468 | 469 | def remaining(self) -> float: 470 | """Number of shares remaining to be filled.""" 471 | return float(self.order.totalQuantity) - self.filled() 472 | 473 | 474 | class BracketOrder(NamedTuple): 475 | parent: Order 476 | takeProfit: Order 477 | stopLoss: Order 478 | 479 | 480 | @dataclass 481 | class OrderCondition: 482 | @staticmethod 483 | def createClass(condType): 484 | d = { 485 | 1: PriceCondition, 486 | 3: TimeCondition, 487 | 4: MarginCondition, 488 | 5: ExecutionCondition, 489 | 6: VolumeCondition, 490 | 7: PercentChangeCondition, 491 | } 492 | return d[condType] 493 | 494 | def And(self): 495 | self.conjunction = "a" 496 | return self 497 | 498 | def Or(self): 499 | self.conjunction = "o" 500 | return self 501 | 502 | 503 | @dataclass 504 | class PriceCondition(OrderCondition): 505 | condType: int = 1 506 | conjunction: str = "a" 507 | isMore: bool = True 508 | price: float = 0.0 509 | conId: int = 0 510 | exch: str = "" 511 | triggerMethod: int = 0 512 | 513 | 514 | @dataclass 515 | class TimeCondition(OrderCondition): 516 | condType: int = 3 517 | conjunction: str = "a" 518 | isMore: bool = True 519 | time: str = "" 520 | 521 | 522 | @dataclass 523 | class MarginCondition(OrderCondition): 524 | condType: int = 4 525 | conjunction: str = "a" 526 | isMore: bool = True 527 | percent: int = 0 528 | 529 | 530 | @dataclass 531 | class ExecutionCondition(OrderCondition): 532 | condType: int = 5 533 | conjunction: str = "a" 534 | secType: str = "" 535 | exch: str = "" 536 | symbol: str = "" 537 | 538 | 539 | @dataclass 540 | class VolumeCondition(OrderCondition): 541 | condType: int = 6 542 | conjunction: str = "a" 543 | isMore: bool = True 544 | volume: int = 0 545 | conId: int = 0 546 | exch: str = "" 547 | 548 | 549 | @dataclass 550 | class PercentChangeCondition(OrderCondition): 551 | condType: int = 7 552 | conjunction: str = "a" 553 | isMore: bool = True 554 | changePercent: float = 0.0 555 | conId: int = 0 556 | exch: str = "" 557 | --------------------------------------------------------------------------------