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-codeib_async
parent
196422433c
commit
b1cb67d1bd
|
|
@ -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
|
||||
******
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
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.
|
||||
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ from pendulum import (
|
|||
parse,
|
||||
from_timestamp,
|
||||
)
|
||||
from ib_insync import (
|
||||
from ib_async import (
|
||||
Contract,
|
||||
Commodity,
|
||||
Fill,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
39
uv.lock
39
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" },
|
||||
|
|
|
|||
Loading…
Reference in New Issue