Port to new streaming api, yield whole tickers
parent
50f903d7c5
commit
8fa569787d
|
@ -2,28 +2,35 @@
|
||||||
Interactive Brokers API backend.
|
Interactive Brokers API backend.
|
||||||
|
|
||||||
Note the client runs under an ``asyncio`` loop (since ``ib_insync`` is
|
Note the client runs under an ``asyncio`` loop (since ``ib_insync`` is
|
||||||
built on it) and thus actor aware apis must be spawned with
|
built on it) and thus actor aware API calls must be spawned with
|
||||||
``infected_aio==True``.
|
``infected_aio==True``.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from typing import List, Dict, Any, Tuple, Optional, AsyncGenerator
|
||||||
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
from typing import List, Dict, Any, Tuple
|
import itertools
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import tractor
|
|
||||||
from async_generator import aclosing
|
from async_generator import aclosing
|
||||||
import ib_insync as ibis
|
|
||||||
from ib_insync.ticker import Ticker
|
|
||||||
from ib_insync.contract import Contract, ContractDetails
|
from ib_insync.contract import Contract, ContractDetails
|
||||||
|
from ib_insync.ticker import Ticker
|
||||||
|
import ib_insync as ibis
|
||||||
|
import tractor
|
||||||
|
|
||||||
from ..log import get_logger, get_console_log
|
from ..log import get_logger, get_console_log
|
||||||
|
from ..data import maybe_spawn_brokerd
|
||||||
|
from ..ui._source import from_df
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
# passed to ``tractor.ActorNursery.start_actor()``
|
||||||
|
_spawn_kwargs = {
|
||||||
|
'infect_asyncio': True,
|
||||||
|
}
|
||||||
_time_units = {
|
_time_units = {
|
||||||
's': ' sec',
|
's': ' sec',
|
||||||
'm': ' mins',
|
'm': ' mins',
|
||||||
|
@ -82,17 +89,15 @@ class Client:
|
||||||
endDateTime='',
|
endDateTime='',
|
||||||
# durationStr='60 S',
|
# durationStr='60 S',
|
||||||
# durationStr='1 D',
|
# durationStr='1 D',
|
||||||
durationStr='{count} S'.format(count=3000*5),
|
durationStr='{count} S'.format(count=3000 * 5),
|
||||||
barSizeSetting='5 secs',
|
barSizeSetting='5 secs',
|
||||||
whatToShow='TRADES',
|
whatToShow='TRADES',
|
||||||
useRTH=False
|
useRTH=False
|
||||||
)
|
)
|
||||||
|
# TODO: raise underlying error here
|
||||||
assert bars
|
assert bars
|
||||||
# barSizeSetting='1 min', whatToShow='MIDPOINT', useRTH=True)
|
|
||||||
# convert to pandas dataframe:
|
# convert to pandas dataframe:
|
||||||
df = ibis.util.df(bars)
|
df = ibis.util.df(bars)
|
||||||
# print(df[['date', 'open', 'high', 'low', 'close', 'volume']])
|
|
||||||
from piker.ui._source import from_df
|
|
||||||
return from_df(df)
|
return from_df(df)
|
||||||
|
|
||||||
async def search_stocks(
|
async def search_stocks(
|
||||||
|
@ -194,7 +199,6 @@ class Client:
|
||||||
# default config ports
|
# default config ports
|
||||||
_tws_port: int = 7497
|
_tws_port: int = 7497
|
||||||
_gw_port: int = 4002
|
_gw_port: int = 4002
|
||||||
# list of ports to try in order
|
|
||||||
_try_ports = [_tws_port, _gw_port]
|
_try_ports = [_tws_port, _gw_port]
|
||||||
|
|
||||||
|
|
||||||
|
@ -202,10 +206,20 @@ _try_ports = [_tws_port, _gw_port]
|
||||||
async def _aio_get_client(
|
async def _aio_get_client(
|
||||||
host: str = '127.0.0.1',
|
host: str = '127.0.0.1',
|
||||||
port: int = None,
|
port: int = None,
|
||||||
client_id: int = 1,
|
client_id: Optional[int] = None,
|
||||||
) -> Client:
|
) -> Client:
|
||||||
"""Return an ``ib_insync.IB`` instance wrapped in our client API.
|
"""Return an ``ib_insync.IB`` instance wrapped in our client API.
|
||||||
"""
|
"""
|
||||||
|
if client_id is None:
|
||||||
|
# if this is a persistent brokerd, try to allocate a new id for
|
||||||
|
# each client
|
||||||
|
try:
|
||||||
|
ss = tractor.current_actor().statespace
|
||||||
|
client_id = next(ss.setdefault('client_ids', itertools.count()))
|
||||||
|
except RuntimeError:
|
||||||
|
# tractor likely isn't running
|
||||||
|
client_id = 1
|
||||||
|
|
||||||
ib = ibis.IB()
|
ib = ibis.IB()
|
||||||
ports = _try_ports if port is None else [port]
|
ports = _try_ports if port is None else [port]
|
||||||
_err = None
|
_err = None
|
||||||
|
@ -271,26 +285,27 @@ async def _trio_run_client_method(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_method_proxy(portal, target):
|
class _MethodProxy:
|
||||||
|
def __init__(self, portal: tractor._portal.Portal):
|
||||||
|
self._portal = portal
|
||||||
|
|
||||||
class MethodProxy:
|
async def _run_method(
|
||||||
def __init__(self, portal: tractor._portal.Portal):
|
self,
|
||||||
self._portal = portal
|
*,
|
||||||
|
meth: str = None,
|
||||||
async def _run_method(
|
**kwargs
|
||||||
self,
|
) -> Any:
|
||||||
*,
|
return await self._portal.run(
|
||||||
meth: str = None,
|
__name__,
|
||||||
|
'_trio_run_client_method',
|
||||||
|
method=meth,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> Any:
|
)
|
||||||
return await self._portal.run(
|
|
||||||
__name__,
|
|
||||||
'_trio_run_client_method',
|
|
||||||
method=meth,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
proxy = MethodProxy(portal)
|
|
||||||
|
def get_method_proxy(portal, target) -> _MethodProxy:
|
||||||
|
|
||||||
|
proxy = _MethodProxy(portal)
|
||||||
|
|
||||||
# mock all remote methods
|
# mock all remote methods
|
||||||
for name, method in inspect.getmembers(
|
for name, method in inspect.getmembers(
|
||||||
|
@ -303,33 +318,6 @@ def get_method_proxy(portal, target):
|
||||||
return proxy
|
return proxy
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def maybe_spawn_brokerd(
|
|
||||||
**kwargs,
|
|
||||||
) -> tractor._portal.Portal:
|
|
||||||
async with tractor.find_actor('brokerd_ib') as portal:
|
|
||||||
# WTF: why doesn't this work?
|
|
||||||
print(__name__)
|
|
||||||
if portal is not None:
|
|
||||||
yield portal
|
|
||||||
else: # no broker daemon created yet
|
|
||||||
async with tractor.open_nursery() as n:
|
|
||||||
# XXX: this needs to somehow be hidden
|
|
||||||
portal = await n.start_actor(
|
|
||||||
'brokerd_ib',
|
|
||||||
rpc_module_paths=[__name__],
|
|
||||||
infect_asyncio=True,
|
|
||||||
)
|
|
||||||
async with tractor.wait_for_actor(
|
|
||||||
'brokerd_ib'
|
|
||||||
) as portal:
|
|
||||||
yield portal
|
|
||||||
|
|
||||||
# client code may block indefinitely so cancel when
|
|
||||||
# teardown is invoked
|
|
||||||
await n.cancel()
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def get_client(
|
async def get_client(
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
@ -337,45 +325,47 @@ async def get_client(
|
||||||
"""Init the ``ib_insync`` client in another actor and return
|
"""Init the ``ib_insync`` client in another actor and return
|
||||||
a method proxy to it.
|
a method proxy to it.
|
||||||
"""
|
"""
|
||||||
async with maybe_spawn_brokerd(**kwargs) as portal:
|
async with maybe_spawn_brokerd(
|
||||||
|
brokername='ib',
|
||||||
|
expose_mods=[__name__],
|
||||||
|
infect_asyncio=True,
|
||||||
|
**kwargs
|
||||||
|
) as portal:
|
||||||
yield get_method_proxy(portal, Client)
|
yield get_method_proxy(portal, Client)
|
||||||
|
|
||||||
|
|
||||||
async def trio_stream_ticker(sym):
|
async def stream_quotes(
|
||||||
|
symbols: List[str],
|
||||||
|
) -> AsyncGenerator[str, Dict[str, Any]]:
|
||||||
|
"""Stream symbol quotes.
|
||||||
|
|
||||||
|
This is a ``trio`` callable routine meant to be invoked
|
||||||
|
once the brokerd is up.
|
||||||
|
"""
|
||||||
get_console_log('info')
|
get_console_log('info')
|
||||||
|
|
||||||
stream = await tractor.to_asyncio.run_task(
|
stream = await tractor.to_asyncio.run_task(
|
||||||
_trio_run_client_method,
|
_trio_run_client_method,
|
||||||
method='stream_ticker',
|
method='stream_ticker',
|
||||||
symbol=sym,
|
symbol=symbols[0],
|
||||||
)
|
)
|
||||||
async with aclosing(stream):
|
async with aclosing(stream):
|
||||||
async for ticker in stream:
|
async for ticker in stream:
|
||||||
# TODO: validate this value
|
# convert named tuples to dicts so we send usable keys
|
||||||
lft = ticker.rtTime
|
# for tick_data in ticker.ticks:
|
||||||
for tick_data in ticker.ticks:
|
ticker.ticks = [td._asdict() for td in ticker.ticks]
|
||||||
value = tick_data._asdict()
|
|
||||||
now = time.time()
|
|
||||||
value['time'] = now
|
|
||||||
value['last_fill_time'] = lft
|
|
||||||
if lft:
|
|
||||||
# convert from milliseconds
|
|
||||||
lft = float(lft) / 1000.
|
|
||||||
value['latency'] = now - lft
|
|
||||||
|
|
||||||
yield value
|
data = asdict(ticker)
|
||||||
|
|
||||||
|
# add time stamps for downstream latency measurements
|
||||||
|
data['brokerd_ts'] = time.time()
|
||||||
|
if ticker.rtTime:
|
||||||
|
data['rtTime_s'] = float(ticker.rtTime) / 1000.
|
||||||
|
|
||||||
async def stream_from_brokerd(sym):
|
yield data
|
||||||
|
|
||||||
async with maybe_spawn_brokerd() as portal:
|
# ugh, clear ticks since we've consumed them
|
||||||
stream = await portal.run(
|
ticker.ticks = []
|
||||||
__name__,
|
|
||||||
'trio_stream_ticker',
|
|
||||||
sym=sym,
|
|
||||||
)
|
|
||||||
async for tick in stream:
|
|
||||||
print(f"trio got: {tick}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Loading…
Reference in New Issue