Skip to content

Commit

Permalink
Merge pull request #566 from mraniki/dev
Browse files Browse the repository at this point in the history
💥 breaking changing ibkr connectivity and lib
  • Loading branch information
mraniki authored Feb 11, 2025
2 parents be8a2da + a2d8803 commit a2c146d
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 171 deletions.
316 changes: 146 additions & 170 deletions cefi/handler/ibkr.py
Original file line number Diff line number Diff line change
@@ -1,233 +1,209 @@
"""
Interactive Brokers client
Interactive Brokers client using ibind library
"""

from ib_insync import IB, IBC, Contract, Order
import asyncio

from ibind import IbkrClient, QuestionType, StockQuery, make_order_request
from loguru import logger

from ._client import CexClient


class IbHandler(CexClient):
"""
CEX client for IBKR
CEX client for IBKR using ibind library
Args:
None
Returns:
None
"""

def __init__(
self,
**kwargs,
):
def __init__(self, **kwargs):
"""
Initializes the Broker_IBKR_Plugin class.
This function creates an instance of the IB class from the ib_insync
library and sets it as the 'ib' attribute of the class.
It also sets the 'client' attribute to None as a placeholder
for actual client details.
Initializes the IBKR client using ibind
"""
super().__init__(**kwargs)
self.client = None
self.account_number = None

To connect to the Interactive Brokers (IBKR) platform,
the 'connect' method of the 'ib' instance is called.
This method requires the host, port, and clientId as parameters.
In this case, the function connects to the IBKR platform using
the IP address "127.0.0.1", port 7497, and clientId 1.
if not self.enabled:
return

After successfully connecting to IBKR, the function logs
a debug message using the logger module.
try:
# Initialize IbkrClient with IBeam gateway
self.url = self.url or "http://ibeam:5000/v1/api/"
self.client = IbkrClient(url=self.url)

# Verify connection
tickle_result = self.client.tickle()
if (
not tickle_result.data.get("iserver", {})
.get("authStatus", {})
.get("authenticated", False)
):
raise ConnectionError("Failed to authenticate with IBKR")

For IBC gateway setup,
refer to https://github.com/IbcAlpha/IBC/blob/master/userguide.md
# Get account number
accounts = self.client.portfolio_accounts().data
if not accounts:
raise ValueError("No trading accounts found")
self.account_number = accounts[0]["accountId"]

"""
try:
super().__init__(**kwargs)
if self.enabled:
if self.broker_gateway:
ibc = IBC(
976,
gateway=True,
tradingMode="paper" if self.testmode else "live",
userid=self.user_id,
password=self.password,
)
ibc.start()
IB.run()
self.client = IB()
self.client.connect(
host=self.host,
port=self.port,
clientId=self.broker_client_id or 1,
readonly=False,
account=self.broker_account_number or "",
)
self.name = self.client.id
self.account_number = self.client.managedAccounts()[0]
logger.debug("Connected to IBKR {}", self.client.isConnected())
logger.debug("Broker_IBKR initialized with account: {}", self.account)
logger.debug("Connected to IBKR successfully")
logger.debug(f"Account number: {self.account_number}")

except Exception as e:
logger.error("IBC Initialization Error {}", e)
return None
logger.error(f"IbkrClient initialization error: {e}")
raise

async def _async_request(self, fn, *args, **kwargs):
"""Helper to run synchronous ibind calls in thread pool"""
return await asyncio.to_thread(fn, *args, **kwargs)

async def get_info(self):
"""
Retrieves information from the accountValues method of the `ib` object.
Returns:
The result of calling the accountValues method of the `ib` object.
Retrieves account information
"""
return self.client.accountValues()
result = await self._async_request(
self.client.portfolio_account_summary, self.account_number
)
return result.data

async def get_quote(self, instrument):
"""
Return a quote for a instrument
of a given ib object
Args:
cex
instrument
Returns:
quote
Get market data snapshot for instrument
"""
try:
instrument = await self.replace_instrument(instrument)
conid = await self.search_contract(instrument)

if contract := self.search_contract(instrument):
self.client.reqMktData(contract)
quote = self.client.ticker(contract)
logger.debug("Quote: {}", quote)
return quote
if not conid:
logger.warning(f"No contract found for {instrument}")
return None

# Get real-time market data (adjust fields as needed)
result = await self._async_request(
self.client.marketdata_snapshot,
conid,
fields=["31", "84", "86"], # bid, ask, last price
)
return result.data

except Exception as e:
logger.error("{} Error {}", self.name, e)
logger.error(f"Error getting quote: {e}")
return None

async def get_account_balance(self):
"""
return account balance
Args:
None
Returns:
balance
Get account balance summary
"""

return self.client.accountSummary(self.account)
result = await self._async_request(
self.client.portfolio_account_summary, self.account_number
)
return result.data

async def get_account_position(self):
"""
Return account position.
Args:
None
Returns:
position
Get current positions
"""
result = await self._async_request(
self.client.portfolio_positions, self.account_number
)
return result.data

async def search_contract(self, instrument):
"""
Find contract CONID using symbol and mapping configuration
"""
try:
return self.client.positions()
except Exception as e:
logger.error("{} Error {}", self.name, e)

async def pre_order_checks(self, order_params):
""" """
return True
# Find instrument in mapping
asset = next(
(
item
for item in self.mapping
if item["id"] == instrument or item["alt"] == instrument
),
None,
)

async def get_trading_asset_balance(self):
""" """
return self.client.accountSummary(self.account_number)
if not asset:
logger.warning(f"Instrument {instrument} not found in mapping")
return None

# Build stock query from mapping
query = StockQuery(
symbol=asset["id"],
contract_conditions={
"exchange": asset.get("exchange", "SMART"),
"currency": asset.get("currency", "USD"),
"secType": asset.get("type", "STK"),
},
)

async def execute_order(self, order_params):
"""
Execute order
# Get CONID
result = await self._async_request(
self.client.stock_conid_by_symbol, query, default_filtering=True
)

Args:
order_params (dict):
action(str)
instrument(str)
quantity(int)
return result.data.get(instrument)

Returns:
trade_confirmation(dict)
except Exception as e:
logger.error(f"Contract search error: {e}")
return None

async def execute_order(self, order_params):
"""
Execute order using IBKR REST API
"""
try:
action = order_params.get("action")
instrument = await self.replace_instrument(order_params.get("instrument"))
quantity = order_params.get("quantity", self.trading_risk_amount)
logger.debug("quantity {}", quantity)
amount = await self.get_order_amount(
quantity=quantity,
instrument=instrument,
is_percentage=self.trading_risk_percentage,
instrument = await self.replace_instrument(order_params["instrument"])
conid = await self.search_contract(instrument)

if not conid:
return {"error": f"Contract not found for {instrument}"}

# Create order request
order_request = make_order_request(
conid=conid,
side=order_params["action"].upper(),
quantity=order_params.get("quantity", 1),
order_type=order_params.get("order_type", "MKT"),
acct_id=self.account_number,
price=order_params.get("price"),
coid=order_params.get("client_order_id"),
)

logger.debug("amount {}", amount)
pre_order_checks = await self.pre_order_checks(order_params)
logger.debug("pre_order_checks {}", pre_order_checks)
# Define standard answers for order questions
answers = {
QuestionType.PRICE_PERCENTAGE_CONSTRAINT: True,
QuestionType.ORDER_VALUE_LIMIT: True,
"_DEFAULT": True, # Accept any unexpected questions
}

if amount and pre_order_checks:
if contract := self.search_contract(instrument):
order = Order()
order.action = order_params["action"]
order.orderType = order_params["order_type"] or "MKT"
order.totalQuantity = amount
trade = self.client.placeOrder(contract, order)
return await self.get_trade_confirmation(trade, instrument, action)
# Place order
result = await self._async_request(
self.client.place_order, order_request, answers=answers
)

return f"Error executing {self.name}"
if result.data.get("order_id"):
return {
"status": "success",
"order_id": result.data["order_id"],
"details": result.data,
}
return {"status": "error", "message": result.data}

except Exception as e:
logger.error("{} Error {}", self.name, e)
return f"Error executing {self.name}"
logger.error(f"Order execution error: {e}")
return {"error": str(e)}

async def search_contract(self, instrument):
"""
Asynchronously searches for a contract based on the given instrument.
Args:
self: The object instance.
instrument: The instrument to search for.
Returns:
Contract: The contract matching the instrument, or None if not found.
"""
try:
if asset := next(
(
item
for item in self.mapping
if item["id"] == instrument or item["alt"] == instrument
),
None,
):
return Contract(
secType=asset["type"],
symbol=asset["id"],
lastTradeDateOrContractMonth=asset["lastTradeDateOrContractMonth"],
strike=asset["strike"],
right=asset["right"],
multiplier=asset["multiplier"],
exchange=asset["exchange"],
currency=asset["currency"],
)
logger.warning("Asset {} not found in mapping", instrument)
return None
async def get_trading_asset_balance(self):
"""Alias for get_account_balance"""
return await self.get_account_balance()

except Exception as e:
logger.error("search_contract {} Error {}", instrument, e)
async def pre_order_checks(self, order_params):
"""Implement custom pre-trade checks if needed"""
return True
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ ccxt = "^4.2.80"
capitalcom-python = "0.2.3"
degiro-connector = "3.0.23"
easyoanda = {version ="1.0.19", python = ">=3.11,<4.0"}
ib_insync = "0.9.86"
ibind = "0.1.9"
# ib_insync = "0.9.86"
# mt5linux = "0.1.9"
mt5linux_updated = "0.0.1"
# pyetrade = {version ="2.1.0"}
Expand Down

0 comments on commit a2c146d

Please sign in to comment.