Compare commits
16 Commits
ae7cd8df7c
...
87385a4e2d
| Author | SHA1 | Date |
|---|---|---|
|
|
87385a4e2d | |
|
|
b3c5478017 | |
|
|
6c9a78c5a0 | |
|
|
da223f7a55 | |
|
|
49fe0a3398 | |
|
|
29fc3b8a8b | |
|
|
1bfe777637 | |
|
|
c694d915f1 | |
|
|
c120cb51a4 | |
|
|
7c20231f16 | |
|
|
d809c79788 | |
|
|
9f2f8a1664 | |
|
|
9f141635d1 | |
|
|
0604ca7c82 | |
|
|
82c2256271 | |
|
|
a743fa28b5 |
|
|
@ -98,13 +98,14 @@ async def open_cached_client(
|
||||||
If one has not been setup do it and cache it.
|
If one has not been setup do it and cache it.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
brokermod = get_brokermod(brokername)
|
brokermod: ModuleType = get_brokermod(brokername)
|
||||||
|
|
||||||
|
# TODO: make abstract or `typing.Protocol`
|
||||||
|
# client: Client
|
||||||
async with maybe_open_context(
|
async with maybe_open_context(
|
||||||
acm_func=brokermod.get_client,
|
acm_func=brokermod.get_client,
|
||||||
kwargs=kwargs,
|
kwargs=kwargs,
|
||||||
|
|
||||||
) as (cache_hit, client):
|
) as (cache_hit, client):
|
||||||
|
|
||||||
if cache_hit:
|
if cache_hit:
|
||||||
log.runtime(f'Reusing existing {client}')
|
log.runtime(f'Reusing existing {client}')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -374,9 +374,14 @@ class Client:
|
||||||
pair: Pair = pair_type(**item)
|
pair: Pair = pair_type(**item)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
e.add_note(
|
e.add_note(
|
||||||
"\nDon't panic, prolly stupid binance changed their symbology schema again..\n"
|
f'\n'
|
||||||
'Check out their API docs here:\n\n'
|
f'New or removed field we need to codify!\n'
|
||||||
'https://binance-docs.github.io/apidocs/spot/en/#exchange-information'
|
f'pair-type: {pair_type!r}\n'
|
||||||
|
f'\n'
|
||||||
|
f"Don't panic, prolly stupid binance changed their symbology schema again..\n"
|
||||||
|
f'Check out their API docs here:\n'
|
||||||
|
f'\n'
|
||||||
|
f'https://binance-docs.github.io/apidocs/spot/en/#exchange-information\n'
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
pair_table[pair.symbol.upper()] = pair
|
pair_table[pair.symbol.upper()] = pair
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,13 @@ class Pair(Struct, frozen=True, kw_only=True):
|
||||||
baseAsset: str
|
baseAsset: str
|
||||||
baseAssetPrecision: int
|
baseAssetPrecision: int
|
||||||
|
|
||||||
|
permissionSets: list[list[str]]
|
||||||
|
|
||||||
|
# https://developers.binance.com/docs/binance-spot-api-docs#2025-08-26
|
||||||
|
# will become non-optional 2025-08-28?
|
||||||
|
# https://developers.binance.com/docs/binance-spot-api-docs#future-changes
|
||||||
|
pegInstructionsAllowed: bool|None = None
|
||||||
|
|
||||||
filters: dict[
|
filters: dict[
|
||||||
str,
|
str,
|
||||||
str | int | float,
|
str | int | float,
|
||||||
|
|
@ -142,7 +149,11 @@ class SpotPair(Pair, frozen=True):
|
||||||
defaultSelfTradePreventionMode: str
|
defaultSelfTradePreventionMode: str
|
||||||
allowedSelfTradePreventionModes: list[str]
|
allowedSelfTradePreventionModes: list[str]
|
||||||
permissions: list[str]
|
permissions: list[str]
|
||||||
permissionSets: list[list[str]]
|
|
||||||
|
# can the paint botz creat liq gaps even easier on this asset?
|
||||||
|
# Bp
|
||||||
|
# https://developers.binance.com/docs/binance-spot-api-docs/faqs/order_amend_keep_priority
|
||||||
|
amendAllowed: bool
|
||||||
|
|
||||||
# NOTE: see `.data._symcache.SymbologyCache.load()` for why
|
# NOTE: see `.data._symcache.SymbologyCache.load()` for why
|
||||||
ns_path: str = 'piker.brokers.binance:SpotPair'
|
ns_path: str = 'piker.brokers.binance:SpotPair'
|
||||||
|
|
|
||||||
|
|
@ -471,11 +471,15 @@ def search(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# global opts
|
# global opts
|
||||||
brokermods = list(config['brokermods'].values())
|
brokermods: list[ModuleType] = list(config['brokermods'].values())
|
||||||
|
|
||||||
|
# TODO: this is coming from the `search --pdb` NOT from
|
||||||
|
# the `piker --pdb` XD ..
|
||||||
|
# -[ ] pull from the parent click ctx's values..dumdum
|
||||||
|
# assert pdb
|
||||||
|
|
||||||
# define tractor entrypoint
|
# define tractor entrypoint
|
||||||
async def main(func):
|
async def main(func):
|
||||||
|
|
||||||
async with maybe_open_pikerd(
|
async with maybe_open_pikerd(
|
||||||
loglevel=config['loglevel'],
|
loglevel=config['loglevel'],
|
||||||
debug_mode=pdb,
|
debug_mode=pdb,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ routines should be primitive data types where possible.
|
||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import (
|
||||||
|
Any,
|
||||||
|
)
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
|
@ -34,8 +36,10 @@ from ..accounting import MktPair
|
||||||
|
|
||||||
|
|
||||||
async def api(brokername: str, methname: str, **kwargs) -> dict:
|
async def api(brokername: str, methname: str, **kwargs) -> dict:
|
||||||
"""Make (proxy through) a broker API call by name and return its result.
|
'''
|
||||||
"""
|
Make (proxy through) a broker API call by name and return its result.
|
||||||
|
|
||||||
|
'''
|
||||||
brokermod = get_brokermod(brokername)
|
brokermod = get_brokermod(brokername)
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
meth = getattr(client, methname, None)
|
meth = getattr(client, methname, None)
|
||||||
|
|
@ -62,10 +66,14 @@ async def api(brokername: str, methname: str, **kwargs) -> dict:
|
||||||
|
|
||||||
async def stocks_quote(
|
async def stocks_quote(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
tickers: List[str]
|
tickers: list[str]
|
||||||
) -> Dict[str, Dict[str, Any]]:
|
|
||||||
"""Return quotes dict for ``tickers``.
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
'''
|
||||||
|
Return a `dict` of snapshot quotes for the provided input
|
||||||
|
`tickers`: a `list` of fqmes.
|
||||||
|
|
||||||
|
'''
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
return await client.quote(tickers)
|
return await client.quote(tickers)
|
||||||
|
|
||||||
|
|
@ -74,13 +82,15 @@ async def stocks_quote(
|
||||||
async def option_chain(
|
async def option_chain(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
date: Optional[str] = None,
|
date: str|None = None,
|
||||||
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
"""Return option chain for ``symbol`` for ``date``.
|
'''
|
||||||
|
Return option chain for ``symbol`` for ``date``.
|
||||||
|
|
||||||
By default all expiries are returned. If ``date`` is provided
|
By default all expiries are returned. If ``date`` is provided
|
||||||
then contract quotes for that single expiry are returned.
|
then contract quotes for that single expiry are returned.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
if date:
|
if date:
|
||||||
id = int((await client.tickers2ids([symbol]))[symbol])
|
id = int((await client.tickers2ids([symbol]))[symbol])
|
||||||
|
|
@ -98,7 +108,7 @@ async def option_chain(
|
||||||
# async def contracts(
|
# async def contracts(
|
||||||
# brokermod: ModuleType,
|
# brokermod: ModuleType,
|
||||||
# symbol: str,
|
# symbol: str,
|
||||||
# ) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
# ) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
# """Return option contracts (all expiries) for ``symbol``.
|
# """Return option contracts (all expiries) for ``symbol``.
|
||||||
# """
|
# """
|
||||||
# async with brokermod.get_client() as client:
|
# async with brokermod.get_client() as client:
|
||||||
|
|
@ -110,15 +120,24 @@ async def bars(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
"""Return option contracts (all expiries) for ``symbol``.
|
'''
|
||||||
"""
|
Return option contracts (all expiries) for ``symbol``.
|
||||||
|
|
||||||
|
'''
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
return await client.bars(symbol, **kwargs)
|
return await client.bars(symbol, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def search_w_brokerd(name: str, pattern: str) -> dict:
|
async def search_w_brokerd(
|
||||||
|
name: str,
|
||||||
|
pattern: str,
|
||||||
|
) -> dict:
|
||||||
|
|
||||||
|
# TODO: WHY NOT WORK!?!
|
||||||
|
# when we `step` through the next block?
|
||||||
|
# import tractor
|
||||||
|
# await tractor.pause()
|
||||||
async with open_cached_client(name) as client:
|
async with open_cached_client(name) as client:
|
||||||
|
|
||||||
# TODO: support multiple asset type concurrent searches.
|
# TODO: support multiple asset type concurrent searches.
|
||||||
|
|
@ -130,12 +149,12 @@ async def symbol_search(
|
||||||
pattern: str,
|
pattern: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
'''
|
'''
|
||||||
Return symbol info from broker.
|
Return symbol info from broker.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
results = []
|
results: list[str] = []
|
||||||
|
|
||||||
async def search_backend(
|
async def search_backend(
|
||||||
brokermod: ModuleType
|
brokermod: ModuleType
|
||||||
|
|
@ -143,6 +162,13 @@ async def symbol_search(
|
||||||
|
|
||||||
brokername: str = mod.name
|
brokername: str = mod.name
|
||||||
|
|
||||||
|
# TODO: figure this the FUCK OUT
|
||||||
|
# -> ok so obvi in the root actor any async task that's
|
||||||
|
# spawned outside the main tractor-root-actor task needs to
|
||||||
|
# call this..
|
||||||
|
# await tractor.devx._debug.maybe_init_greenback()
|
||||||
|
# tractor.pause_from_sync()
|
||||||
|
|
||||||
async with maybe_spawn_brokerd(
|
async with maybe_spawn_brokerd(
|
||||||
mod.name,
|
mod.name,
|
||||||
infect_asyncio=getattr(
|
infect_asyncio=getattr(
|
||||||
|
|
@ -162,7 +188,6 @@ async def symbol_search(
|
||||||
))
|
))
|
||||||
|
|
||||||
async with trio.open_nursery() as n:
|
async with trio.open_nursery() as n:
|
||||||
|
|
||||||
for mod in brokermods:
|
for mod in brokermods:
|
||||||
n.start_soon(search_backend, mod.name)
|
n.start_soon(search_backend, mod.name)
|
||||||
|
|
||||||
|
|
@ -172,11 +197,13 @@ async def symbol_search(
|
||||||
async def mkt_info(
|
async def mkt_info(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> MktPair:
|
) -> MktPair:
|
||||||
'''
|
'''
|
||||||
Return MktPair info from broker including src and dst assets.
|
Return the `piker.accounting.MktPair` info struct from a given
|
||||||
|
backend broker tradable src/dst asset pair.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with open_cached_client(brokermod.name) as client:
|
async with open_cached_client(brokermod.name) as client:
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ from piker.brokers._util import get_logger
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .api import Client
|
from .api import Client
|
||||||
from ib_insync import IB
|
from ib_insync import IB
|
||||||
|
import i3ipc
|
||||||
|
|
||||||
log = get_logger('piker.brokers.ib')
|
log = get_logger('piker.brokers.ib')
|
||||||
|
|
||||||
|
|
@ -48,6 +49,37 @@ _reset_tech: Literal[
|
||||||
] = 'vnc'
|
] = 'vnc'
|
||||||
|
|
||||||
|
|
||||||
|
no_setup_msg:str = (
|
||||||
|
'No data reset hack test setup for {vnc_sockaddr}!\n'
|
||||||
|
'See config setup tips @\n'
|
||||||
|
'https://github.com/pikers/piker/tree/master/piker/brokers/ib'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def try_xdo_manual(
|
||||||
|
vnc_sockaddr: str,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Do the "manual" `xdo`-based screen switch + click
|
||||||
|
combo since apparently the `asyncvnc` client ain't workin..
|
||||||
|
|
||||||
|
Note this is only meant as a backup method for Xorg users,
|
||||||
|
ideally you can use a real vnc client and the `vnc_click_hack()`
|
||||||
|
impl!
|
||||||
|
|
||||||
|
'''
|
||||||
|
global _reset_tech
|
||||||
|
try:
|
||||||
|
i3ipc_xdotool_manual_click_hack()
|
||||||
|
_reset_tech = 'i3ipc_xdotool'
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
log.exception(
|
||||||
|
no_setup_msg.format(vnc_sockaddr)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def data_reset_hack(
|
async def data_reset_hack(
|
||||||
# vnc_host: str,
|
# vnc_host: str,
|
||||||
client: Client,
|
client: Client,
|
||||||
|
|
@ -90,15 +122,9 @@ async def data_reset_hack(
|
||||||
vnc_port: int
|
vnc_port: int
|
||||||
vnc_sockaddr: tuple[str] | None = client.conf.get('vnc_addrs')
|
vnc_sockaddr: tuple[str] | None = client.conf.get('vnc_addrs')
|
||||||
|
|
||||||
no_setup_msg:str = (
|
|
||||||
f'No data reset hack test setup for {vnc_sockaddr}!\n'
|
|
||||||
'See config setup tips @\n'
|
|
||||||
'https://github.com/pikers/piker/tree/master/piker/brokers/ib'
|
|
||||||
)
|
|
||||||
|
|
||||||
if not vnc_sockaddr:
|
if not vnc_sockaddr:
|
||||||
log.warning(
|
log.warning(
|
||||||
no_setup_msg
|
no_setup_msg.format(vnc_sockaddr)
|
||||||
+
|
+
|
||||||
'REQUIRES A `vnc_addrs: array` ENTRY'
|
'REQUIRES A `vnc_addrs: array` ENTRY'
|
||||||
)
|
)
|
||||||
|
|
@ -119,27 +145,38 @@ async def data_reset_hack(
|
||||||
port=vnc_port,
|
port=vnc_port,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except OSError:
|
except (
|
||||||
if vnc_host != 'localhost':
|
OSError, # no VNC server avail..
|
||||||
log.warning(no_setup_msg)
|
PermissionError, # asyncvnc pw fail..
|
||||||
return False
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import i3ipc # noqa (since a deps dynamic check)
|
import i3ipc # noqa (since a deps dynamic check)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
log.warning(no_setup_msg)
|
log.warning(
|
||||||
|
no_setup_msg.format(vnc_sockaddr)
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
if vnc_host not in {
|
||||||
i3ipc_xdotool_manual_click_hack()
|
'localhost',
|
||||||
_reset_tech = 'i3ipc_xdotool'
|
'127.0.0.1',
|
||||||
return True
|
}:
|
||||||
except OSError:
|
focussed, matches = i3ipc_fin_wins_titled()
|
||||||
log.exception(no_setup_msg)
|
if not matches:
|
||||||
return False
|
log.warning(
|
||||||
|
no_setup_msg.format(vnc_sockaddr)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
try_xdo_manual(vnc_sockaddr)
|
||||||
|
|
||||||
|
# localhost but no vnc-client or it borked..
|
||||||
|
else:
|
||||||
|
try_xdo_manual(vnc_sockaddr)
|
||||||
|
|
||||||
case 'i3ipc_xdotool':
|
case 'i3ipc_xdotool':
|
||||||
i3ipc_xdotool_manual_click_hack()
|
try_xdo_manual(vnc_sockaddr)
|
||||||
|
# i3ipc_xdotool_manual_click_hack()
|
||||||
|
|
||||||
case _ as tech:
|
case _ as tech:
|
||||||
raise RuntimeError(f'{tech} is not supported for reset tech!?')
|
raise RuntimeError(f'{tech} is not supported for reset tech!?')
|
||||||
|
|
@ -178,9 +215,9 @@ async def vnc_click_hack(
|
||||||
host,
|
host,
|
||||||
port=port,
|
port=port,
|
||||||
|
|
||||||
# TODO: doesn't work see:
|
# TODO: doesn't work?
|
||||||
# https://github.com/barneygale/asyncvnc/issues/7
|
# see, https://github.com/barneygale/asyncvnc/issues/7
|
||||||
# password='ibcansmbz',
|
password='doggy',
|
||||||
|
|
||||||
) as client:
|
) as client:
|
||||||
|
|
||||||
|
|
@ -194,70 +231,103 @@ async def vnc_click_hack(
|
||||||
client.keyboard.press('Ctrl', 'Alt', key) # keys are stacked
|
client.keyboard.press('Ctrl', 'Alt', key) # keys are stacked
|
||||||
|
|
||||||
|
|
||||||
|
def i3ipc_fin_wins_titled(
|
||||||
|
titles: list[str] = [
|
||||||
|
'Interactive Brokers', # tws running in i3
|
||||||
|
'IB Gateway', # gw running in i3
|
||||||
|
# 'IB', # gw running in i3 (newer version?)
|
||||||
|
|
||||||
|
# !TODO, remote vnc instance
|
||||||
|
# -[ ] something in title (or other Con-props) that indicates
|
||||||
|
# this is explicitly for ibrk sw?
|
||||||
|
# |_[ ] !can use modden spawn eventually!
|
||||||
|
'TigerVNC',
|
||||||
|
# 'vncviewer', # the terminal..
|
||||||
|
],
|
||||||
|
) -> tuple[
|
||||||
|
i3ipc.Con, # orig focussed win
|
||||||
|
list[tuple[str, i3ipc.Con]], # matching wins by title
|
||||||
|
]:
|
||||||
|
'''
|
||||||
|
Attempt to find a local-DE window titled with an entry in
|
||||||
|
`titles`.
|
||||||
|
|
||||||
|
If found deliver the current focussed window and all matching
|
||||||
|
`i3ipc.Con`s in a list.
|
||||||
|
|
||||||
|
'''
|
||||||
|
import i3ipc
|
||||||
|
ipc = i3ipc.Connection()
|
||||||
|
|
||||||
|
# TODO: might be worth offering some kinda api for grabbing
|
||||||
|
# the window id from the pid?
|
||||||
|
# https://stackoverflow.com/a/2250879
|
||||||
|
tree = ipc.get_tree()
|
||||||
|
focussed: i3ipc.Con = tree.find_focused()
|
||||||
|
|
||||||
|
matches: list[i3ipc.Con] = []
|
||||||
|
for name in titles:
|
||||||
|
results = tree.find_titled(name)
|
||||||
|
print(f'results for {name}: {results}')
|
||||||
|
if results:
|
||||||
|
con = results[0]
|
||||||
|
matches.append((
|
||||||
|
name,
|
||||||
|
con,
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
focussed,
|
||||||
|
matches,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def i3ipc_xdotool_manual_click_hack() -> None:
|
def i3ipc_xdotool_manual_click_hack() -> None:
|
||||||
'''
|
'''
|
||||||
Do the data reset hack but expecting a local X-window using `xdotool`.
|
Do the data reset hack but expecting a local X-window using `xdotool`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
import i3ipc
|
focussed, matches = i3ipc_fin_wins_titled()
|
||||||
i3 = i3ipc.Connection()
|
orig_win_id = focussed.window
|
||||||
|
|
||||||
# TODO: might be worth offering some kinda api for grabbing
|
|
||||||
# the window id from the pid?
|
|
||||||
# https://stackoverflow.com/a/2250879
|
|
||||||
t = i3.get_tree()
|
|
||||||
|
|
||||||
orig_win_id = t.find_focused().window
|
|
||||||
|
|
||||||
# for tws
|
|
||||||
win_names: list[str] = [
|
|
||||||
'Interactive Brokers', # tws running in i3
|
|
||||||
'IB Gateway', # gw running in i3
|
|
||||||
# 'IB', # gw running in i3 (newer version?)
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for name in win_names:
|
for name, con in matches:
|
||||||
results = t.find_titled(name)
|
print(f'Resetting data feed for {name}')
|
||||||
print(f'results for {name}: {results}')
|
win_id = str(con.window)
|
||||||
if results:
|
w, h = con.rect.width, con.rect.height
|
||||||
con = results[0]
|
|
||||||
print(f'Resetting data feed for {name}')
|
|
||||||
win_id = str(con.window)
|
|
||||||
w, h = con.rect.width, con.rect.height
|
|
||||||
|
|
||||||
# TODO: seems to be a few libs for python but not sure
|
# TODO: seems to be a few libs for python but not sure
|
||||||
# if they support all the sub commands we need, order of
|
# if they support all the sub commands we need, order of
|
||||||
# most recent commit history:
|
# most recent commit history:
|
||||||
# https://github.com/rr-/pyxdotool
|
# https://github.com/rr-/pyxdotool
|
||||||
# https://github.com/ShaneHutter/pyxdotool
|
# https://github.com/ShaneHutter/pyxdotool
|
||||||
# https://github.com/cphyc/pyxdotool
|
# https://github.com/cphyc/pyxdotool
|
||||||
|
|
||||||
# TODO: only run the reconnect (2nd) kc on a detected
|
# TODO: only run the reconnect (2nd) kc on a detected
|
||||||
# disconnect?
|
# disconnect?
|
||||||
for key_combo, timeout in [
|
for key_combo, timeout in [
|
||||||
# only required if we need a connection reset.
|
# only required if we need a connection reset.
|
||||||
# ('ctrl+alt+r', 12),
|
# ('ctrl+alt+r', 12),
|
||||||
# data feed reset.
|
# data feed reset.
|
||||||
('ctrl+alt+f', 6)
|
('ctrl+alt+f', 6)
|
||||||
]:
|
]:
|
||||||
subprocess.call([
|
subprocess.call([
|
||||||
'xdotool',
|
'xdotool',
|
||||||
'windowactivate', '--sync', win_id,
|
'windowactivate', '--sync', win_id,
|
||||||
|
|
||||||
# move mouse to bottom left of window (where
|
# move mouse to bottom left of window (where
|
||||||
# there should be nothing to click).
|
# there should be nothing to click).
|
||||||
'mousemove_relative', '--sync', str(w-4), str(h-4),
|
'mousemove_relative', '--sync', str(w-4), str(h-4),
|
||||||
|
|
||||||
# NOTE: we may need to stick a `--retry 3` in here..
|
# NOTE: we may need to stick a `--retry 3` in here..
|
||||||
'click', '--window', win_id,
|
'click', '--window', win_id,
|
||||||
'--repeat', '3', '1',
|
'--repeat', '3', '1',
|
||||||
|
|
||||||
# hackzorzes
|
# hackzorzes
|
||||||
'key', key_combo,
|
'key', key_combo,
|
||||||
],
|
],
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
# re-activate and focus original window
|
# re-activate and focus original window
|
||||||
subprocess.call([
|
subprocess.call([
|
||||||
|
|
|
||||||
|
|
@ -1241,32 +1241,47 @@ async def deliver_trade_events(
|
||||||
# never relay errors for non-broker related issues
|
# never relay errors for non-broker related issues
|
||||||
# https://interactivebrokers.github.io/tws-api/message_codes.html
|
# https://interactivebrokers.github.io/tws-api/message_codes.html
|
||||||
code: int = err['error_code']
|
code: int = err['error_code']
|
||||||
if code in {
|
reason: str = err['reason']
|
||||||
200, # uhh
|
reqid: str = str(err['reqid'])
|
||||||
|
|
||||||
|
# "Warning:" msg codes,
|
||||||
|
# https://interactivebrokers.github.io/tws-api/message_codes.html#warning_codes
|
||||||
|
# - 2109: 'Outside Regular Trading Hours'
|
||||||
|
if 'Warning:' in reason:
|
||||||
|
log.warning(
|
||||||
|
f'Order-API-warning: {code!r}\n'
|
||||||
|
f'reqid: {reqid!r}\n'
|
||||||
|
f'\n'
|
||||||
|
f'{pformat(err)}\n'
|
||||||
|
# ^TODO? should we just print the `reason`
|
||||||
|
# not the full `err`-dict?
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# XXX known special (ignore) cases
|
||||||
|
elif code in {
|
||||||
|
200, # uhh.. ni idea
|
||||||
|
|
||||||
# hist pacing / connectivity
|
# hist pacing / connectivity
|
||||||
162,
|
162,
|
||||||
165,
|
165,
|
||||||
|
|
||||||
# WARNING codes:
|
|
||||||
# https://interactivebrokers.github.io/tws-api/message_codes.html#warning_codes
|
|
||||||
# Attribute 'Outside Regular Trading Hours' is
|
|
||||||
# " 'ignored based on the order type and
|
|
||||||
# destination. PlaceOrder is now ' 'being
|
|
||||||
# processed.',
|
|
||||||
2109,
|
|
||||||
|
|
||||||
# XXX: lol this isn't even documented..
|
# XXX: lol this isn't even documented..
|
||||||
# 'No market data during competing live session'
|
# 'No market data during competing live session'
|
||||||
1669,
|
1669,
|
||||||
}:
|
}:
|
||||||
|
log.error(
|
||||||
|
f'Order-API-error which is non-cancel-causing ?!\n'
|
||||||
|
f'\n'
|
||||||
|
f'{pformat(err)}\n'
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
reqid: str = str(err['reqid'])
|
|
||||||
reason: str = err['reason']
|
|
||||||
|
|
||||||
if err['reqid'] == -1:
|
if err['reqid'] == -1:
|
||||||
log.error(f'TWS external order error:\n{pformat(err)}')
|
log.error(
|
||||||
|
f'TWS external order error ??\n'
|
||||||
|
f'{pformat(err)}\n'
|
||||||
|
)
|
||||||
|
|
||||||
flow: dict = dict(
|
flow: dict = dict(
|
||||||
flows.get(reqid)
|
flows.get(reqid)
|
||||||
|
|
|
||||||
|
|
@ -587,7 +587,7 @@ async def get_bars(
|
||||||
data_cs.cancel()
|
data_cs.cancel()
|
||||||
|
|
||||||
# spawn new data reset task
|
# spawn new data reset task
|
||||||
data_cs, reset_done = await nurse.start(
|
data_cs, reset_done = await tn.start(
|
||||||
partial(
|
partial(
|
||||||
wait_on_data_reset,
|
wait_on_data_reset,
|
||||||
proxy,
|
proxy,
|
||||||
|
|
@ -607,11 +607,11 @@ async def get_bars(
|
||||||
# such that simultaneous symbol queries don't try data resettingn
|
# such that simultaneous symbol queries don't try data resettingn
|
||||||
# too fast..
|
# too fast..
|
||||||
unset_resetter: bool = False
|
unset_resetter: bool = False
|
||||||
async with trio.open_nursery() as nurse:
|
async with trio.open_nursery() as tn:
|
||||||
|
|
||||||
# start history request that we allow
|
# start history request that we allow
|
||||||
# to run indefinitely until a result is acquired
|
# to run indefinitely until a result is acquired
|
||||||
nurse.start_soon(query)
|
tn.start_soon(query)
|
||||||
|
|
||||||
# start history reset loop which waits up to the timeout
|
# start history reset loop which waits up to the timeout
|
||||||
# for a result before triggering a data feed reset.
|
# for a result before triggering a data feed reset.
|
||||||
|
|
@ -631,7 +631,7 @@ async def get_bars(
|
||||||
unset_resetter: bool = True
|
unset_resetter: bool = True
|
||||||
|
|
||||||
# spawn new data reset task
|
# spawn new data reset task
|
||||||
data_cs, reset_done = await nurse.start(
|
data_cs, reset_done = await tn.start(
|
||||||
partial(
|
partial(
|
||||||
wait_on_data_reset,
|
wait_on_data_reset,
|
||||||
proxy,
|
proxy,
|
||||||
|
|
@ -705,7 +705,9 @@ async def _setup_quote_stream(
|
||||||
# to_trio, from_aio = trio.open_memory_channel(2**8) # type: ignore
|
# to_trio, from_aio = trio.open_memory_channel(2**8) # type: ignore
|
||||||
def teardown():
|
def teardown():
|
||||||
ticker.updateEvent.disconnect(push)
|
ticker.updateEvent.disconnect(push)
|
||||||
log.error(f"Disconnected stream for `{symbol}`")
|
log.error(
|
||||||
|
f'Disconnected stream for `{symbol}`'
|
||||||
|
)
|
||||||
client.ib.cancelMktData(contract)
|
client.ib.cancelMktData(contract)
|
||||||
|
|
||||||
# decouple broadcast mem chan
|
# decouple broadcast mem chan
|
||||||
|
|
@ -761,7 +763,10 @@ async def open_aio_quote_stream(
|
||||||
symbol: str,
|
symbol: str,
|
||||||
contract: Contract | None = None,
|
contract: Contract | None = None,
|
||||||
|
|
||||||
) -> trio.abc.ReceiveStream:
|
) -> (
|
||||||
|
trio.abc.Channel| # iface
|
||||||
|
tractor.to_asyncio.LinkedTaskChannel # actually
|
||||||
|
):
|
||||||
|
|
||||||
from tractor.trionics import broadcast_receiver
|
from tractor.trionics import broadcast_receiver
|
||||||
global _quote_streams
|
global _quote_streams
|
||||||
|
|
@ -778,6 +783,7 @@ async def open_aio_quote_stream(
|
||||||
yield from_aio
|
yield from_aio
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from_aio: tractor.to_asyncio.LinkedTaskChannel
|
||||||
async with tractor.to_asyncio.open_channel_from(
|
async with tractor.to_asyncio.open_channel_from(
|
||||||
_setup_quote_stream,
|
_setup_quote_stream,
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
|
|
@ -983,17 +989,18 @@ async def stream_quotes(
|
||||||
)
|
)
|
||||||
cs: trio.CancelScope | None = None
|
cs: trio.CancelScope | None = None
|
||||||
startup: bool = True
|
startup: bool = True
|
||||||
|
iter_quotes: trio.abc.Channel
|
||||||
while (
|
while (
|
||||||
startup
|
startup
|
||||||
or cs.cancel_called
|
or cs.cancel_called
|
||||||
):
|
):
|
||||||
with trio.CancelScope() as cs:
|
with trio.CancelScope() as cs:
|
||||||
async with (
|
async with (
|
||||||
trio.open_nursery() as nurse,
|
trio.open_nursery() as tn,
|
||||||
open_aio_quote_stream(
|
open_aio_quote_stream(
|
||||||
symbol=sym,
|
symbol=sym,
|
||||||
contract=con,
|
contract=con,
|
||||||
) as stream,
|
) as iter_quotes,
|
||||||
):
|
):
|
||||||
# ugh, clear ticks since we've consumed them
|
# ugh, clear ticks since we've consumed them
|
||||||
# (ahem, ib_insync is stateful trash)
|
# (ahem, ib_insync is stateful trash)
|
||||||
|
|
@ -1021,9 +1028,9 @@ async def stream_quotes(
|
||||||
await rt_ev.wait()
|
await rt_ev.wait()
|
||||||
cs.cancel() # cancel called should now be set
|
cs.cancel() # cancel called should now be set
|
||||||
|
|
||||||
nurse.start_soon(reset_on_feed)
|
tn.start_soon(reset_on_feed)
|
||||||
|
|
||||||
async with aclosing(stream):
|
async with aclosing(iter_quotes):
|
||||||
# if syminfo.get('no_vlm', False):
|
# if syminfo.get('no_vlm', False):
|
||||||
if not init_msg.shm_write_opts['has_vlm']:
|
if not init_msg.shm_write_opts['has_vlm']:
|
||||||
|
|
||||||
|
|
@ -1038,19 +1045,21 @@ async def stream_quotes(
|
||||||
# wait for real volume on feed (trading might be
|
# wait for real volume on feed (trading might be
|
||||||
# closed)
|
# closed)
|
||||||
while True:
|
while True:
|
||||||
ticker = await stream.receive()
|
ticker = await iter_quotes.receive()
|
||||||
|
|
||||||
# for a real volume contract we rait for
|
# for a real volume contract we rait for
|
||||||
# the first "real" trade to take place
|
# the first "real" trade to take place
|
||||||
if (
|
if (
|
||||||
# not calc_price
|
# not calc_price
|
||||||
# and not ticker.rtTime
|
# and not ticker.rtTime
|
||||||
not ticker.rtTime
|
False
|
||||||
|
# not ticker.rtTime
|
||||||
):
|
):
|
||||||
# spin consuming tickers until we
|
# spin consuming tickers until we
|
||||||
# get a real market datum
|
# get a real market datum
|
||||||
log.debug(f"New unsent ticker: {ticker}")
|
log.debug(f"New unsent ticker: {ticker}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug("Received first volume tick")
|
log.debug("Received first volume tick")
|
||||||
# ugh, clear ticks since we've
|
# ugh, clear ticks since we've
|
||||||
|
|
@ -1066,13 +1075,18 @@ async def stream_quotes(
|
||||||
log.debug(f"First ticker received {quote}")
|
log.debug(f"First ticker received {quote}")
|
||||||
|
|
||||||
# tell data-layer spawner-caller that live
|
# tell data-layer spawner-caller that live
|
||||||
# quotes are now streaming.
|
# quotes are now active desptie not having
|
||||||
|
# necessarily received a first vlm/clearing
|
||||||
|
# tick.
|
||||||
|
ticker = await iter_quotes.receive()
|
||||||
feed_is_live.set()
|
feed_is_live.set()
|
||||||
|
fqme: str = quote['fqme']
|
||||||
|
await send_chan.send({fqme: quote})
|
||||||
|
|
||||||
# last = time.time()
|
# last = time.time()
|
||||||
async for ticker in stream:
|
async for ticker in iter_quotes:
|
||||||
quote = normalize(ticker)
|
quote = normalize(ticker)
|
||||||
fqme = quote['fqme']
|
fqme: str = quote['fqme']
|
||||||
await send_chan.send({fqme: quote})
|
await send_chan.send({fqme: quote})
|
||||||
|
|
||||||
# ugh, clear ticks since we've consumed them
|
# ugh, clear ticks since we've consumed them
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import urllib.parse
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import base64
|
import base64
|
||||||
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from piker import config
|
from piker import config
|
||||||
|
|
@ -372,8 +373,7 @@ class Client:
|
||||||
# 1658347714, 'status': 'Success'}]}
|
# 1658347714, 'status': 'Success'}]}
|
||||||
|
|
||||||
if xfers:
|
if xfers:
|
||||||
import tractor
|
await tractor.pause()
|
||||||
await tractor.pp()
|
|
||||||
|
|
||||||
trans: dict[str, Transaction] = {}
|
trans: dict[str, Transaction] = {}
|
||||||
for entry in xfers:
|
for entry in xfers:
|
||||||
|
|
@ -501,7 +501,8 @@ class Client:
|
||||||
for xkey, data in resp['result'].items():
|
for xkey, data in resp['result'].items():
|
||||||
|
|
||||||
# NOTE: always cache in pairs tables for faster lookup
|
# NOTE: always cache in pairs tables for faster lookup
|
||||||
pair = Pair(xname=xkey, **data)
|
with tractor.devx.maybe_open_crash_handler(): # as bxerr:
|
||||||
|
pair = Pair(xname=xkey, **data)
|
||||||
|
|
||||||
# register the above `Pair` structs for all
|
# register the above `Pair` structs for all
|
||||||
# key-sets/monikers: a set of 4 (frickin) tables
|
# key-sets/monikers: a set of 4 (frickin) tables
|
||||||
|
|
|
||||||
|
|
@ -175,9 +175,8 @@ async def handle_order_requests(
|
||||||
|
|
||||||
case {
|
case {
|
||||||
'account': 'kraken.spot' as account,
|
'account': 'kraken.spot' as account,
|
||||||
'action': action,
|
'action': 'buy'|'sell',
|
||||||
} if action in {'buy', 'sell'}:
|
}:
|
||||||
|
|
||||||
# validate
|
# validate
|
||||||
order = BrokerdOrder(**msg)
|
order = BrokerdOrder(**msg)
|
||||||
|
|
||||||
|
|
@ -262,6 +261,12 @@ async def handle_order_requests(
|
||||||
} | extra
|
} | extra
|
||||||
|
|
||||||
log.info(f'Submitting WS order request:\n{pformat(req)}')
|
log.info(f'Submitting WS order request:\n{pformat(req)}')
|
||||||
|
|
||||||
|
# NOTE HOWTO, debug order requests
|
||||||
|
#
|
||||||
|
# if 'XRP' in pair:
|
||||||
|
# await tractor.pause()
|
||||||
|
|
||||||
await ws.send_msg(req)
|
await ws.send_msg(req)
|
||||||
|
|
||||||
# placehold for sanity checking in relay loop
|
# placehold for sanity checking in relay loop
|
||||||
|
|
@ -544,7 +549,7 @@ async def open_trade_dialog(
|
||||||
# to be reloaded.
|
# to be reloaded.
|
||||||
balances: dict[str, float] = await client.get_balances()
|
balances: dict[str, float] = await client.get_balances()
|
||||||
|
|
||||||
verify_balances(
|
await verify_balances(
|
||||||
acnt,
|
acnt,
|
||||||
src_fiat,
|
src_fiat,
|
||||||
balances,
|
balances,
|
||||||
|
|
@ -1085,6 +1090,8 @@ async def handle_order_updates(
|
||||||
f'Failed to {action} order {reqid}:\n'
|
f'Failed to {action} order {reqid}:\n'
|
||||||
f'{errmsg}'
|
f'{errmsg}'
|
||||||
)
|
)
|
||||||
|
# if tractor._state.debug_mode():
|
||||||
|
# await tractor.pause()
|
||||||
|
|
||||||
symbol: str = 'N/A'
|
symbol: str = 'N/A'
|
||||||
if chain := apiflows.get(reqid):
|
if chain := apiflows.get(reqid):
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ Symbology defs and search.
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
from rapidfuzz import process as fuzzy
|
|
||||||
|
|
||||||
from piker._cacheables import (
|
from piker._cacheables import (
|
||||||
async_lifo_cache,
|
async_lifo_cache,
|
||||||
|
|
@ -41,8 +40,13 @@ from piker.accounting._mktinfo import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# https://www.kraken.com/features/api#get-tradable-pairs
|
|
||||||
class Pair(Struct):
|
class Pair(Struct):
|
||||||
|
'''
|
||||||
|
A tradable asset pair as schema-defined by,
|
||||||
|
|
||||||
|
https://docs.kraken.com/api/docs/rest-api/get-tradable-asset-pairs
|
||||||
|
|
||||||
|
'''
|
||||||
xname: str # idiotic bs_mktid equiv i guess?
|
xname: str # idiotic bs_mktid equiv i guess?
|
||||||
altname: str # alternate pair name
|
altname: str # alternate pair name
|
||||||
wsname: str # WebSocket pair name (if available)
|
wsname: str # WebSocket pair name (if available)
|
||||||
|
|
@ -53,7 +57,6 @@ class Pair(Struct):
|
||||||
lot: str # volume lot size
|
lot: str # volume lot size
|
||||||
|
|
||||||
cost_decimals: int
|
cost_decimals: int
|
||||||
costmin: float
|
|
||||||
pair_decimals: int # scaling decimal places for pair
|
pair_decimals: int # scaling decimal places for pair
|
||||||
lot_decimals: int # scaling decimal places for volume
|
lot_decimals: int # scaling decimal places for volume
|
||||||
|
|
||||||
|
|
@ -79,6 +82,7 @@ class Pair(Struct):
|
||||||
tick_size: float # min price step size
|
tick_size: float # min price step size
|
||||||
status: str
|
status: str
|
||||||
|
|
||||||
|
costmin: str|None = None # XXX, only some mktpairs?
|
||||||
short_position_limit: float = 0
|
short_position_limit: float = 0
|
||||||
long_position_limit: float = float('inf')
|
long_position_limit: float = float('inf')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,12 @@ import tractor
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
|
# TODO, port to `httpx`/`trio-websocket` whenver i get back to
|
||||||
|
# writing a proper ws-api streamer for this backend (since the data
|
||||||
|
# feeds are free now) as per GH feat-req:
|
||||||
|
# https://github.com/pikers/piker/issues/509
|
||||||
|
#
|
||||||
import asks
|
import asks
|
||||||
|
|
||||||
from ..calc import humanize, percent_change
|
from ..calc import humanize, percent_change
|
||||||
|
|
|
||||||
|
|
@ -740,7 +740,7 @@ async def sample_and_broadcast(
|
||||||
|
|
||||||
log.warning(
|
log.warning(
|
||||||
f'Feed OVERRUN {sub_key}'
|
f'Feed OVERRUN {sub_key}'
|
||||||
'@{bus.brokername} -> \n'
|
f'@{bus.brokername} -> \n'
|
||||||
f'feed @ {chan.uid}\n'
|
f'feed @ {chan.uid}\n'
|
||||||
f'throttle = {throttle} Hz'
|
f'throttle = {throttle} Hz'
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -786,7 +786,6 @@ async def install_brokerd_search(
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def maybe_open_feed(
|
async def maybe_open_feed(
|
||||||
|
|
||||||
fqmes: list[str],
|
fqmes: list[str],
|
||||||
loglevel: str | None = None,
|
loglevel: str | None = None,
|
||||||
|
|
||||||
|
|
@ -840,13 +839,12 @@ async def maybe_open_feed(
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def open_feed(
|
async def open_feed(
|
||||||
|
|
||||||
fqmes: list[str],
|
fqmes: list[str],
|
||||||
|
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
allow_overruns: bool = True,
|
allow_overruns: bool = True,
|
||||||
start_stream: bool = True,
|
start_stream: bool = True,
|
||||||
tick_throttle: float | None = None, # Hz
|
tick_throttle: float|None = None, # Hz
|
||||||
|
|
||||||
allow_remote_ctl_ui: bool = False,
|
allow_remote_ctl_ui: bool = False,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,10 @@ from ._sharedmem import (
|
||||||
ShmArray,
|
ShmArray,
|
||||||
_Token,
|
_Token,
|
||||||
)
|
)
|
||||||
|
from piker.accounting import MktPair
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..accounting import MktPair
|
from piker.data.feed import Feed
|
||||||
from .feed import Feed
|
|
||||||
|
|
||||||
|
|
||||||
class Flume(Struct):
|
class Flume(Struct):
|
||||||
|
|
@ -82,7 +82,7 @@ class Flume(Struct):
|
||||||
|
|
||||||
# TODO: do we need this really if we can pull the `Portal` from
|
# TODO: do we need this really if we can pull the `Portal` from
|
||||||
# ``tractor``'s internals?
|
# ``tractor``'s internals?
|
||||||
feed: Feed | None = None
|
feed: Feed|None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rt_shm(self) -> ShmArray:
|
def rt_shm(self) -> ShmArray:
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,9 @@ def validate_backend(
|
||||||
)
|
)
|
||||||
if ep is None:
|
if ep is None:
|
||||||
log.warning(
|
log.warning(
|
||||||
f'Provider backend {mod.name} is missing '
|
f'Provider backend {mod.name!r} is missing '
|
||||||
f'{daemon_name} support :(\n'
|
f'{daemon_name!r} support?\n'
|
||||||
f'The following endpoint is missing: {name}'
|
f'|_module endpoint-func missing: {name!r}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
inits: list[
|
inits: list[
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue