Skip to content

Refactor IB adapter #2647

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

Merged
merged 2 commits into from
Jun 7, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@
"BTC/USD.PAXOS",
"SPY.ARCA",
"V.NYSE",
"YMH24.CBOT",
"CLZ27.NYMEX",
"ESZ27.CME",
"YMH4.CBOT",
"CLZ7.NYMEX",
"ESZ7.CME",
],
),
load_contracts=frozenset(ib_contracts),
Expand Down
4 changes: 2 additions & 2 deletions examples/live/interactive_brokers/connect_with_tws.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
"SPY.ARCA",
"AAPL.NASDAQ",
"V.NYSE",
"CLZ28.NYMEX",
"ESZ28.CME",
"CLZ8.NYMEX",
"ESZ8.CME",
],
),
)
Expand Down
4 changes: 2 additions & 2 deletions examples/live/interactive_brokers/historical_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async def main(
)

trade_ticks = await client.request_ticks(
"TRADES",
tick_type="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",
Expand All @@ -83,7 +83,7 @@ async def main(
)

quote_ticks = await client.request_ticks(
"BID_ASK",
tick_type="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",
Expand Down
2 changes: 1 addition & 1 deletion examples/live/interactive_brokers/with_databento_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
ibg_client_id=1,
account_id="DU123456", # This must match with the IB Gateway/TWS node is connecting to
instrument_provider=InteractiveBrokersInstrumentProviderConfig(
symbology_method=SymbologyMethod.DATABENTO,
symbology_method=SymbologyMethod.IB_SIMPLIFIED,
load_ids=frozenset(instrument_ids),
),
routing=RoutingConfig(
Expand Down
249 changes: 249 additions & 0 deletions examples/live/interactive_brokers/with_databento_instrument_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# ---
# jupyter:
# jupytext:
# formats: py:percent
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.17.2
# kernelspec:
# display_name: Python 3 (ipykernel)
# language: python
# name: python3
# ---

# %%
# fmt: off
import os

from nautilus_trader.adapters.interactive_brokers.common import IB
from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE
from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersDataClientConfig
from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersExecClientConfig
from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig
from nautilus_trader.adapters.interactive_brokers.config import SymbologyMethod
from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveDataClientFactory
from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveExecClientFactory
from nautilus_trader.common.enums import LogColor
from nautilus_trader.config import LiveDataEngineConfig
from nautilus_trader.config import LoggingConfig
from nautilus_trader.config import RoutingConfig
from nautilus_trader.config import TradingNodeConfig
from nautilus_trader.live.node import TradingNode
from nautilus_trader.model.data import Bar
from nautilus_trader.model.data import BarType
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.enums import TimeInForce
from nautilus_trader.model.events import PositionOpened
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.trading.config import StrategyConfig
from nautilus_trader.trading.strategy import Strategy


# fmt: on


# %%
class DemoStrategyConfig(StrategyConfig, frozen=True):
bar_type: BarType
instrument_id: InstrumentId


class DemoStrategy(Strategy):
def __init__(self, config: DemoStrategyConfig):
super().__init__(config=config)

# Track if we've already placed an order
self.order_placed = False

# Track total bars seen
self.count_of_bars: int = 0
self.show_portfolio_at_bar: int | None = 0

def on_start(self):
"""
Handle strategy start event.
"""
self.request_instrument(self.config.instrument_id)
self.instrument = self.cache.instrument(self.config.instrument_id)

self.request_bars(BarType.from_str(f"{self.config.instrument_id}-1-MINUTE-LAST-EXTERNAL"))

# Subscribe to market data
self.subscribe_bars(self.config.bar_type)

# Show initial portfolio state
self.show_portfolio_info("Portfolio state (Before trade)")

def on_bar(self, bar: Bar):
"""
Handle new bar event.
"""
# Increment total bars seen
self.count_of_bars += 1

# Show portfolio state if we reached target bar
if self.show_portfolio_at_bar == self.count_of_bars:
self.show_portfolio_info("Portfolio state (2 minutes after position opened)")

# Only place one order for demonstration
if not self.order_placed:
# Prepare values for order
last_price = bar.close
tick_size = self.instrument.price_increment
profit_price = self.instrument.make_price(last_price + (10 * tick_size))
stoploss_price = self.instrument.make_price(last_price - (10 * tick_size))

# Create BUY MARKET order with PT and SL (both 10 ticks)
bracket_order_list = self.order_factory.bracket(
instrument_id=self.config.instrument_id,
order_side=OrderSide.BUY,
quantity=self.instrument.make_qty(1), # Trade size: 1 contract
time_in_force=TimeInForce.GTC,
tp_price=profit_price,
sl_trigger_price=stoploss_price,
entry_post_only=False,
tp_post_only=False,
)

# Submit order and remember it
self.submit_order_list(bracket_order_list)
self.order_placed = True
self.log.info(f"Submitted bracket order: {bracket_order_list}", color=LogColor.GREEN)

def on_position_opened(self, event: PositionOpened):
"""
Handle position opened event.
"""
# Log position details
self.log.info(f"Position opened: {event}", color=LogColor.GREEN)

# Show portfolio state when position is opened
self.show_portfolio_info("Portfolio state (In position):")

# Set target bar number for next portfolio display
self.show_portfolio_at_bar = self.count_of_bars + 2 # Show after 2 bars

def on_stop(self):
"""
Handle strategy stop event.
"""
# Show final portfolio state
self.show_portfolio_info("Portfolio state (After trade)")

def show_portfolio_info(self, intro_message: str = ""):
"""
Display current portfolio information.
"""
if intro_message:
self.log.info(f"====== {intro_message} ======")

# POSITION information
self.log.info("Portfolio -> Position information:", color=LogColor.BLUE)
is_flat = self.portfolio.is_flat(self.config.instrument_id)
self.log.info(f"Is flat: {is_flat}", color=LogColor.BLUE)

net_position = self.portfolio.net_position(self.config.instrument_id)
self.log.info(f"Net position: {net_position} contract(s)", color=LogColor.BLUE)

net_exposure = self.portfolio.net_exposure(self.config.instrument_id)
self.log.info(f"Net exposure: {net_exposure}", color=LogColor.BLUE)

# -----------------------------------------------------

# P&L information
self.log.info("Portfolio -> P&L information:", color=LogColor.YELLOW)

realized_pnl = self.portfolio.realized_pnl(self.config.instrument_id)
self.log.info(f"Realized P&L: {realized_pnl}", color=LogColor.YELLOW)

unrealized_pnl = self.portfolio.unrealized_pnl(self.config.instrument_id)
self.log.info(f"Unrealized P&L: {unrealized_pnl}", color=LogColor.YELLOW)

# -----------------------------------------------------

self.log.info("Portfolio -> Account information:", color=LogColor.CYAN)
margins_init = self.portfolio.margins_init(IB_VENUE)
self.log.info(f"Initial margin: {margins_init}", color=LogColor.CYAN)

margins_maint = self.portfolio.margins_maint(IB_VENUE)
self.log.info(f"Maintenance margin: {margins_maint}", color=LogColor.CYAN)

balances_locked = self.portfolio.balances_locked(IB_VENUE)
self.log.info(f"Locked balance: {balances_locked}", color=LogColor.CYAN)


# %%
# Tested instrument id
instrument_id = "ES.XCME" # "^SPX.XCBO", "ES.XCME", "AAPL.XNAS", "YMM5.XCBT"

instrument_provider = InteractiveBrokersInstrumentProviderConfig(
symbology_method=SymbologyMethod.IB_SIMPLIFIED,
convert_exchange_to_mic_venue=True,
load_ids=frozenset(
[
instrument_id,
],
),
)

# Configure the trading node
# IMPORTANT: you must use the imported IB string so this client works properly
config_node = TradingNodeConfig(
trader_id="TESTER-001",
logging=LoggingConfig(log_level="INFO"),
data_clients={
IB: InteractiveBrokersDataClientConfig(
ibg_port=7497,
handle_revised_bars=False,
use_regular_trading_hours=False,
instrument_provider=instrument_provider,
),
},
exec_clients={
IB: InteractiveBrokersExecClientConfig(
ibg_port=7497,
instrument_provider=instrument_provider,
routing=RoutingConfig(default=True),
account_id=os.environ["TWS_ACCOUNT"],
),
},
data_engine=LiveDataEngineConfig(
time_bars_timestamp_on_close=False, # Will use opening time as `ts_event` (same as IB)
validate_data_sequence=True, # Will make sure DataEngine discards any Bars received out of sequence
),
timeout_connection=90.0,
timeout_reconciliation=5.0,
timeout_portfolio=5.0,
timeout_disconnection=5.0,
timeout_post_stop=2.0,
)


# Instantiate the node with a configuration
node = TradingNode(config=config_node)

# Instantiate your strategy
strategy_config = DemoStrategyConfig(
bar_type=BarType.from_str(f"{instrument_id}-1-MINUTE-LAST-EXTERNAL"),
instrument_id=InstrumentId.from_str(instrument_id),
)
strategy = DemoStrategy(config=strategy_config)

# Add your strategies and modules
node.trader.add_strategy(strategy)

# Register your client factories with the node (can take user-defined factories)
node.add_data_client_factory(IB, InteractiveBrokersLiveDataClientFactory)
node.add_exec_client_factory(IB, InteractiveBrokersLiveExecClientFactory)
node.build()

# %%
node.run()

# %%
node.stop()

# %%
node.dispose()
3 changes: 3 additions & 0 deletions nautilus_trader/adapters/interactive_brokers/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ def __init__(
self._order_id_to_order_ref: dict[int, AccountOrderRef] = {}
self._next_valid_order_id: int = -1

# Instrument provider (set by data/execution clients during connection)
self._instrument_provider = None

# Start client
self._request_id_seq: int = 10000

Expand Down
3 changes: 3 additions & 0 deletions nautilus_trader/adapters/interactive_brokers/client/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@ class BaseMixin:
_port: int
_client_id: int
_requests: Requests
_instrument_provider: (
Any # InteractiveBrokersInstrumentProvider | None - Will be set by data/execution client
)
_subscriptions: Subscriptions
_event_subscriptions: dict[str, Callable]
_eclient: EClient
Expand Down
Loading
Loading