-
Notifications
You must be signed in to change notification settings - Fork 0
Add IBKR client helpers for bracket and OCA orders #6
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
Open
AlstonLi007
wants to merge
1
commit into
main
Choose a base branch
from
codex/add-methods-to-ibkr_client.py
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # OpenBB Lab Execution Helpers | ||
|
|
||
| This repository exposes a small helper module under | ||
| `openbb_lab.execution` that enriches the [`ib_insync`](https://ib-insync.readthedocs.io) | ||
| client with OpenBB-friendly models. The `IBKRClient` entry points provide | ||
| normalised output via `OrderTicket` objects so that downstream consumers can | ||
| serialize the responses or store them without depending on the raw | ||
| `ib_insync` objects. | ||
|
|
||
| ## Newly added helpers | ||
|
|
||
| | Helper | Description | | ||
| | --- | --- | | ||
| | `create_bracket` | Build and transmit a bracket order (entry, take-profit, stop-loss) and return normalised tickets. | | ||
| | `create_oca` | Submit a one-cancels-all order group with consistent identifiers. | | ||
| | `modify_order` | Apply updates to an existing trade/order and receive the updated ticket. | | ||
| | `ibkr_stream_trades` | Async iterator yielding normalised `OrderTicket` instances whenever IBKR notifies about a trade update. | | ||
| | `req_market_data` | Request streaming or snapshot market data for a contract and return a light-weight summary payload. | | ||
| | `ensure_permissions` | Simple guard that asserts the current connection exposes the requested managed accounts. | | ||
| | `set_client_ratelimits` | Apply pacing/ratelimit options to the underlying client connection. | | ||
| | `ibkr_health` | Provide a diagnostic snapshot suitable for readiness/health checks. | | ||
|
|
||
| Each helper is documented in `openbb_lab/execution/ibkr_client.py` with | ||
| usage details and parameter descriptions. | ||
|
|
||
| ## Examples | ||
|
|
||
| The module level docstring of `openbb_lab.execution.ibkr_client` contains a | ||
| complete snippet for creating a bracket order. Additional stubs that can be | ||
| used as starting points for unit tests live in `tests/test_ibkr_client.py`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """OpenBB Lab execution package.""" | ||
|
|
||
| from .execution.ibkr_client import IBKRClient | ||
|
|
||
| __all__ = ["IBKRClient"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,289 @@ | ||
| """Interactive Brokers (IBKR) execution helpers built on top of ``ib_insync``. | ||
|
|
||
| The :class:`IBKRClient` class provided here wraps an active ``ib_insync.IB`` | ||
| connection and exposes high-level helpers that return the normalized models | ||
| from :mod:`openbb_lab.execution.models`. These helpers provide a consistent | ||
| schema for downstream services and notebooks while keeping the direct | ||
| ``ib_insync`` dependency isolated in a single place. | ||
|
|
||
| Examples | ||
| -------- | ||
| The following snippet illustrates how to place a bracket order using the | ||
| client. The example is also mirrored in :mod:`tests.test_ibkr_client` as a | ||
| usage stub. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from ib_insync import IB, Stock | ||
| from openbb_lab.execution import IBKRClient | ||
|
|
||
| ib = IB().connect("127.0.0.1", 7497, clientId=1) | ||
| client = IBKRClient(ib) | ||
| contract = Stock("AAPL", "SMART", "USD") | ||
| tickets = client.create_bracket( | ||
| contract=contract, | ||
| action="BUY", | ||
| quantity=10, | ||
| limit_price=175.5, | ||
| take_profit_price=180.0, | ||
| stop_loss_price=170.0, | ||
| tif="GTC", | ||
| ) | ||
| for ticket in tickets: | ||
| print(ticket.identifier, ticket.status) | ||
|
|
||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| from typing import Any, AsyncIterator, Dict, List, Optional, Sequence | ||
|
|
||
| from ib_insync import Contract, IB, Order, Trade | ||
|
|
||
| from .models import ContractModel, OrderTicket, normalize_tif, normalize_trades | ||
|
|
||
| __all__ = ["IBKRClient"] | ||
|
|
||
|
|
||
| class IBKRClient: | ||
| """A thin convenience wrapper around :class:`ib_insync.IB`. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| ib: | ||
| An active ``ib_insync.IB`` connection. | ||
| """ | ||
|
|
||
| def __init__(self, ib: IB): | ||
| self._ib = ib | ||
|
|
||
| # --------------------------------------------------------------------- | ||
| # Internal utilities | ||
| # ------------------------------------------------------------------ | ||
| def _ensure_connection(self) -> None: | ||
| if not self._ib.isConnected(): | ||
| raise RuntimeError("IBKR client is not connected") | ||
|
|
||
| def _normalize_trade(self, trade: Trade) -> OrderTicket: | ||
| return OrderTicket.from_trade(trade) | ||
|
|
||
| def _apply_tif(self, order: Order, tif: Optional[str]) -> None: | ||
| if tif is None: | ||
| return | ||
| order.tif = normalize_tif(tif) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Public API | ||
| # ------------------------------------------------------------------ | ||
| def create_bracket( | ||
| self, | ||
| contract: Contract, | ||
| *, | ||
| action: str, | ||
| quantity: float, | ||
| limit_price: float, | ||
| take_profit_price: Optional[float] = None, | ||
| stop_loss_price: Optional[float] = None, | ||
| tif: Optional[str] = None, | ||
| transmit: bool = True, | ||
| **kwargs: Any, | ||
| ) -> List[OrderTicket]: | ||
| """Submit a bracket order and return the resulting order tickets. | ||
|
|
||
| The method is a structured wrapper around | ||
| :meth:`ib_insync.IB.bracketOrder`. Returned orders are immediately | ||
| transmitted using :meth:`ib_insync.IB.placeOrder` and normalised into | ||
| :class:`OrderTicket` instances. | ||
| """ | ||
|
|
||
| self._ensure_connection() | ||
| parent, take_profit, stop_loss = self._ib.bracketOrder( | ||
| action=action, | ||
| quantity=quantity, | ||
| limitPrice=limit_price, | ||
| takeProfitPrice=take_profit_price, | ||
| stopLossPrice=stop_loss_price, | ||
| **kwargs, | ||
| ) | ||
|
|
||
| for order in (parent, take_profit, stop_loss): | ||
| if order is None: | ||
| continue | ||
| self._apply_tif(order, tif) | ||
|
|
||
| active_orders = [o for o in (parent, take_profit, stop_loss) if o is not None] | ||
| tickets: List[OrderTicket] = [] | ||
| for idx, order in enumerate(active_orders): | ||
| order.transmit = transmit and idx == len(active_orders) - 1 | ||
| trade = self._ib.placeOrder(contract, order) | ||
| tickets.append(self._normalize_trade(trade)) | ||
| return tickets | ||
|
|
||
| def create_oca( | ||
| self, | ||
| contract: Contract, | ||
| orders: Sequence[Order], | ||
| *, | ||
| group: str, | ||
| oca_type: int = 3, | ||
| tif: Optional[str] = None, | ||
| transmit: bool = True, | ||
| ) -> List[OrderTicket]: | ||
| """Submit an OCA (one-cancels-all) order set. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| contract: | ||
| The qualified contract the orders apply to. | ||
| orders: | ||
| A sequence of pre-configured ``ib_insync.Order`` objects. | ||
| group: | ||
| The OCA group identifier that will be assigned to each order. | ||
| oca_type: | ||
| Matches the IB API ``ocaType`` flag. The default of ``3`` mirrors | ||
| the behaviour in TWS/IBKR mobile where the remaining orders are | ||
| cancelled once one order fills. | ||
| tif: | ||
| Optional time-in-force override applied to each order. | ||
| transmit: | ||
| When ``True`` (default) the final order will be transmitted. | ||
| """ | ||
|
|
||
| self._ensure_connection() | ||
| tickets: List[OrderTicket] = [] | ||
| for index, order in enumerate(orders): | ||
| order.ocaGroup = group | ||
| order.ocaType = oca_type | ||
| order.transmit = transmit and index == len(orders) - 1 | ||
| self._apply_tif(order, tif) | ||
| trade = self._ib.placeOrder(contract, order) | ||
| tickets.append(self._normalize_trade(trade)) | ||
| return tickets | ||
|
|
||
| def modify_order( | ||
| self, | ||
| trade: Trade, | ||
| *, | ||
| tif: Optional[str] = None, | ||
| **updates: Any, | ||
| ) -> OrderTicket: | ||
| """Modify an active order and return the updated ticket.""" | ||
|
|
||
| self._ensure_connection() | ||
| order = trade.order | ||
| for key, value in updates.items(): | ||
| setattr(order, key, value) | ||
| self._apply_tif(order, tif) | ||
| self._ib.modifyOrder(order) | ||
| return self._normalize_trade(trade) | ||
|
|
||
| async def ibkr_stream_trades(self) -> AsyncIterator[OrderTicket]: | ||
| """Yield trades in real-time using ``ib_insync`` trade events.""" | ||
|
|
||
| self._ensure_connection() | ||
| queue: "asyncio.Queue[Trade]" = asyncio.Queue() | ||
|
|
||
| def _handler(trade: Trade, fill: Any) -> None: # pragma: no cover - event callback | ||
| queue.put_nowait(trade) | ||
|
|
||
| self._ib.tradeEvent += _handler | ||
| try: | ||
| while True: | ||
| trade = await queue.get() | ||
| yield self._normalize_trade(trade) | ||
| finally: | ||
| self._ib.tradeEvent -= _handler | ||
|
|
||
| def req_market_data( | ||
| self, | ||
| contract: Contract, | ||
| *, | ||
| generic_tick_list: str = "", | ||
| snapshot: bool = False, | ||
| regulatory_snapshot: bool = False, | ||
| mkt_data_options: Optional[Sequence[Any]] = None, | ||
| ) -> Dict[str, Any]: | ||
| """Request market data for the given contract.""" | ||
|
|
||
| self._ensure_connection() | ||
| ticker = self._ib.reqMktData( | ||
| contract, | ||
| generic_tick_list, | ||
| snapshot=snapshot, | ||
| regulatorySnapshot=regulatory_snapshot, | ||
| mktDataOptions=mkt_data_options, | ||
| ) | ||
| contract_model = ( | ||
| ContractModel.from_ib(ticker.contract) | ||
| if getattr(ticker, "contract", None) | ||
| else ContractModel.from_ib(contract) | ||
| ) | ||
| return { | ||
| "contract": contract_model.__dict__, | ||
| "last": getattr(ticker, "last", None), | ||
| "close": getattr(ticker, "close", None), | ||
| "bid": getattr(ticker, "bid", None), | ||
| "ask": getattr(ticker, "ask", None), | ||
| "volume": getattr(ticker, "volume", None), | ||
| } | ||
|
|
||
| def ensure_permissions(self, *, permissions: Optional[Sequence[str]] = None) -> None: | ||
| """Verify that the current connection has the requested permissions.""" | ||
|
|
||
| self._ensure_connection() | ||
| try: | ||
| accounts = set(self._ib.managedAccounts()) | ||
| except Exception as error: # pragma: no cover - defensive guard | ||
| raise PermissionError("Unable to retrieve managed accounts") from error | ||
|
|
||
| if permissions: | ||
| missing = [perm for perm in permissions if perm not in accounts] | ||
| if missing: | ||
| raise PermissionError( | ||
| "Missing required IBKR permissions: " + ", ".join(missing) | ||
| ) | ||
|
|
||
| def set_client_ratelimits( | ||
| self, | ||
| *, | ||
| pacing_api: bool = True, | ||
| max_requests_per_second: Optional[int] = None, | ||
| ) -> None: | ||
| """Configure pacing options for the underlying IB client.""" | ||
|
|
||
| self._ensure_connection() | ||
| options: List[str] = [] | ||
| if pacing_api: | ||
| options.append("PACEAPI") | ||
| if max_requests_per_second is not None: | ||
| options.append(f"MaxRequests={max_requests_per_second}") | ||
| if options: | ||
| self._ib.client.setConnectOptions(";".join(options)) | ||
|
|
||
| def ibkr_health(self) -> Dict[str, Any]: | ||
| """Return a diagnostic snapshot for health monitoring.""" | ||
|
|
||
| connected = self._ib.isConnected() | ||
| summary: Dict[str, Any] = { | ||
| "connected": connected, | ||
| "client_id": getattr(self._ib.client, "clientId", None), | ||
| "server_version": getattr(self._ib.client, "serverVersion", None), | ||
| } | ||
| if connected: | ||
| summary["server_time"] = self._ib.reqCurrentTime() | ||
| summary["accounts"] = self._ib.managedAccounts() | ||
| return summary | ||
|
|
||
| # Convenience proxies ------------------------------------------------- | ||
| def list_trades(self) -> List[OrderTicket]: | ||
| """Return currently open trades as :class:`OrderTicket` objects.""" | ||
|
|
||
| self._ensure_connection() | ||
| return normalize_trades(self._ib.trades()) | ||
|
|
||
| def cancel_order(self, trade: Trade) -> None: | ||
| """Cancel an order associated with the provided trade.""" | ||
|
|
||
| self._ensure_connection() | ||
| self._ib.cancelOrder(trade.order) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
req_market_datathe returned mapping usescontract_model.__dict__, butContractModelwas declared withslots=Trueso it lacks a__dict__attribute. This call path will raiseAttributeErrorbefore any market data is returned. Usedataclasses.asdictor construct the contract dictionary field by field to avoid the exception.Useful? React with 👍 / 👎.