diff --git a/docs/integrations/ib.md b/docs/integrations/ib.md index 3af85cbfaa95..a4a8a09bb684 100644 --- a/docs/integrations/ib.md +++ b/docs/integrations/ib.md @@ -1,110 +1,260 @@ # Interactive Brokers -NautilusTrader offers an adapter for integrating with the Interactive Brokers Gateway via -[ibapi](https://github.com/nautechsystems/ibapi). +Interactive Brokers (IB) is a trading platform that allows trading in a wide range of financial instruments, including stocks, options, futures, currencies, bonds, funds, and cryptocurrencies. NautilusTrader offers an adapter to integrate with IB using their [Trader Workstation (TWS) API](https://interactivebrokers.github.io/tws-api/index.html) through their Python library, [ibapi](https://github.com/nautechsystems/ibapi). -**Note**: If you are planning on using the built-in docker TWS Gateway when using the Interactive Brokers adapter, -you must ensure the `docker` package is installed. Run `poetry install --extras "ib docker"` -or `poetry install --all-extras` inside your environment to ensure the necessary packages are installed. +The TWS API serves as an interface to IB's standalone trading applications: TWS and IB Gateway. Both can be downloaded from the IB website. If you haven't installed TWS or IB Gateway yet, refer to the [Initial Setup](https://interactivebrokers.github.io/tws-api/initial_setup.html) guide. In NautilusTrader, you'll establish a connection to one of these applications via the `InteractiveBrokersClient`. + +Alternatively, you can start with a [dockerized version](https://github.com/gnzsnz/ib-gateway-docker) of the IB Gateway, particularly useful when deploying trading strategies on a hosted cloud platform. This requires having [Docker](https://www.docker.com/) installed on your machine, along with the [docker](https://pypi.org/project/docker/) Python package, which NautilusTrader conveniently includes as an extra package. + +**Note**: The standalone TWS and IB Gateway applications necessitate manual input of username, password, and trading mode (live or paper) at startup. The dockerized version of the IB Gateway handles these steps programmatically. + +## Installation + +To install the latest nautilus-trader package along with the `ibapi` and optional `docker` dependencies using pip, execute: + +``` +pip install -U "nautilus_trader[ib,docker]" +``` + +For installation via poetry, use: + +``` +poetry add "nautilus_trader[ib,docker]" +``` + +**Note**: Because IB does not provide wheels for `ibapi`, NautilusTrader [repackages]( https://pypi.org/project/nautilus-ibapi/) it for release on PyPI. + + +## Getting Started + +Before deploying strategies, ensure that TWS / IB Gateway is running. Launch one of the standalone applications and log in with your credentials, or start the dockerized IB Gateway using `InteractiveBrokersGateway`: + +```python +from nautilus_trader.adapters.interactive_brokers.gateway import InteractiveBrokersGateway + + +gateway_config = InteractiveBrokersGatewayConfig( + username="test", + password="test", + trading_mode="paper", + start=True +) + +# This may take a short while to start up, especially the first time +gateway = InteractiveBrokersGateway( + config=gateway_config +) + +# Confirm you are logged in +print(gateway.is_logged_in(gateway.container)) + +# Inspect the logs +print(gateway.container.logs()) +``` + +**Note**: To supply credentials to the Interactive Brokers Gateway, either pass the `username` and `password` to the config dictionaries, or set the following environment variables: +- `TWS_USERNAME` +- `TWS_PASSWORD` ## Overview -The following adapter classes are available: -- `InteractiveBrokersInstrumentProvider` which allows querying Interactive Brokers for instruments. -- `InteractiveBrokersDataClient` which connects to the `Gateway` and streams market data. -- `InteractiveBrokersExecutionClient` which allows the retrieval of account information and execution of orders. +The adapter includes several major components: +- `InteractiveBrokersClient`: Executes TWS API requests using `ibapi`. +- `HistoricInteractiveBrokersClient`: Provides methods for retrieving instruments and historical data, useful for backtesting. +- `InteractiveBrokersInstrumentProvider`: Retrieves or queries instruments for trading. +- `InteractiveBrokersDataClient`: Connects to the Gateway for streaming market data. +- `InteractiveBrokersExecutionClient`: Handles account information and executes trades. -## Instruments -Interactive Brokers allows searching for instruments via the `qualifyContracts` API, which, if given enough information -can usually resolve a filter into an actual contract(s). A node can request instruments to be loaded by passing -configuration to the `InstrumentProviderConfig` when initialising a `TradingNodeConfig` (note that while `filters` -is a dict, it must be converted to a tuple when passed to `InstrumentProviderConfig`). +## Instruments & Contracts -At a minimum, you must specify the `secType` (security type) and `symbol` (equities etc) or `pair` (FX). See examples -queries below for common use cases +In IB, a NautilusTrader `Instrument` is equivalent to a [Contract](https://interactivebrokers.github.io/tws-api/contracts.html). Contracts can be either a [basic contract](https://interactivebrokers.github.io/tws-api/classIBApi_1_1Contract.html) or a more [detailed](https://interactivebrokers.github.io/tws-api/classIBApi_1_1ContractDetails.html) version (ContractDetails). The adapter models these using `IBContract` and `IBContractDetails` classes. The latter includes critical data like order types and trading hours, which are absent in the basic contract. As a result, `IBContractDetails` can be converted to an `Instrument` while `IBContract` cannot. -Example config: +To search for contract information, use the [IB Contract Information Center](https://pennies.interactivebrokers.com/cstools/contract_info/). +Examples of `IBContracts`: ```python from nautilus_trader.adapters.interactive_brokers.common import IBContract -from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersDataClientConfig + +# Stock +IBContract(secType='STK', exchange='SMART', primaryExchange='ARCA', symbol='SPY') + +# Bond +IBContract(secType='BOND', secIdType='ISIN', secId='US03076KAA60') + +# Option +IBContract(secType='STK', exchange='SMART', primaryExchange='ARCA', symbol='SPY', lastTradeDateOrContractMonth='20251219', build_options_chain=True) + +# CFD +IBContract(secType='CFD', symbol='IBUS30') + +# Future +IBContract(secType='CONTFUT', exchange='CME', symbol='ES', build_futures_chain=True) + +# Forex +IBContract(secType='CASH', exchange='IDEALPRO', symbol='EUR', currency='GBP') + +# Crypto +IBContract(secType='CRYPTO', symbol='ETH', exchange='PAXOS', currency='USD') +``` + +## Historical Data & Backtesting + +When developing strategies with the IB adapter, the first step usually involves acquiring historical data for backtesting. The `HistoricInteractiveBrokersClient` offers methods to request and save this data. + +Here's an example of retrieving and saving instrument and bar data. A more comprehensive example is available [here](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/interactive_brokers/historic_download.py). + +```python +import datetime +from nautilus_trader.adapters.interactive_brokers.common import IBContract +from nautilus_trader.adapters.interactive_brokers.historic import HistoricInteractiveBrokersClient +from nautilus_trader.persistence.catalog import ParquetDataCatalog + + +async def main(): + contract = IBContract( + secType="STK", + symbol="AAPL", + exchange="SMART", + primaryExchange="NASDAQ", + ) + client = HistoricInteractiveBrokersClient() + + instruments = await client.request_instruments( + contracts=[contract], + ) + + bars = await client.request_bars( + bar_specifications=["1-HOUR-LAST", "30-MINUTE-MID"], + end_date_time=datetime.datetime(2023, 11, 6, 16, 30), + tz_name="America/New_York", + duration="1 D", + contracts=[contract], + ) + + catalog = ParquetDataCatalog("./catalog") + catalog.write_data(instruments) + catalog.write_data(bars) +``` + +## Live Trading + +Engaging in live or paper trading requires constructing and running a `TradingNode`. This node incorporates both `InteractiveBrokersDataClient` and `InteractiveBrokersExecutionClient`, which depend on the `InteractiveBrokersInstrumentProvider` to operate. + +### InstrumentProvider + +The `InteractiveBrokersInstrumentProvider` class functions as a bridge for accessing financial instrument data from IB. Configurable through `InteractiveBrokersInstrumentProviderConfig`, it allows for the customization of various instrument type parameters. Additionally, this provider offers specialized methods to build and retrieve the entire futures and options chains. + +```python from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig -from nautilus_trader.config import TradingNodeConfig -config_node = TradingNodeConfig( - data_clients={ - "IB": InteractiveBrokersDataClientConfig( - instrument_provider=InteractiveBrokersInstrumentProviderConfig( - load_ids={"EUR/USD.IDEALPRO", "AAPL.NASDAQ"}, - load_contracts={IBContract(secType="CONTFUT", exchange="CME", symbol="MES")}, - ) + +instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( + build_futures_chain=False, # Set to True if fetching futures + build_options_chain=False, # Set to True if fetching options + min_expiry_days=10, # Relevant for futures/options with expiration + max_expiry_days=60, # Relevant for futures/options with expiration + load_ids=frozenset( + [ + "EUR/USD.IDEALPRO", + "BTC/USD.PAXOS", + "SPY.ARCA", + "V.NYSE", + "YMH24.CBOT", + "CLZ27.NYMEX", + "ESZ27.CME", + ], + ), + load_contracts=frozenset( + [ + IBContract(secType='STK', symbol='SPY', exchange='SMART', primaryExchange='ARCA'), + IBContract(secType='STK', symbol='AAPL', exchange='SMART', primaryExchange='NASDAQ') + ] ), - ... ) ``` -### Examples queries -- Stock: `IBContract(secType='STK', exchange='SMART', symbol='AMD', currency='USD')` -- Stock: `IBContract(secType='STK', exchange='SMART', primaryExchange='NASDAQ', symbol='INTC')` -- Forex: `InstrumentId('EUR/USD.IDEALPRO')`, `InstrumentId('USD/JPY.IDEALPRO')` -- CFD: `IBContract(secType='CFD', symbol='IBUS30')` -- Future: `InstrumentId("YMH24.CBOT")`, `InstrumentId("CLZ27.NYMEX")`, `InstrumentId("ESZ27.CME")`, `InstrumentId('ES.CME')`, `IBContract(secType='CONTFUT', exchange='CME', symbol='ES', build_futures_chain=True)` -- Option: `InstrumentId('SPY251219C00395000.SMART')`, `IBContract(secType='STK', exchange='SMART', primaryExchange='ARCA', symbol='SPY', lastTradeDateOrContractMonth='20251219', build_options_chain=True)` -- Bond: `IBContract(secType='BOND', secIdType='ISIN', secId='US03076KAA60')` -- Crypto: `InstrumentId('BTC/USD.PAXOS')` +### Data Client + +`InteractiveBrokersDataClient` interfaces with IB for streaming and retrieving market data. Upon connection, it configures the [market data type](https://interactivebrokers.github.io/tws-api/market_data_type.html) and loads instruments based on the settings in `InteractiveBrokersInstrumentProviderConfig`. This client can subscribe to and unsubscribe from various market data types, including quote ticks, trade ticks, and bars. + +Configurable through `InteractiveBrokersDataClientConfig`, it allows adjustments for handling revised bars, trading hours preferences, and market data types (e.g., `IBMarketDataTypeEnum.REALTIME` or `IBMarketDataTypeEnum.DELAYED_FROZEN`). + +```python +from nautilus_trader.adapters.interactive_brokers.config import IBMarketDataTypeEnum +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersDataClientConfig + + +data_client_config = InteractiveBrokersDataClientConfig( + ibg_port=4002, + handle_revised_bars=False, + use_regular_trading_hours=True, + market_data_type=IBMarketDataTypeEnum.DELAYED_FROZEN, # Default is REALTIME if not set + instrument_provider=instrument_provider_config, + gateway=gateway_config, +) +``` +### Execution Client -## Configuration -The most common use case is to configure a live `TradingNode` to include Interactive Brokers -data and execution clients. To achieve this, add an `IB` section to your client -configuration(s) and set the environment variables to your TWS (Traders Workstation) credentials: +The `InteractiveBrokersExecutionClient` facilitates executing trades, accessing account information, and processing order and trade-related details. It encompasses a range of methods for order management, including reporting order statuses, placing new orders, and modifying or canceling existing ones. Additionally, it generates position reports, although trade reports are not yet implemented. ```python -import os from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersExecClientConfig -from nautilus_trader.config import TradingNodeConfig +from nautilus_trader.config import RoutingConfig -config = TradingNodeConfig( - data_clients={ - "IB": InteractiveBrokersDataClientConfig( - username=os.getenv("TWS_USERNAME"), - password=os.getenv("TWS_PASSWORD"), - ... # Omitted - }, - exec_clients = { - "IB": InteractiveBrokersExecClientConfig( - username=os.getenv("TWS_USERNAME"), - password=os.getenv("TWS_PASSWORD"), - ... # Omitted - }, - ... # Omitted +exec_client_config = InteractiveBrokersExecClientConfig( + ibg_port=4002, + account_id="DU123456", # Must match the connected IB Gateway/TWS + gateway=gateway_config, + instrument_provider=instrument_provider_config, + routing=RoutingConfig( + default=True, + ) ) ``` -Then, create a `TradingNode` and add the client factories: +### Full Configuration + +Setting up a complete trading environment typically involves configuring a `TradingNodeConfig`, which includes data and execution client configurations. Additional configurations are specified in `LiveDataEngineConfig` to accommodate IB-specific requirements. A `TradingNode` is then instantiated from these configurations, and factories for creating `InteractiveBrokersDataClient` and `InteractiveBrokersExecutionClient` are added. Finally, the node is built and run. + +For a comprehensive example, refer to this [script](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/interactive_brokers/interactive_brokers_example.py). + ```python +from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveDataClientFactory from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveExecClientFactory +from nautilus_trader.config import LiveDataEngineConfig +from nautilus_trader.config import LoggingConfig +from nautilus_trader.config import TradingNodeConfig +from nautilus_trader.live.node import TradingNode + -# Instantiate the live trading node with a configuration -node = TradingNode(config=config) +# ... [continuing from prior example code] ... -# Register the client factories with the node +# Configure the trading node +config_node = TradingNodeConfig( + trader_id="TESTER-001", + logging=LoggingConfig(log_level="INFO"), + data_clients={"IB": data_client_config}, + exec_clients={"IB": exec_client_config}, + data_engine=LiveDataEngineConfig( + time_bars_timestamp_on_close=False, # Use opening time as `ts_event`, as per IB standard + validate_data_sequence=True, # Discards bars received out of sequence + ), +) + +node = TradingNode(config=config_node) node.add_data_client_factory("IB", InteractiveBrokersLiveDataClientFactory) node.add_exec_client_factory("IB", InteractiveBrokersLiveExecClientFactory) - -# Finally build the node node.build() -``` +node.portfolio.set_specific_venue(IB_VENUE) -### API credentials -There are two options for supplying your credentials to the Interactive Brokers clients. -Either pass the corresponding `username` and `password` values to the config dictionaries, or -set the following environment variables: -- `TWS_USERNAME` -- `TWS_PASSWORD` - -When starting the trading node, you'll receive immediate confirmation of whether your -credentials are valid and have trading permissions. +if __name__ == "__main__": + try: + node.run() + finally: + # Stop and dispose of the node with SIGINT/CTRL+C + node.dispose() +``` diff --git a/docs/tutorials/backtest_high_level.md b/docs/tutorials/backtest_high_level.md index 6162fb256125..b4454f4ca45b 100644 --- a/docs/tutorials/backtest_high_level.md +++ b/docs/tutorials/backtest_high_level.md @@ -178,4 +178,4 @@ node = BacktestNode(configs=[config]) results = node.run() results -``` +``` \ No newline at end of file diff --git a/examples/live/interactive_brokers/historic_download.py b/examples/live/interactive_brokers/historic_download.py index afcd2980ad0f..e7af08ba7829 100644 --- a/examples/live/interactive_brokers/historic_download.py +++ b/examples/live/interactive_brokers/historic_download.py @@ -15,13 +15,24 @@ # ------------------------------------------------------------------------------------------------- import asyncio import datetime +import os from nautilus_trader.adapters.interactive_brokers.common import IBContract +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersGatewayConfig +from nautilus_trader.adapters.interactive_brokers.gateway import InteractiveBrokersGateway from nautilus_trader.adapters.interactive_brokers.historic import HistoricInteractiveBrokersClient from nautilus_trader.persistence.catalog import ParquetDataCatalog async def main(): + gateway_config = InteractiveBrokersGatewayConfig( + username=os.environ["TWS_USERNAME"], + password=os.environ["TWS_PASSWORD"], + port=4002, + ) + gateway = InteractiveBrokersGateway(config=gateway_config) + gateway.start() + contract = IBContract( secType="STK", symbol="AAPL", @@ -41,9 +52,9 @@ async def main(): bars = await client.request_bars( bar_specifications=["1-HOUR-LAST", "30-MINUTE-MID"], + start_date_time=datetime.datetime(2023, 11, 6, 9, 30), end_date_time=datetime.datetime(2023, 11, 6, 16, 30), tz_name="America/New_York", - duration="1 D", contracts=[contract], instrument_ids=[instrument_id], ) @@ -66,6 +77,8 @@ async def main(): instrument_ids=[instrument_id], ) + gateway.stop() + catalog = ParquetDataCatalog("./catalog") catalog.write_data(instruments) catalog.write_data(bars) diff --git a/examples/notebooks/backtest_example.ipynb b/examples/notebooks/backtest_example.ipynb index f27db49c8893..e13608ab4924 100644 --- a/examples/notebooks/backtest_example.ipynb +++ b/examples/notebooks/backtest_example.ipynb @@ -168,14 +168,6 @@ "source": [ "result" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af22401c-4d5b-4a58-bb18-97f460cb284c", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 20952aa43d05..922368248573 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -1298,6 +1298,7 @@ async def get_historical_ticks( start_date_time: pd.Timestamp | str = "", end_date_time: pd.Timestamp | str = "", use_rth: bool = True, + timeout: int = 60, ): if isinstance(start_date_time, pd.Timestamp): start_date_time = start_date_time.strftime("%Y%m%d %H:%M:%S %Z") @@ -1325,7 +1326,7 @@ async def get_historical_ticks( cancel=functools.partial(self._client.cancelHistoricalData, reqId=req_id), ) request.handle() - return await self._await_request(request, 60) + return await self._await_request(request, timeout) else: self._log.info(f"Request already exist for {request}") diff --git a/nautilus_trader/adapters/interactive_brokers/config.py b/nautilus_trader/adapters/interactive_brokers/config.py index 43f762479335..514e79f7be09 100644 --- a/nautilus_trader/adapters/interactive_brokers/config.py +++ b/nautilus_trader/adapters/interactive_brokers/config.py @@ -36,6 +36,10 @@ class InteractiveBrokersGatewayConfig(NautilusConfig, frozen=True): password : str, optional The Interactive Brokers account password. If ``None`` then will source the `TWS_PASSWORD`. + host : str, optional + The hostname or ip address for the IB Gateway or TWS. + port : int, optional + The port for the gateway server ("paper" 4002, or "live" 4001). trading_mode: str paper or live. start: bool, optional @@ -49,6 +53,8 @@ class InteractiveBrokersGatewayConfig(NautilusConfig, frozen=True): username: str | None = None password: str | None = None + host: str | None = "127.0.0.1" + port: Literal[4001, 4002] | None = None trading_mode: Literal["paper", "live"] = "paper" start: bool = False read_only_api: bool = True @@ -155,8 +161,8 @@ class InteractiveBrokersExecClientConfig(LiveExecClientConfig, frozen=True): ---------- ibg_host : str, default "127.0.0.1" The hostname or ip address for the IB Gateway or TWS. - ibg_port : int, default for "paper" 4002, or "live" 4001 - The port for the gateway server. + ibg_port : int + The port for the gateway server ("paper" 4002, or "live" 4001). ibg_client_id: int, default 1 The client_id to be passed into connect call. ibg_account_id : str diff --git a/nautilus_trader/adapters/interactive_brokers/data.py b/nautilus_trader/adapters/interactive_brokers/data.py index 4fac7a13133a..f96ed0764327 100644 --- a/nautilus_trader/adapters/interactive_brokers/data.py +++ b/nautilus_trader/adapters/interactive_brokers/data.py @@ -49,7 +49,8 @@ class InteractiveBrokersDataClient(LiveMarketDataClient): """ - Provides a data client for the InteractiveBrokers exchange. + Provides a data client for the InteractiveBrokers exchange by using the `Gateway` to + stream market data. """ def __init__( diff --git a/nautilus_trader/adapters/interactive_brokers/execution.py b/nautilus_trader/adapters/interactive_brokers/execution.py index c4d9cb9f92bb..e10ad9e5cb49 100644 --- a/nautilus_trader/adapters/interactive_brokers/execution.py +++ b/nautilus_trader/adapters/interactive_brokers/execution.py @@ -96,7 +96,8 @@ class InteractiveBrokersExecutionClient(LiveExecutionClient): """ - Provides an execution client for Interactive Brokers TWS API. + Provides an execution client for Interactive Brokers TWS API, allowing for the + retrieval of account information and execution of orders. Parameters ---------- diff --git a/nautilus_trader/adapters/interactive_brokers/gateway.py b/nautilus_trader/adapters/interactive_brokers/gateway.py index 074a2bbe10a9..58519f1841ff 100644 --- a/nautilus_trader/adapters/interactive_brokers/gateway.py +++ b/nautilus_trader/adapters/interactive_brokers/gateway.py @@ -15,19 +15,17 @@ import logging import os -import warnings from enum import IntEnum from time import sleep -from typing import ClassVar +from typing import ClassVar, Literal + +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersGatewayConfig try: import docker except ImportError as e: - warnings.warn( - f"Docker required for Gateway, install manually via `pip install docker` ({e})", - ) - docker = None + raise RuntimeError("Docker required for Gateway, install via `pip install docker`") from e class ContainerStatus(IntEnum): @@ -45,7 +43,7 @@ class InteractiveBrokersGateway: A class to manage starting an Interactive Brokers Gateway docker container. """ - IMAGE: ClassVar[str] = "ghcr.io/unusualalpha/ib-gateway:10.19" + IMAGE: ClassVar[str] = "ghcr.io/gnzsnz/ib-gateway:stable" CONTAINER_NAME: ClassVar[str] = "nautilus-ib-gateway" PORTS: ClassVar[dict[str, int]] = {"paper": 4002, "live": 4001} @@ -53,26 +51,36 @@ def __init__( self, username: str | None = None, password: str | None = None, - host: str | None = "localhost", + host: str | None = "127.0.0.1", port: int | None = None, - trading_mode: str | None = "paper", + trading_mode: Literal["paper", "live"] | None = "paper", start: bool = False, read_only_api: bool = True, timeout: int = 90, logger: logging.Logger | None = None, + config: InteractiveBrokersGatewayConfig | None = None, ): - username = username if username is not None else os.environ["TWS_USERNAME"] - password = password if password is not None else os.environ["TWS_PASSWORD"] - assert username is not None, "`username` not set nor available in env `TWS_USERNAME`" - assert password is not None, "`password` not set nor available in env `TWS_PASSWORD`" - self.username = username - self.password = password + if config: + username = config.username + password = config.password + host = config.host + port = config.port + trading_mode = config.trading_mode + start = config.start + read_only_api = config.read_only_api + timeout = config.timeout + + self.username = username or os.getenv("TWS_USERNAME") + self.password = password or os.getenv("TWS_PASSWORD") + if self.username is None: + raise ValueError("`username` not set nor available in env `TWS_USERNAME`") + if self.password is None: + raise ValueError("`password` not set nor available in env `TWS_PASSWORD`") + self.trading_mode = trading_mode self.read_only_api = read_only_api self.host = host self.port = port or self.PORTS[trading_mode] - if docker is None: - raise RuntimeError("Docker not installed") self._docker = docker.from_env() self._container = None self.log = logger or logging.getLogger("nautilus_trader") @@ -150,7 +158,11 @@ def start(self, wait: int | None = 90): name=f"{self.CONTAINER_NAME}-{self.port}", restart_policy={"Name": "always"}, detach=True, - ports={str(self.port): self.PORTS[self.trading_mode], str(self.port + 100): "5900"}, + ports={ + "4003": (self.host, 4001), + "4004": (self.host, 4002), + "5900": (self.host, 5900), + }, platform="amd64", environment={ "TWS_USERID": self.username, @@ -165,11 +177,10 @@ def start(self, wait: int | None = 90): for _ in range(wait): if self.is_logged_in(container=self._container): break - else: - self.log.debug("Waiting for IB Gateway to start ..") - sleep(1) + self.log.debug("Waiting for IB Gateway to start ..") + sleep(1) else: - raise GatewayLoginFailure + raise RuntimeError(f"Gateway `{self.CONTAINER_NAME}-{self.port}` not ready") self.log.info( f"Gateway `{self.CONTAINER_NAME}-{self.port}` ready. VNC port is {self.port+100}", @@ -178,8 +189,8 @@ def start(self, wait: int | None = 90): def safe_start(self, wait: int = 90): try: self.start(wait=wait) - except ContainerExists: - return + except docker.errors.APIError as e: + raise RuntimeError("Container already exists") from e def stop(self): if self.container: @@ -189,8 +200,11 @@ def stop(self): def __enter__(self): self.start() - def __exit__(self, type, value, traceback): - self.stop() + def __exit__(self, exc_type, exc_val, exc_tb): + try: + self.stop() + except Exception as e: + logging.error("Error stopping container: %s", e) # -- Exceptions ----------------------------------------------------------------------------------- diff --git a/nautilus_trader/adapters/interactive_brokers/historic/client.py b/nautilus_trader/adapters/interactive_brokers/historic/client.py index e4faeb6e3b85..ba1b960c0aba 100644 --- a/nautilus_trader/adapters/interactive_brokers/historic/client.py +++ b/nautilus_trader/adapters/interactive_brokers/historic/client.py @@ -31,7 +31,6 @@ from nautilus_trader.model.identifiers import TraderId from nautilus_trader.model.instruments.base import Instrument from nautilus_trader.msgbus.bus import MessageBus -from nautilus_trader.persistence.catalog import ParquetDataCatalog class HistoricInteractiveBrokersClient: @@ -213,7 +212,7 @@ async def request_bars( ): self.log.info( f"{instrument_id}: Requesting historical bars: {bar_type} ending on '{segment_end_date_time}' " - "with duration '{segment_duration}'", + f"with duration '{segment_duration}'", ) bars = await self._client.get_historical_bars( @@ -244,6 +243,7 @@ async def request_ticks( contracts: list[IBContract] | None = None, instrument_ids: list[str] | None = None, use_rth: bool = True, + timeout: int = 60, ) -> list[TradeTick | QuoteTick]: """ Return TradeTicks or QuoteTicks for one or more bar specifications for a list of @@ -265,6 +265,8 @@ async def request_ticks( Instrument IDs (e.g. AAPL.NASDAQ) defining which ticks to retrieve. use_rth : bool, default 'True' Whether to use regular trading hours. + timeout : int, default '60' + The timeout in seconds for each request. Returns ------- @@ -317,6 +319,7 @@ async def request_ticks( tick_type=tick_type, start_date_time=current_start_date_time, use_rth=use_rth, + timeout=timeout, ) if not ticks: @@ -484,60 +487,3 @@ def _calculate_duration_segments( results.append((minus_days_date, f"{seconds} S")) return results - - -# will remove this post testing and review -async def main(): - contract = IBContract( - secType="STK", - symbol="AAPL", - exchange="SMART", - primaryExchange="NASDAQ", - ) - instrument_id = "TSLA.NASDAQ" - - client = HistoricInteractiveBrokersClient(port=4002, client_id=5) - await client._connect() - await asyncio.sleep(2) - - instruments = await client.request_instruments( - contracts=[contract], - instrument_ids=[instrument_id], - ) - - bars = await client.request_bars( - bar_specifications=["1-DAY-LAST", "8-HOUR-MID"], - start_date_time=datetime.datetime(2022, 10, 15, 3), - end_date_time=datetime.datetime(2023, 11, 1), - tz_name="America/New_York", - contracts=[contract], - instrument_ids=[instrument_id], - ) - - trade_ticks = await client.request_ticks( - "TRADES", - start_date_time=datetime.datetime(2023, 11, 6, 10, 0), - end_date_time=datetime.datetime(2023, 11, 6, 10, 1), - tz_name="America/New_York", - contracts=[contract], - instrument_ids=[instrument_id], - ) - - quote_ticks = await client.request_ticks( - "BID_ASK", - start_date_time=datetime.datetime(2023, 11, 6, 10, 0), - end_date_time=datetime.datetime(2023, 11, 6, 10, 1), - tz_name="America/New_York", - contracts=[contract], - instrument_ids=[instrument_id], - ) - - catalog = ParquetDataCatalog("./catalog") - catalog.write_data(instruments) - catalog.write_data(bars) - catalog.write_data(trade_ticks) - catalog.write_data(quote_ticks) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py index abe248d55681..2bd8a61d27d1 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py @@ -20,6 +20,7 @@ import msgspec import pandas as pd +from ibapi.contract import ContractDetails # fmt: off from nautilus_trader.adapters.interactive_brokers.common import IBContract @@ -118,6 +119,12 @@ def sec_type_to_asset_class(sec_type: str) -> AssetClass: return asset_class_from_str(mapping.get(sec_type, sec_type)) +def contract_details_to_ib_contract_details(details: ContractDetails) -> IBContractDetails: + details.contract = IBContract(**details.contract.__dict__) + details = IBContractDetails(**details.__dict__) + return details + + def parse_instrument( contract_details: IBContractDetails, ) -> Instrument: diff --git a/nautilus_trader/persistence/wranglers_v2.py b/nautilus_trader/persistence/wranglers_v2.py index 8847ee71a388..95d5781fda65 100644 --- a/nautilus_trader/persistence/wranglers_v2.py +++ b/nautilus_trader/persistence/wranglers_v2.py @@ -29,7 +29,6 @@ from nautilus_trader.core.nautilus_pyo3 import QuoteTickDataWrangler as RustQuoteTickDataWrangler from nautilus_trader.core.nautilus_pyo3 import TradeTick as RustTradeTick from nautilus_trader.core.nautilus_pyo3 import TradeTickDataWrangler as RustTradeTickDataWrangler -from nautilus_trader.model.data import BarType from nautilus_trader.model.instruments import Instrument @@ -413,8 +412,13 @@ class BarDataWrangler(WranglerBase): Parameters ---------- - instrument : Instrument - The instrument for the data wrangler. + bar_type : str + The bar type for the data wrangler. For example, + "GBP/USD.SIM-1-MINUTE-BID-EXTERNAL" + price_precision: int + The price precision for the data wrangler. + size_precision: int + The size precision for the data wrangler. Warnings -------- @@ -425,7 +429,7 @@ class BarDataWrangler(WranglerBase): def __init__( self, - bar_type: BarType, + bar_type: str, price_precision: int, size_precision: int, ) -> None: @@ -471,7 +475,7 @@ def from_pandas( Returns ------- list[RustBar] - A list of PyO3 [pyclass] `TradeTick` objects. + A list of PyO3 [pyclass] `Bar` objects. """ # Rename column