Skip to content

Commit 3c1c534

Browse files
committed
feat: add Interactive Brokers TWS broker integration
- add IB TWS broker adapter using ib_insync - wire IB TWS mode into broker initialization - add IB host/port/clientId/account settings - add broker connection test button in settings - add ib-insync dependency to requirements and pyproject
1 parent 35294cd commit 3c1c534

6 files changed

Lines changed: 389 additions & 3 deletions

File tree

forexsmartbot/adapters/brokers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
from .paper_broker import PaperBroker
44
from .mt4_broker import MT4Broker
55
from .rest_broker import RestBroker
6+
from .ib_tws_broker import IBTWSBroker
67

7-
__all__ = ['PaperBroker', 'MT4Broker', 'RestBroker']
8+
__all__ = ['PaperBroker', 'MT4Broker', 'RestBroker', 'IBTWSBroker']
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""Interactive Brokers TWS broker adapter."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Dict, Optional, Tuple
6+
7+
from ...core.interfaces import IBroker, Position
8+
9+
10+
class IBTWSBroker(IBroker):
11+
"""Interactive Brokers broker via TWS / IB Gateway using ib_insync."""
12+
13+
def __init__(
14+
self,
15+
host: str = "127.0.0.1",
16+
port: int = 7497,
17+
client_id: int = 1,
18+
account_id: str = "",
19+
):
20+
self._host = host
21+
self._port = int(port)
22+
self._client_id = int(client_id)
23+
self._account_id = account_id
24+
self._connected = False
25+
self._contracts: Dict[str, object] = {}
26+
self._orders: Dict[str, Tuple[str, float]] = {}
27+
self._ib = None
28+
29+
try:
30+
from ib_insync import IB
31+
32+
self._ib = IB()
33+
except Exception:
34+
self._ib = None
35+
36+
def connect(self) -> bool:
37+
if self._ib is None:
38+
print("IBTWSBroker: ib_insync is not installed. Install with: pip install ib_insync")
39+
return False
40+
try:
41+
self._ib.connect(self._host, self._port, clientId=self._client_id, timeout=10)
42+
self._connected = bool(self._ib.isConnected())
43+
return self._connected
44+
except Exception as e:
45+
print(f"IBTWSBroker: Connection failed: {e}")
46+
self._connected = False
47+
return False
48+
49+
def disconnect(self) -> None:
50+
try:
51+
if self._ib is not None and self._ib.isConnected():
52+
self._ib.disconnect()
53+
finally:
54+
self._connected = False
55+
56+
def is_connected(self) -> bool:
57+
return self._connected and self._ib is not None and self._ib.isConnected()
58+
59+
def _get_contract(self, symbol: str):
60+
if symbol in self._contracts:
61+
return self._contracts[symbol]
62+
63+
from ib_insync import Forex
64+
65+
normalized = symbol.replace("=", "").replace("/", "").upper()
66+
if normalized.endswith("X"):
67+
normalized = normalized[:-1]
68+
if len(normalized) != 6:
69+
raise ValueError(f"Unsupported symbol format for IB Forex: {symbol}")
70+
71+
contract = Forex(normalized)
72+
self._ib.qualifyContracts(contract)
73+
self._contracts[symbol] = contract
74+
return contract
75+
76+
def get_price(self, symbol: str) -> Optional[float]:
77+
if not self.is_connected():
78+
return None
79+
try:
80+
contract = self._get_contract(symbol)
81+
ticker = self._ib.reqMktData(contract, "", False, False)
82+
self._ib.sleep(0.5)
83+
84+
if ticker and ticker.last and ticker.last > 0:
85+
return float(ticker.last)
86+
if ticker and ticker.bid and ticker.ask and ticker.bid > 0 and ticker.ask > 0:
87+
return float((ticker.bid + ticker.ask) / 2.0)
88+
if ticker and ticker.close and ticker.close > 0:
89+
return float(ticker.close)
90+
return None
91+
except Exception as e:
92+
print(f"IBTWSBroker: get_price failed for {symbol}: {e}")
93+
return None
94+
95+
def submit_order(
96+
self,
97+
symbol: str,
98+
side: int,
99+
quantity: float,
100+
stop_loss: Optional[float] = None,
101+
take_profit: Optional[float] = None,
102+
) -> Optional[str]:
103+
if not self.is_connected():
104+
return None
105+
try:
106+
from ib_insync import LimitOrder, MarketOrder, StopOrder
107+
108+
contract = self._get_contract(symbol)
109+
action = "BUY" if side > 0 else "SELL"
110+
exit_action = "SELL" if action == "BUY" else "BUY"
111+
order_qty = float(abs(quantity))
112+
if order_qty <= 0:
113+
return None
114+
115+
order = MarketOrder(action, order_qty, transmit=True)
116+
if self._account_id:
117+
order.account = self._account_id
118+
119+
# If SL/TP are provided, place as linked child orders for risk control.
120+
if stop_loss is not None or take_profit is not None:
121+
order.transmit = False
122+
123+
trade = self._ib.placeOrder(contract, order)
124+
self._ib.sleep(0.5)
125+
126+
order_id = str(trade.order.orderId)
127+
self._orders[order_id] = (symbol, order_qty)
128+
129+
if stop_loss is not None:
130+
sl_order = StopOrder(
131+
action=exit_action,
132+
totalQuantity=order_qty,
133+
stopPrice=float(stop_loss),
134+
parentId=trade.order.orderId,
135+
transmit=(take_profit is None),
136+
)
137+
if self._account_id:
138+
sl_order.account = self._account_id
139+
self._ib.placeOrder(contract, sl_order)
140+
141+
if take_profit is not None:
142+
tp_order = LimitOrder(
143+
action=exit_action,
144+
totalQuantity=order_qty,
145+
lmtPrice=float(take_profit),
146+
parentId=trade.order.orderId,
147+
transmit=True,
148+
)
149+
if self._account_id:
150+
tp_order.account = self._account_id
151+
self._ib.placeOrder(contract, tp_order)
152+
153+
self._ib.sleep(0.3)
154+
return order_id
155+
except Exception as e:
156+
print(f"IBTWSBroker: submit_order failed for {symbol}: {e}")
157+
return None
158+
159+
def close_all(self, symbol: str) -> bool:
160+
if not self.is_connected():
161+
return False
162+
try:
163+
from ib_insync import MarketOrder
164+
165+
closed_any = False
166+
target = symbol.replace("=", "").replace("/", "").upper()
167+
if target.endswith("X"):
168+
target = target[:-1]
169+
170+
for pos in self._ib.positions():
171+
local_symbol = (pos.contract.localSymbol or "").replace(".", "").upper()
172+
con_symbol = f"{pos.contract.symbol}{getattr(pos.contract, 'currency', '')}".upper()
173+
if target not in (local_symbol, con_symbol):
174+
continue
175+
176+
qty = float(pos.position)
177+
if qty == 0:
178+
continue
179+
action = "SELL" if qty > 0 else "BUY"
180+
order = MarketOrder(action, abs(qty))
181+
if self._account_id:
182+
order.account = self._account_id
183+
self._ib.placeOrder(pos.contract, order)
184+
closed_any = True
185+
186+
if closed_any:
187+
self._ib.sleep(0.5)
188+
return closed_any
189+
except Exception as e:
190+
print(f"IBTWSBroker: close_all failed for {symbol}: {e}")
191+
return False
192+
193+
def get_positions(self) -> Dict[str, Position]:
194+
positions: Dict[str, Position] = {}
195+
if not self.is_connected():
196+
return positions
197+
198+
try:
199+
for pos in self._ib.positions():
200+
qty = float(pos.position)
201+
if qty == 0:
202+
continue
203+
204+
symbol = f"{pos.contract.symbol}{getattr(pos.contract, 'currency', '')}"
205+
market_price = self.get_price(symbol) or float(pos.avgCost)
206+
side = 1 if qty > 0 else -1
207+
unrealized = side * abs(qty) * (market_price - float(pos.avgCost))
208+
209+
positions[symbol] = Position(
210+
symbol=symbol,
211+
side=side,
212+
quantity=abs(qty),
213+
entry_price=float(pos.avgCost),
214+
current_price=float(market_price),
215+
unrealized_pnl=float(unrealized),
216+
)
217+
except Exception as e:
218+
print(f"IBTWSBroker: get_positions failed: {e}")
219+
220+
return positions
221+
222+
def get_balance(self) -> float:
223+
if not self.is_connected():
224+
return 0.0
225+
try:
226+
values = self._ib.accountSummary()
227+
for row in values:
228+
if row.tag == "TotalCashValue" and (not self._account_id or row.account == self._account_id):
229+
return float(row.value)
230+
except Exception as e:
231+
print(f"IBTWSBroker: get_balance failed: {e}")
232+
return 0.0
233+
234+
def get_equity(self) -> float:
235+
if not self.is_connected():
236+
return 0.0
237+
try:
238+
values = self._ib.accountSummary()
239+
for row in values:
240+
if row.tag == "NetLiquidation" and (not self._account_id or row.account == self._account_id):
241+
return float(row.value)
242+
except Exception as e:
243+
print(f"IBTWSBroker: get_equity failed: {e}")
244+
return 0.0

forexsmartbot/ui/enhanced_main_window.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from ..services.controller import TradingController
2727
from ..services.persistence import SettingsManager, DatabaseManager, LogManager
2828
from ..strategies import get_strategy, list_strategies
29-
from ..adapters.brokers import PaperBroker, MT4Broker, RestBroker
29+
from ..adapters.brokers import PaperBroker, MT4Broker, RestBroker, IBTWSBroker
3030
from ..adapters.data import YFinanceProvider, CSVProvider, MultiProvider, AlphaVantageProvider, OANDAProvider
3131
from .settings_dialog import SettingsDialog
3232
from .backtest_dialog import BacktestDialog
@@ -1141,6 +1141,20 @@ def initialize_broker(self):
11411141
from forexsmartbot.adapters.brokers.rest_broker import RestBroker
11421142
self.broker = RestBroker(api_key=api_key, api_secret=api_secret, base_url=base_url)
11431143
self.append_log("REST API broker initialized")
1144+
elif broker_mode == 'IB TWS':
1145+
ib_host = self.settings_manager.get('ib_host', '127.0.0.1')
1146+
ib_port = int(self.settings_manager.get('ib_port', 7497))
1147+
ib_client_id = int(self.settings_manager.get('ib_client_id', 1))
1148+
ib_account_id = self.settings_manager.get('ib_account_id', '')
1149+
self.broker = IBTWSBroker(
1150+
host=ib_host,
1151+
port=ib_port,
1152+
client_id=ib_client_id,
1153+
account_id=ib_account_id,
1154+
)
1155+
self.append_log(
1156+
f"IB TWS broker initialized: {ib_host}:{ib_port}, clientId={ib_client_id}"
1157+
)
11441158
else:
11451159
self.broker = PaperBroker(10000.0)
11461160
self.append_log("Unknown broker mode - using paper broker")
@@ -2044,6 +2058,8 @@ def update_broker_status(self, broker_mode: str):
20442058
self.trading_status_widget.broker_status.setStyleSheet("color: orange; font-weight: bold;")
20452059
elif broker_mode == "REST API":
20462060
self.trading_status_widget.broker_status.setStyleSheet("color: green; font-weight: bold;")
2061+
elif broker_mode == "IB TWS":
2062+
self.trading_status_widget.broker_status.setStyleSheet("color: purple; font-weight: bold;")
20472063
else:
20482064
self.trading_status_widget.broker_status.setStyleSheet("color: gray; font-weight: bold;")
20492065

0 commit comments

Comments
 (0)