From 196422433c359621ce213415c26241fee6c00502 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 26 Jan 2026 11:38:29 -0500 Subject: [PATCH 1/5] Capture `cons` in `Client.get_fute()` That is to be able to (eventually) introspect "ambiguous" contract sets once we move to `ib_async` and its `returnAll: bool` now offered by `IB.qualifyContractsAsync()`, https://github.com/ib-api-reloaded/ib_async/blob/main/ib_async/ib.py#L2115 Also, tweak some type type annots to multline style in sibling mods. --- piker/brokers/ib/api.py | 13 +++++++++---- piker/brokers/ib/symbols.py | 6 +++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/piker/brokers/ib/api.py b/piker/brokers/ib/api.py index adb1bb89..74c3aaab 100644 --- a/piker/brokers/ib/api.py +++ b/piker/brokers/ib/api.py @@ -779,13 +779,14 @@ class Client: ContFuture(symbol, exchange=exchange) ))[0] else: - con = (await self.ib.qualifyContractsAsync( + cons = (await self.ib.qualifyContractsAsync( Future( symbol, exchange=exchange, lastTradeDateOrContractMonth=expiry, ) - ))[0] + )) + con = cons[0] return con @@ -992,7 +993,6 @@ class Client: async def get_sym_details( self, fqme: str, - ) -> tuple[ Contract, ContractDetails, @@ -1631,6 +1631,7 @@ async def open_aio_client_method_relay( ) -> None: + # with tractor.devx.maybe_open_crash_handler() as _bxerr: # sync with `open_client_proxy()` caller chan.started_nowait(client) @@ -1640,7 +1641,11 @@ async def open_aio_client_method_relay( # relay all method requests to ``asyncio``-side client and deliver # back results while not chan._to_trio._closed: # <- TODO, better check like `._web_bs`? - msg: tuple[str, dict]|dict|None = await chan.get() + msg: ( + None + |tuple[str, dict] + |dict + ) = await chan.get() match msg: case None: # termination sentinel log.info('asyncio `Client` method-proxy SHUTDOWN!') diff --git a/piker/brokers/ib/symbols.py b/piker/brokers/ib/symbols.py index a9ca0594..39ce0924 100644 --- a/piker/brokers/ib/symbols.py +++ b/piker/brokers/ib/symbols.py @@ -522,7 +522,11 @@ async def get_mkt_info( if atype == 'commodity': venue: str = 'cmdty' else: - venue = con.primaryExchange or con.exchange + venue: str = ( + con.primaryExchange + or + con.exchange + ) price_tick: Decimal = Decimal(str(details.minTick)) ib_min_tick_gt_2: Decimal = Decimal('0.01') -- 2.34.1 From b1cb67d1bdfe8580d7d3e88f32e4e0974b3b2a56 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 24 Feb 2026 15:13:28 -0500 Subject: [PATCH 2/5] Port `.ib` backend from `ib_insync` to `ib_async` Migrate the IB broker backend to use `ib_async` (the actively maintained fork) instead of the now stale, original `ib_insync` lib. Deats, - update `pyproject.toml` dep: drop `ib-insync` pin, add `ib-async>=2.1.0`. - update lock file with `ib-async` and its new `aeventkit` dep (which i guess replaces `eventkit`). - obvi, change all `ib_insync` imports to `ib_async` across `.ib.*`. - update docs and select internal comments referencing the original lib. Also, - drop unused `ledger_dict` init in `_flex_reports.load_flex_trades()`. - fix union type annot style: `dict | None` -> `dict|None`. - strip `.tzinfo` from `lastTimeStamp` in `normalize()` to avoid IPC codec issues with `ib_async`'s `timezone.utc` injection. - pop `'defaults'` from ticker data dict in `normalize()` to avoid non-serializable `timezone` objects and warning-log in such cases. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- piker/brokers/ib/README.rst | 2 +- piker/brokers/ib/__init__.py | 2 +- piker/brokers/ib/_flex_reports.py | 5 ++-- piker/brokers/ib/api.py | 21 +++++++++-------- piker/brokers/ib/broker.py | 14 +++++------ piker/brokers/ib/feed.py | 23 ++++++++++++++---- piker/brokers/ib/ledger.py | 2 +- piker/brokers/ib/symbols.py | 2 +- piker/brokers/ib/venues.py | 4 ++-- pyproject.toml | 2 +- uv.lock | 39 ++++++++++++++++--------------- 11 files changed, 66 insertions(+), 50 deletions(-) diff --git a/piker/brokers/ib/README.rst b/piker/brokers/ib/README.rst index d56b52ca..301ab208 100644 --- a/piker/brokers/ib/README.rst +++ b/piker/brokers/ib/README.rst @@ -2,7 +2,7 @@ -------------- more or less the "everything broker" for traditional and international markets. they are the "go to" provider for automatic retail trading -and we interface to their APIs using the `ib_insync` project. +and we interface to their APIs using the `ib_async` project. status ****** diff --git a/piker/brokers/ib/__init__.py b/piker/brokers/ib/__init__.py index e792eb25..e07ad482 100644 --- a/piker/brokers/ib/__init__.py +++ b/piker/brokers/ib/__init__.py @@ -22,7 +22,7 @@ Sub-modules within break into the core functionalities: - ``broker.py`` part for orders / trading endpoints - ``feed.py`` for real-time data feed endpoints - ``api.py`` for the core API machinery which is ``trio``-ized - wrapping around ``ib_insync``. + wrapping around `ib_async`. """ from .api import ( diff --git a/piker/brokers/ib/_flex_reports.py b/piker/brokers/ib/_flex_reports.py index e8c22ecb..4b8110cc 100644 --- a/piker/brokers/ib/_flex_reports.py +++ b/piker/brokers/ib/_flex_reports.py @@ -111,7 +111,7 @@ def load_flex_trades( ) -> dict[str, Any]: - from ib_insync import flexreport, util + from ib_async import flexreport, util conf = get_config() @@ -154,8 +154,7 @@ def load_flex_trades( trade_entries, ) - ledger_dict: dict | None = None - + ledger_dict: dict|None for acctid in trades_by_account: trades_by_id = trades_by_account[acctid] diff --git a/piker/brokers/ib/api.py b/piker/brokers/ib/api.py index 74c3aaab..b359ab06 100644 --- a/piker/brokers/ib/api.py +++ b/piker/brokers/ib/api.py @@ -15,7 +15,8 @@ # along with this program. If not, see . ''' -Core API client machinery; mostly sane/useful wrapping around `ib_insync`.. +Core API client machinery; mostly sane/useful wrapping around +`ib_async`.. ''' from __future__ import annotations @@ -57,7 +58,7 @@ from pendulum import ( Interval, ) from eventkit import Event -from ib_insync import ( +from ib_async import ( client as ib_client, IB, Contract, @@ -143,7 +144,7 @@ _bar_sizes = { _show_wap_in_history: bool = False # overrides to sidestep pretty questionable design decisions in -# ``ib_insync``: +# ``ib_async``: class NonShittyWrapper(Wrapper): def tcpDataArrived(self): """Override time stamps to be floats for now. @@ -183,7 +184,7 @@ class NonShittyIB(IB): ''' def __init__(self): - # override `ib_insync` internal loggers so we can see wtf + # override `ib_async` internal loggers so we can see wtf # it's doing.. self._logger = get_logger( name=__name__, @@ -194,7 +195,7 @@ class NonShittyIB(IB): self.wrapper = NonShittyWrapper(self) self.client = ib_client.Client(self.wrapper) self.client._logger = get_logger( - name='ib_insync.client', + name='ib_async.client', ) # self.errorEvent += self._onError @@ -879,7 +880,7 @@ class Client: currency='USD', exchange='PAXOS', ) - # XXX, on `ib_insync` when first tried this, + # XXX, on `ib_async` when first tried this, # > Error 10299, reqId 141: Expected what to show is # > AGGTRADES, please use that instead of TRADES., # > contract: Crypto(conId=479624278, symbol='BTC', @@ -1092,7 +1093,7 @@ class Client: size: int, account: str, # if blank the "default" tws account is used - # XXX: by default 0 tells ``ib_insync`` methods that there is no + # XXX: by default 0 tells ``ib_async`` methods that there is no # existing order so ask the client to create a new one (which it # seems to do by allocating an int counter - collision prone..) reqid: int = None, @@ -1281,7 +1282,7 @@ async def load_aio_clients( port: int = None, client_id: int = 6116, - # the API TCP in `ib_insync` connection can be flaky af so instead + # the API TCP in `ib_async` connection can be flaky af so instead # retry a few times to get the client going.. connect_retries: int = 3, connect_timeout: float = 30, # in case a remote-host @@ -1289,7 +1290,7 @@ async def load_aio_clients( ) -> dict[str, Client]: ''' - Return an ``ib_insync.IB`` instance wrapped in our client API. + Return an ``ib_async.IB`` instance wrapped in our client API. Client instances are cached for later use. @@ -1747,7 +1748,7 @@ async def get_client( ) -> Client: ''' - Init the ``ib_insync`` client in another actor and return + Init the ``ib_async`` client in another actor and return a method proxy to it. ''' diff --git a/piker/brokers/ib/broker.py b/piker/brokers/ib/broker.py index e8514958..37b6f602 100644 --- a/piker/brokers/ib/broker.py +++ b/piker/brokers/ib/broker.py @@ -35,14 +35,14 @@ from trio_typing import TaskStatus import tractor from tractor.to_asyncio import LinkedTaskChannel from tractor import trionics -from ib_insync.contract import ( +from ib_async.contract import ( Contract, ) -from ib_insync.order import ( +from ib_async.order import ( Trade, OrderStatus, ) -from ib_insync.objects import ( +from ib_async.objects import ( Fill, Execution, CommissionReport, @@ -181,7 +181,7 @@ async def handle_order_requests( # validate order = BrokerdOrder(**request_msg) - # XXX: by default 0 tells ``ib_insync`` methods that + # XXX: by default 0 tells ``ib_async`` methods that # there is no existing order so ask the client to create # a new one (which it seems to do by allocating an int # counter - collision prone..) @@ -237,7 +237,7 @@ async def recv_trade_updates( ) -> None: ''' Receive and relay order control and positioning related events - from `ib_insync`, pack as tuples and push over mem-chan to our + from `ib_async`, pack as tuples and push over mem-chan to our trio relay task for processing and relay to EMS. ''' @@ -303,7 +303,7 @@ async def recv_trade_updates( # much more then a few more pnl fields.. # 'updatePortfolioEvent', - # XXX: these all seem to be weird ib_insync internal + # XXX: these all seem to be weird ib_async internal # events that we probably don't care that much about # given the internal design is wonky af.. # 'newOrderEvent', @@ -499,7 +499,7 @@ async def open_trade_event_stream( ] = trio.TASK_STATUS_IGNORED, ): ''' - Proxy wrapper for starting trade event stream from ib_insync + Proxy wrapper for starting trade event stream from ib_async which spawns an asyncio task that registers an internal closure (`push_tradies()`) which in turn relays trading events through a `tractor.to_asyncio.LinkedTaskChannel` which the parent diff --git a/piker/brokers/ib/feed.py b/piker/brokers/ib/feed.py index 0576b9e5..e148bca4 100644 --- a/piker/brokers/ib/feed.py +++ b/piker/brokers/ib/feed.py @@ -36,7 +36,7 @@ from typing import ( ) from async_generator import aclosing -import ib_insync as ibis +import ib_async as ibis import numpy as np from pendulum import ( now, @@ -100,7 +100,7 @@ tick_types = { 5: 'size', 8: 'volume', - # ``ib_insync`` already packs these into + # `ib_async` already packs these into # quotes under the following fields. 55: 'trades_per_min', # `'tradeRate'` 56: 'vlm_per_min', # `'volumeRate'` @@ -304,7 +304,7 @@ async def open_history_client( # TODO: it seems like we can do async queries for ohlc # but getting the order right still isn't working and I'm not # quite sure why.. needs some tinkering and probably - # a lookthrough of the `ib_insync` machinery, for eg. maybe + # a lookthrough of the `ib_async` machinery, for eg. maybe # we have to do the batch queries on the `asyncio` side? yield ( get_hist, @@ -1051,6 +1051,21 @@ def normalize( # ticker.rtTime.timestamp) / 1000. data.pop('rtTime') + # XXX, `ib_async` seems to set a + # `'timezone': datetime.timezone.utc` in this `dict` + # which is NOT IPC serializeable sin codec! + # + # pretty sure we don't need any of this field for now anyway? + data.pop('defaults') + + if lts := data.get('lastTimeStamp'): + lts.replace(tzinfo=None) + log.warning( + f'Stripping `.tzinfo` from datetime\n' + f'{lts}\n' + ) + # breakpoint() + return data @@ -1227,7 +1242,7 @@ async def stream_quotes( ): # ?TODO? can we rm this - particularly for `ib_async`? # ugh, clear ticks since we've consumed them - # (ahem, ib_insync is stateful trash) + # (ahem, ib_async is stateful trash) # first_ticker.ticks = [] # only on first entry at feed boot up diff --git a/piker/brokers/ib/ledger.py b/piker/brokers/ib/ledger.py index dc23748d..d8510f8b 100644 --- a/piker/brokers/ib/ledger.py +++ b/piker/brokers/ib/ledger.py @@ -36,7 +36,7 @@ from pendulum import ( parse, from_timestamp, ) -from ib_insync import ( +from ib_async import ( Contract, Commodity, Fill, diff --git a/piker/brokers/ib/symbols.py b/piker/brokers/ib/symbols.py index 39ce0924..cbdb5625 100644 --- a/piker/brokers/ib/symbols.py +++ b/piker/brokers/ib/symbols.py @@ -30,7 +30,7 @@ from typing import ( ) from rapidfuzz import process as fuzzy -import ib_insync as ibis +import ib_async as ibis import tractor import trio diff --git a/piker/brokers/ib/venues.py b/piker/brokers/ib/venues.py index 7f73af77..ea561d86 100644 --- a/piker/brokers/ib/venues.py +++ b/piker/brokers/ib/venues.py @@ -41,7 +41,7 @@ from pendulum import ( ) if TYPE_CHECKING: - from ib_insync import ( + from ib_async import ( TradingSession, ContractDetails, ) @@ -236,7 +236,7 @@ def is_venue_closure( # # NOTE, this was generated by @guille from a gpt5 prompt # and was originally thot to be needed before learning about -# `ib_insync.contract.ContractDetails._parseSessions()` and +# `ib_async.contract.ContractDetails._parseSessions()` and # it's downstream meths.. # # This is still likely useful to keep for now to parse the diff --git a/pyproject.toml b/pyproject.toml index 31de030e..6706826c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,6 @@ dependencies = [ "bidict >=0.23.1", "colorama >=0.4.6, <0.5.0", "colorlog >=6.7.0, <7.0.0", - "ib-insync >=0.9.86, <0.10.0", "numpy>=2.0", "polars >=0.20.6", "polars-fuzzy-match>=0.1.5", @@ -76,6 +75,7 @@ dependencies = [ "numba>=0.61.0", "pyvnc", "exchange-calendars>=4.13.1", + "ib-async>=2.1.0", ] # ------ dependencies ------ # NOTE, by default we ship only a "headless" deps set bc diff --git a/uv.lock b/uv.lock index e0f08915..eae0a2c9 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,18 @@ resolution-markers = [ "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] +[[package]] +name = "aeventkit" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/8c/c08db1a1910f8d04ec6a524de522edd0bac181bdf94dbb01183f7685cd77/aeventkit-2.1.0.tar.gz", hash = "sha256:4e7d81bb0a67227121da50a23e19e5bbf13eded541a9f4857eeb6b7b857b738a", size = 24703, upload-time = "2025-06-22T15:54:03.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/8c/2a4b912b1afa201b25bdd0f5bccf96d5a8b5dccb6131316a8dd2d9cabcc1/aeventkit-2.1.0-py3-none-any.whl", hash = "sha256:962d43f79e731ac43527f2d0defeed118e6dbaa85f1487f5667540ebb8f00729", size = 26678, upload-time = "2025-06-22T15:54:02.141Z" }, +] + [[package]] name = "aiodns" version = "3.6.0" @@ -396,18 +408,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/01/6f77d042b83260ef9ed73ea9647dfa0ef8414eba0a3fc57a509a088ad39b/elasticsearch-8.19.2-py3-none-any.whl", hash = "sha256:c16ba20c4c76cf6952e836dae7f4e724e00ba7bf31b94b79472b873683accdd4", size = 949706, upload-time = "2025-10-28T16:36:41.003Z" }, ] -[[package]] -name = "eventkit" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/1e/0fac4e45d71ace143a2673ec642701c3cd16f833a0e77a57fa6a40472696/eventkit-1.0.3.tar.gz", hash = "sha256:99497f6f3c638a50ff7616f2f8cd887b18bbff3765dc1bd8681554db1467c933", size = 28320, upload-time = "2023-12-11T11:41:35.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d9/7497d650b69b420e1a913329a843e16c715dac883750679240ef00a921e2/eventkit-1.0.3-py3-none-any.whl", hash = "sha256:0e199527a89aff9d195b9671ad45d2cc9f79ecda0900de8ecfb4c864d67ad6a2", size = 31837, upload-time = "2023-12-11T11:41:33.358Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.1" @@ -630,16 +630,17 @@ wheels = [ ] [[package]] -name = "ib-insync" -version = "0.9.86" +name = "ib-async" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "eventkit" }, + { name = "aeventkit" }, { name = "nest-asyncio" }, + { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/bb/733d5c81c8c2f54e90898afc7ff3a99f4d53619e6917c848833f9cc1ab56/ib_insync-0.9.86.tar.gz", hash = "sha256:73af602ca2463f260999970c5bd937b1c4325e383686eff301743a4de08d381e", size = 69859, upload-time = "2023-07-02T12:43:31.968Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4d/dfc1da8224c3ffcdcd668da7283c4e5f14239a07f83ea66af99700296fc3/ib_async-2.1.0.tar.gz", hash = "sha256:6a03a87d6c06acacb0217a5bea60a8a168ecd5b5a7e86e1c73678d5b48cbc796", size = 87678, upload-time = "2025-12-08T01:42:32.004Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/f3/28ea87be30570f4d6b8fd24380d12fa74e59467ee003755e76aeb29082b8/ib_insync-0.9.86-py3-none-any.whl", hash = "sha256:a61fbe56ff405d93d211dad8238d7300de76dd6399eafc04c320470edec9a4a4", size = 72980, upload-time = "2023-07-02T12:43:29.928Z" }, + { url = "https://files.pythonhosted.org/packages/80/e7/8f33801788c66f15e9250957ff7f53a8000843f79af1a3ed7a96def0e96b/ib_async-2.1.0-py3-none-any.whl", hash = "sha256:f6d8b991bdbd6dd38e700c61b3dced06ebe0f14be4e5263e2ef10ba10b88d434", size = 88876, upload-time = "2025-12-08T01:42:30.883Z" }, ] [[package]] @@ -1107,7 +1108,7 @@ dependencies = [ { name = "cryptofeed" }, { name = "exchange-calendars" }, { name = "httpx" }, - { name = "ib-insync" }, + { name = "ib-async" }, { name = "msgspec" }, { name = "numba" }, { name = "numpy" }, @@ -1183,7 +1184,7 @@ requires-dist = [ { name = "cryptofeed", specifier = ">=2.4.0,<3.0.0" }, { name = "exchange-calendars", specifier = ">=4.13.1" }, { name = "httpx", specifier = ">=0.27.0,<0.28.0" }, - { name = "ib-insync", specifier = ">=0.9.86,<0.10.0" }, + { name = "ib-async", specifier = ">=2.1.0" }, { name = "msgspec", specifier = ">=0.19.0,<0.20" }, { name = "numba", specifier = ">=0.61.0" }, { name = "numpy", specifier = ">=2.0" }, -- 2.34.1 From 9e2af2838fdbb9fb79658aa872bf52d0f7c3df83 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 24 Feb 2026 16:06:24 -0500 Subject: [PATCH 3/5] Ah right, we import types from `eventkit` (now `aeventkit`).. --- pyproject.toml | 1 + uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6706826c..2d63059b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ dependencies = [ "pyvnc", "exchange-calendars>=4.13.1", "ib-async>=2.1.0", + "aeventkit>=2.1.0", # XXX, imports as eventkit? ] # ------ dependencies ------ # NOTE, by default we ship only a "headless" deps set bc diff --git a/uv.lock b/uv.lock index eae0a2c9..103915eb 100644 --- a/uv.lock +++ b/uv.lock @@ -1100,6 +1100,7 @@ name = "piker" version = "0.1.0a0.dev0" source = { editable = "." } dependencies = [ + { name = "aeventkit" }, { name = "async-generator" }, { name = "attrs" }, { name = "bidict" }, @@ -1176,6 +1177,7 @@ uis = [ [package.metadata] requires-dist = [ + { name = "aeventkit", specifier = ">=2.1.0" }, { name = "async-generator", specifier = ">=1.10,<2.0.0" }, { name = "attrs", specifier = ">=23.1.0,<24.0.0" }, { name = "bidict", specifier = ">=0.23.1" }, -- 2.34.1 From 9247746a79d61052c5f889fb7a8a1c19a888040b Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 26 Feb 2026 17:46:54 -0500 Subject: [PATCH 4/5] Handle unknown order statuses in `.ib.broker` Add fallback handling for unknown IB order status strings to avoid crashes when IB returns unexpected status values. Deats, - add `'ValidationError': 'error'` mapping to `_statuses` dict. - use `.get()` with `'error'` default instead of direct dict lookup for `status.status`. - add `elif status_str == 'error'` block to log unknown status values. - add type annots to `event_name` and `item` in `deliver_trade_events()` loop. Also, - reformat log msg in `deliver_trade_events()` to multiline. - drop extra conditional in `if status_str == 'filled'` check. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- piker/brokers/ib/broker.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/piker/brokers/ib/broker.py b/piker/brokers/ib/broker.py index 37b6f602..af6af5e8 100644 --- a/piker/brokers/ib/broker.py +++ b/piker/brokers/ib/broker.py @@ -991,6 +991,9 @@ _statuses: dict[str, str] = { # TODO: see a current ``ib_insync`` issue around this: # https://github.com/erdewit/ib_insync/issues/363 'Inactive': 'pending', + + # XXX, uhh wut the heck is this? + 'ValidationError': 'error', } _action_map = { @@ -1063,8 +1066,19 @@ async def deliver_trade_events( # TODO: for some reason we can receive a ``None`` here when the # ib-gw goes down? Not sure exactly how that's happening looking # at the eventkit code above but we should probably handle it... + event_name: str + item: ( + Trade + |tuple[Trade, Fill] + |CommissionReport + |IbPosition + |dict + ) async for event_name, item in trade_event_stream: - log.info(f'Relaying `{event_name}`:\n{pformat(item)}') + log.info( + f'Relaying {event_name!r}:\n' + f'{pformat(item)}\n' + ) match event_name: case 'orderStatusEvent': @@ -1075,11 +1089,12 @@ async def deliver_trade_events( trade: Trade = item reqid: str = str(trade.order.orderId) status: OrderStatus = trade.orderStatus - status_str: str = _statuses[status.status] + status_str: str = _statuses.get( + status.status, + 'error', + ) remaining: float = status.remaining - if ( - status_str == 'filled' - ): + if status_str == 'filled': fill: Fill = trade.fills[-1] execu: Execution = fill.execution @@ -1110,6 +1125,12 @@ async def deliver_trade_events( # all units were cleared. status_str = 'closed' + elif status_str == 'error': + log.error( + f'IB reported error status for order ??\n' + f'{status.status!r}\n' + ) + # skip duplicate filled updates - we get the deats # from the execution details event msg = BrokerdStatus( -- 2.34.1 From ee09f519a925f1c28819ba146ae7a086d7006d92 Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 26 Feb 2026 17:50:48 -0500 Subject: [PATCH 5/5] Remap non-std IB exchange values Add exchange name translation in `.ib.venues.has_holiday()` to handle non-standard exchange codes when looking up holiday gaps.. Deats, - add an ad-hoc lookup dict to remap an IB `Contract.primaryExchange` val which doesn't exist in the `exchange_calendars`'s alias set. - use `.get()` with fallback to map `exch` to new `std_exch` and pass that to `xcals.get_calendar()`. - add the case i just caught, `'ARCA'` -> `'ARCX'` to the table when i loaded the `gld.arca.ib` market.. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- piker/brokers/ib/venues.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/piker/brokers/ib/venues.py b/piker/brokers/ib/venues.py index ea561d86..cd2dc7b1 100644 --- a/piker/brokers/ib/venues.py +++ b/piker/brokers/ib/venues.py @@ -83,7 +83,14 @@ def has_holiday( ''' tz: str = con_deats.timeZoneId exch: str = con_deats.contract.primaryExchange - cal: ExchangeCalendar = xcals.get_calendar(exch) + + # XXX, ad-hoc handle any IB exchange which are non-std + # via lookup table.. + std_exch: dict = { + 'ARCA': 'ARCX', + }.get(exch, exch) + + cal: ExchangeCalendar = xcals.get_calendar(std_exch) end: datetime = period.end # _start: datetime = period.start # ?TODO, can rm ya? -- 2.34.1