Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update IB documentation and misc enhancements / bug fixes #1336

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 222 additions & 72 deletions docs/integrations/ib.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/tutorials/backtest_high_level.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,4 @@ node = BacktestNode(configs=[config])

results = node.run()
results
```
```
15 changes: 14 additions & 1 deletion examples/live/interactive_brokers/historic_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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],
)
Expand All @@ -66,6 +77,8 @@ async def main():
instrument_ids=[instrument_id],
)

gateway.stop()

catalog = ParquetDataCatalog("./catalog")
catalog.write_data(instruments)
catalog.write_data(bars)
Expand Down
8 changes: 0 additions & 8 deletions examples/notebooks/backtest_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,6 @@
"source": [
"result"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "af22401c-4d5b-4a58-bb18-97f460cb284c",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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}")

Expand Down
10 changes: 8 additions & 2 deletions nautilus_trader/adapters/interactive_brokers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion nautilus_trader/adapters/interactive_brokers/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__(
Expand Down
3 changes: 2 additions & 1 deletion nautilus_trader/adapters/interactive_brokers/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand Down
66 changes: 40 additions & 26 deletions nautilus_trader/adapters/interactive_brokers/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -45,34 +43,44 @@ 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}

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")
Expand Down Expand Up @@ -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,
Expand All @@ -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}",
Expand All @@ -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:
Expand All @@ -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 -----------------------------------------------------------------------------------
Expand Down
64 changes: 5 additions & 59 deletions nautilus_trader/adapters/interactive_brokers/historic/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
-------
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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())
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading