Merge pull request #204 from pikers/ib_adhoc_derivs
Ib adhoc derivs searchpause_feeds_on_sym_switch
commit
ad174c5c21
|
@ -283,7 +283,7 @@ async def maybe_spawn_daemon(
|
||||||
lock = Brokerd.locks[service_name]
|
lock = Brokerd.locks[service_name]
|
||||||
await lock.acquire()
|
await lock.acquire()
|
||||||
|
|
||||||
# attach to existing brokerd if possible
|
# attach to existing daemon by name if possible
|
||||||
async with tractor.find_actor(service_name) as portal:
|
async with tractor.find_actor(service_name) as portal:
|
||||||
if portal is not None:
|
if portal is not None:
|
||||||
lock.release()
|
lock.release()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# piker: trading gear for hackers
|
# piker: trading gear for hackers
|
||||||
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
||||||
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
@ -25,7 +25,10 @@ from contextlib import asynccontextmanager
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List, Dict, Any, Tuple, Optional, AsyncIterator
|
from typing import (
|
||||||
|
Any, Optional,
|
||||||
|
AsyncIterator, Awaitable,
|
||||||
|
)
|
||||||
import asyncio
|
import asyncio
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -171,6 +174,7 @@ _adhoc_futes_set = {
|
||||||
# equities
|
# equities
|
||||||
'nq.globex',
|
'nq.globex',
|
||||||
'mnq.globex',
|
'mnq.globex',
|
||||||
|
|
||||||
'es.globex',
|
'es.globex',
|
||||||
'mes.globex',
|
'mes.globex',
|
||||||
|
|
||||||
|
@ -178,8 +182,20 @@ _adhoc_futes_set = {
|
||||||
'brr.cmecrypto',
|
'brr.cmecrypto',
|
||||||
'ethusdrr.cmecrypto',
|
'ethusdrr.cmecrypto',
|
||||||
|
|
||||||
|
# agriculture
|
||||||
|
'he.globex', # lean hogs
|
||||||
|
'le.globex', # live cattle (geezers)
|
||||||
|
'gf.globex', # feeder cattle (younguns)
|
||||||
|
|
||||||
|
# raw
|
||||||
|
'lb.globex', # random len lumber
|
||||||
|
|
||||||
# metals
|
# metals
|
||||||
'xauusd.cmdty',
|
'xauusd.cmdty', # gold spot
|
||||||
|
'gc.nymex',
|
||||||
|
'mgc.nymex',
|
||||||
|
|
||||||
|
'xagusd.cmdty', # silver spot
|
||||||
}
|
}
|
||||||
|
|
||||||
# exchanges we don't support at the moment due to not knowing
|
# exchanges we don't support at the moment due to not knowing
|
||||||
|
@ -210,8 +226,8 @@ class Client:
|
||||||
self.ib.RaiseRequestErrors = True
|
self.ib.RaiseRequestErrors = True
|
||||||
|
|
||||||
# contract cache
|
# contract cache
|
||||||
self._contracts: Dict[str, Contract] = {}
|
self._contracts: dict[str, Contract] = {}
|
||||||
self._feeds: Dict[str, trio.abc.SendChannel] = {}
|
self._feeds: dict[str, trio.abc.SendChannel] = {}
|
||||||
|
|
||||||
# NOTE: the ib.client here is "throttled" to 45 rps by default
|
# NOTE: the ib.client here is "throttled" to 45 rps by default
|
||||||
|
|
||||||
|
@ -226,7 +242,7 @@ class Client:
|
||||||
period_count: int = int(2e3), # <- max per 1s sample query
|
period_count: int = int(2e3), # <- max per 1s sample query
|
||||||
|
|
||||||
is_paid_feed: bool = False, # placeholder
|
is_paid_feed: bool = False, # placeholder
|
||||||
) -> List[Dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Retreive OHLCV bars for a symbol over a range to the present.
|
"""Retreive OHLCV bars for a symbol over a range to the present.
|
||||||
"""
|
"""
|
||||||
bars_kwargs = {'whatToShow': 'TRADES'}
|
bars_kwargs = {'whatToShow': 'TRADES'}
|
||||||
|
@ -279,23 +295,14 @@ class Client:
|
||||||
df = ibis.util.df(bars)
|
df = ibis.util.df(bars)
|
||||||
return bars, from_df(df)
|
return bars, from_df(df)
|
||||||
|
|
||||||
async def search_stocks(
|
async def con_deats(
|
||||||
self,
|
self,
|
||||||
pattern: str,
|
contracts: list[Contract],
|
||||||
# how many contracts to search "up to"
|
|
||||||
upto: int = 3,
|
|
||||||
) -> Dict[str, ContractDetails]:
|
|
||||||
"""Search for stocks matching provided ``str`` pattern.
|
|
||||||
|
|
||||||
Return a dictionary of ``upto`` entries worth of contract details.
|
) -> dict[str, ContractDetails]:
|
||||||
"""
|
|
||||||
descriptions = await self.ib.reqMatchingSymbolsAsync(pattern)
|
|
||||||
|
|
||||||
if descriptions is not None:
|
|
||||||
|
|
||||||
futs = []
|
futs = []
|
||||||
for d in descriptions:
|
for con in contracts:
|
||||||
con = d.contract
|
|
||||||
if con.primaryExchange not in _exch_skip_list:
|
if con.primaryExchange not in _exch_skip_list:
|
||||||
futs.append(self.ib.reqContractDetailsAsync(con))
|
futs.append(self.ib.reqContractDetailsAsync(con))
|
||||||
|
|
||||||
|
@ -318,11 +325,40 @@ class Client:
|
||||||
|
|
||||||
details[unique_sym] = as_dict
|
details[unique_sym] = as_dict
|
||||||
|
|
||||||
if len(details) == upto:
|
|
||||||
return details
|
return details
|
||||||
|
|
||||||
return details
|
async def search_stocks(
|
||||||
|
self,
|
||||||
|
pattern: str,
|
||||||
|
|
||||||
|
get_details: bool = False,
|
||||||
|
# how many contracts to search "up to"
|
||||||
|
upto: int = 3,
|
||||||
|
|
||||||
|
) -> dict[str, ContractDetails]:
|
||||||
|
"""Search for stocks matching provided ``str`` pattern.
|
||||||
|
|
||||||
|
Return a dictionary of ``upto`` entries worth of contract details.
|
||||||
|
"""
|
||||||
|
descriptions = await self.ib.reqMatchingSymbolsAsync(pattern)
|
||||||
|
|
||||||
|
if descriptions is not None:
|
||||||
|
descrs = descriptions[:upto]
|
||||||
|
|
||||||
|
if get_details:
|
||||||
|
return await self.con_deats([d.contract for d in descrs])
|
||||||
|
|
||||||
|
else:
|
||||||
|
results = {}
|
||||||
|
for d in descrs:
|
||||||
|
con = d.contract
|
||||||
|
# sometimes there's a weird extra suffix returned
|
||||||
|
# from search?
|
||||||
|
exch = con.primaryExchange.rsplit('.')[0]
|
||||||
|
unique_sym = f'{con.symbol}.{exch}'
|
||||||
|
results[unique_sym] = {}
|
||||||
|
|
||||||
|
return results
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@ -332,20 +368,12 @@ class Client:
|
||||||
# how many contracts to search "up to"
|
# how many contracts to search "up to"
|
||||||
upto: int = 3,
|
upto: int = 3,
|
||||||
asdicts: bool = True,
|
asdicts: bool = True,
|
||||||
) -> Dict[str, ContractDetails]:
|
|
||||||
|
) -> dict[str, ContractDetails]:
|
||||||
|
|
||||||
# TODO add search though our adhoc-locally defined symbol set
|
# TODO add search though our adhoc-locally defined symbol set
|
||||||
# for futes/cmdtys/
|
# for futes/cmdtys/
|
||||||
return await self.search_stocks(pattern, upto, asdicts)
|
return await self.search_stocks(pattern, upto, get_details=True)
|
||||||
|
|
||||||
async def search_futes(
|
|
||||||
self,
|
|
||||||
pattern: str,
|
|
||||||
# how many contracts to search "up to"
|
|
||||||
upto: int = 3,
|
|
||||||
asdicts: bool = True,
|
|
||||||
) -> Dict[str, ContractDetails]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def get_cont_fute(
|
async def get_cont_fute(
|
||||||
self,
|
self,
|
||||||
|
@ -371,7 +399,7 @@ class Client:
|
||||||
# ``wrapper.starTicker()`` currently cashes ticker instances
|
# ``wrapper.starTicker()`` currently cashes ticker instances
|
||||||
# which means getting a singel quote will potentially look up
|
# which means getting a singel quote will potentially look up
|
||||||
# a quote for a ticker that it already streaming and thus run
|
# a quote for a ticker that it already streaming and thus run
|
||||||
# into state clobbering (eg. List: Ticker.ticks). It probably
|
# into state clobbering (eg. list: Ticker.ticks). It probably
|
||||||
# makes sense to try this once we get the pub-sub working on
|
# makes sense to try this once we get the pub-sub working on
|
||||||
# individual symbols...
|
# individual symbols...
|
||||||
|
|
||||||
|
@ -483,6 +511,7 @@ class Client:
|
||||||
price: float,
|
price: float,
|
||||||
action: str,
|
action: str,
|
||||||
size: int,
|
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_insync`` methods that there is no
|
||||||
# existing order so ask the client to create a new one (which it
|
# existing order so ask the client to create a new one (which it
|
||||||
|
@ -505,6 +534,7 @@ class Client:
|
||||||
Order(
|
Order(
|
||||||
orderId=reqid or 0, # stupid api devs..
|
orderId=reqid or 0, # stupid api devs..
|
||||||
action=action.upper(), # BUY/SELL
|
action=action.upper(), # BUY/SELL
|
||||||
|
account=account,
|
||||||
orderType='LMT',
|
orderType='LMT',
|
||||||
lmtPrice=price,
|
lmtPrice=price,
|
||||||
totalQuantity=size,
|
totalQuantity=size,
|
||||||
|
@ -556,7 +586,7 @@ class Client:
|
||||||
else:
|
else:
|
||||||
item = ('status', obj)
|
item = ('status', obj)
|
||||||
|
|
||||||
log.info(f'eventkit event -> {eventkit_obj}: {item}')
|
log.info(f'eventkit event ->\n{pformat(item)}')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
to_trio.send_nowait(item)
|
to_trio.send_nowait(item)
|
||||||
|
@ -630,7 +660,7 @@ class Client:
|
||||||
async def positions(
|
async def positions(
|
||||||
self,
|
self,
|
||||||
account: str = '',
|
account: str = '',
|
||||||
) -> List[Position]:
|
) -> list[Position]:
|
||||||
"""
|
"""
|
||||||
Retrieve position info for ``account``.
|
Retrieve position info for ``account``.
|
||||||
"""
|
"""
|
||||||
|
@ -688,6 +718,10 @@ async def _aio_get_client(
|
||||||
# grab first cached client
|
# grab first cached client
|
||||||
client = list(_client_cache.values())[0]
|
client = list(_client_cache.values())[0]
|
||||||
|
|
||||||
|
if not client.ib.isConnected():
|
||||||
|
# we have a stale client to re-allocate
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
|
@ -767,7 +801,6 @@ async def _aio_run_client_method(
|
||||||
kwargs['to_trio'] = to_trio
|
kwargs['to_trio'] = to_trio
|
||||||
|
|
||||||
log.runtime(f'Running {meth}({kwargs})')
|
log.runtime(f'Running {meth}({kwargs})')
|
||||||
|
|
||||||
return await async_meth(**kwargs)
|
return await async_meth(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1034,12 +1067,12 @@ asset_type_map = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_quote_streams: Dict[str, trio.abc.ReceiveStream] = {}
|
_quote_streams: dict[str, trio.abc.ReceiveStream] = {}
|
||||||
|
|
||||||
|
|
||||||
async def _setup_quote_stream(
|
async def _setup_quote_stream(
|
||||||
symbol: str,
|
symbol: str,
|
||||||
opts: Tuple[int] = ('375', '233', '236'),
|
opts: tuple[int] = ('375', '233', '236'),
|
||||||
contract: Optional[Contract] = None,
|
contract: Optional[Contract] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Stream a ticker using the std L1 api.
|
"""Stream a ticker using the std L1 api.
|
||||||
|
@ -1075,6 +1108,11 @@ async def _setup_quote_stream(
|
||||||
# decouple broadcast mem chan
|
# decouple broadcast mem chan
|
||||||
_quote_streams.pop(symbol, None)
|
_quote_streams.pop(symbol, None)
|
||||||
|
|
||||||
|
# except trio.WouldBlock:
|
||||||
|
# # for slow debugging purposes to avoid clobbering prompt
|
||||||
|
# # with log msgs
|
||||||
|
# pass
|
||||||
|
|
||||||
ticker.updateEvent.connect(push)
|
ticker.updateEvent.connect(push)
|
||||||
|
|
||||||
return from_aio
|
return from_aio
|
||||||
|
@ -1110,13 +1148,13 @@ async def start_aio_quote_stream(
|
||||||
async def stream_quotes(
|
async def stream_quotes(
|
||||||
|
|
||||||
send_chan: trio.abc.SendChannel,
|
send_chan: trio.abc.SendChannel,
|
||||||
symbols: List[str],
|
symbols: list[str],
|
||||||
shm: ShmArray,
|
shm: ShmArray,
|
||||||
feed_is_live: trio.Event,
|
feed_is_live: trio.Event,
|
||||||
loglevel: str = None,
|
loglevel: str = None,
|
||||||
|
|
||||||
# startup sync
|
# startup sync
|
||||||
task_status: TaskStatus[Tuple[Dict, Dict]] = trio.TASK_STATUS_IGNORED,
|
task_status: TaskStatus[tuple[dict, dict]] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Stream symbol quotes.
|
"""Stream symbol quotes.
|
||||||
|
@ -1247,7 +1285,7 @@ async def stream_quotes(
|
||||||
# last = time.time()
|
# last = time.time()
|
||||||
|
|
||||||
|
|
||||||
def pack_position(pos: Position) -> Dict[str, Any]:
|
def pack_position(pos: Position) -> dict[str, Any]:
|
||||||
con = pos.contract
|
con = pos.contract
|
||||||
|
|
||||||
if isinstance(con, Option):
|
if isinstance(con, Option):
|
||||||
|
@ -1329,7 +1367,7 @@ async def trades_dialogue(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
loglevel: str = None,
|
loglevel: str = None,
|
||||||
|
|
||||||
) -> AsyncIterator[Dict[str, Any]]:
|
) -> AsyncIterator[dict[str, Any]]:
|
||||||
|
|
||||||
# XXX: required to propagate ``tractor`` loglevel to piker logging
|
# XXX: required to propagate ``tractor`` loglevel to piker logging
|
||||||
get_console_log(loglevel or tractor.current_actor().loglevel)
|
get_console_log(loglevel or tractor.current_actor().loglevel)
|
||||||
|
@ -1362,12 +1400,35 @@ async def trades_dialogue(
|
||||||
# ib-gw goes down? Not sure exactly how that's happening looking
|
# ib-gw goes down? Not sure exactly how that's happening looking
|
||||||
# at the eventkit code above but we should probably handle it...
|
# at the eventkit code above but we should probably handle it...
|
||||||
async for event_name, item in ib_trade_events_stream:
|
async for event_name, item in ib_trade_events_stream:
|
||||||
|
print(f' ib sending {item}')
|
||||||
|
|
||||||
|
# TODO: templating the ib statuses in comparison with other
|
||||||
|
# brokers is likely the way to go:
|
||||||
|
# https://interactivebrokers.github.io/tws-api/interfaceIBApi_1_1EWrapper.html#a17f2a02d6449710b6394d0266a353313
|
||||||
|
# short list:
|
||||||
|
# - PendingSubmit
|
||||||
|
# - PendingCancel
|
||||||
|
# - PreSubmitted (simulated orders)
|
||||||
|
# - ApiCancelled (cancelled by client before submission
|
||||||
|
# to routing)
|
||||||
|
# - Cancelled
|
||||||
|
# - Filled
|
||||||
|
# - Inactive (reject or cancelled but not by trader)
|
||||||
|
|
||||||
|
# XXX: here's some other sucky cases from the api
|
||||||
|
# - short-sale but securities haven't been located, in this
|
||||||
|
# case we should probably keep the order in some kind of
|
||||||
|
# weird state or cancel it outright?
|
||||||
|
# status='PendingSubmit', message=''),
|
||||||
|
# status='Cancelled', message='Error 404,
|
||||||
|
# reqId 1550: Order held while securities are located.'),
|
||||||
|
# status='PreSubmitted', message='')],
|
||||||
|
|
||||||
|
if event_name == 'status':
|
||||||
|
|
||||||
# XXX: begin normalization of nonsense ib_insync internal
|
# XXX: begin normalization of nonsense ib_insync internal
|
||||||
# object-state tracking representations...
|
# object-state tracking representations...
|
||||||
|
|
||||||
if event_name == 'status':
|
|
||||||
|
|
||||||
# unwrap needed data from ib_insync internal types
|
# unwrap needed data from ib_insync internal types
|
||||||
trade: Trade = item
|
trade: Trade = item
|
||||||
status: OrderStatus = trade.orderStatus
|
status: OrderStatus = trade.orderStatus
|
||||||
|
@ -1378,10 +1439,13 @@ async def trades_dialogue(
|
||||||
|
|
||||||
reqid=trade.order.orderId,
|
reqid=trade.order.orderId,
|
||||||
time_ns=time.time_ns(), # cuz why not
|
time_ns=time.time_ns(), # cuz why not
|
||||||
|
|
||||||
|
# everyone doin camel case..
|
||||||
status=status.status.lower(), # force lower case
|
status=status.status.lower(), # force lower case
|
||||||
|
|
||||||
filled=status.filled,
|
filled=status.filled,
|
||||||
reason=status.whyHeld,
|
reason=status.whyHeld,
|
||||||
|
|
||||||
# this seems to not be necessarily up to date in the
|
# this seems to not be necessarily up to date in the
|
||||||
# execDetails event.. so we have to send it here I guess?
|
# execDetails event.. so we have to send it here I guess?
|
||||||
remaining=status.remaining,
|
remaining=status.remaining,
|
||||||
|
@ -1500,6 +1564,12 @@ async def open_symbol_search(
|
||||||
if not pattern or pattern.isspace():
|
if not pattern or pattern.isspace():
|
||||||
log.warning('empty pattern received, skipping..')
|
log.warning('empty pattern received, skipping..')
|
||||||
|
|
||||||
|
# TODO: *BUG* if nothing is returned here the client
|
||||||
|
# side will cache a null set result and not showing
|
||||||
|
# anything to the use on re-searches when this query
|
||||||
|
# timed out. We probably need a special "timeout" msg
|
||||||
|
# or something...
|
||||||
|
|
||||||
# XXX: this unblocks the far end search task which may
|
# XXX: this unblocks the far end search task which may
|
||||||
# hold up a multi-search nursery block
|
# hold up a multi-search nursery block
|
||||||
await stream.send({})
|
await stream.send({})
|
||||||
|
@ -1507,22 +1577,53 @@ async def open_symbol_search(
|
||||||
continue
|
continue
|
||||||
|
|
||||||
log.debug(f'searching for {pattern}')
|
log.debug(f'searching for {pattern}')
|
||||||
# await tractor.breakpoint()
|
|
||||||
last = time.time()
|
last = time.time()
|
||||||
results = await _trio_run_client_method(
|
|
||||||
|
# async batch search using api stocks endpoint and module
|
||||||
|
# defined adhoc symbol set.
|
||||||
|
stock_results = []
|
||||||
|
|
||||||
|
async def stash_results(target: Awaitable[list]):
|
||||||
|
stock_results.extend(await target)
|
||||||
|
|
||||||
|
async with trio.open_nursery() as sn:
|
||||||
|
sn.start_soon(
|
||||||
|
stash_results,
|
||||||
|
_trio_run_client_method(
|
||||||
method='search_stocks',
|
method='search_stocks',
|
||||||
pattern=pattern,
|
pattern=pattern,
|
||||||
upto=5,
|
upto=5,
|
||||||
)
|
)
|
||||||
log.debug(f'got results {results.keys()}')
|
)
|
||||||
|
|
||||||
log.debug("fuzzy matching")
|
# trigger async request
|
||||||
matches = fuzzy.extractBests(
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
# match against our ad-hoc set immediately
|
||||||
|
adhoc_matches = fuzzy.extractBests(
|
||||||
pattern,
|
pattern,
|
||||||
results,
|
list(_adhoc_futes_set),
|
||||||
|
score_cutoff=90,
|
||||||
|
)
|
||||||
|
log.info(f'fuzzy matched adhocs: {adhoc_matches}')
|
||||||
|
adhoc_match_results = {}
|
||||||
|
if adhoc_matches:
|
||||||
|
# TODO: do we need to pull contract details?
|
||||||
|
adhoc_match_results = {i[0]: {} for i in adhoc_matches}
|
||||||
|
|
||||||
|
log.debug(f'fuzzy matching stocks {stock_results}')
|
||||||
|
stock_matches = fuzzy.extractBests(
|
||||||
|
pattern,
|
||||||
|
stock_results,
|
||||||
score_cutoff=50,
|
score_cutoff=50,
|
||||||
)
|
)
|
||||||
|
|
||||||
matches = {item[2]: item[0] for item in matches}
|
matches = adhoc_match_results | {
|
||||||
|
item[0]: {} for item in stock_matches
|
||||||
|
}
|
||||||
|
# TODO: we used to deliver contract details
|
||||||
|
# {item[2]: item[0] for item in stock_matches}
|
||||||
|
|
||||||
log.debug(f"sending matches: {matches.keys()}")
|
log.debug(f"sending matches: {matches.keys()}")
|
||||||
await stream.send(matches)
|
await stream.send(matches)
|
||||||
|
|
Loading…
Reference in New Issue