Compare commits
32 Commits
47cd48fea7
...
89f9763744
| Author | SHA1 | Date |
|---|---|---|
|
|
89f9763744 | |
|
|
abd9b18a25 | |
|
|
b8a7d58aed | |
|
|
e433f24917 | |
|
|
53b4775698 | |
|
|
49f75ee1fe | |
|
|
cf70fb9f2e | |
|
|
50011d33ef | |
|
|
903d39b615 | |
|
|
3dad1c9835 | |
|
|
b98c39598f | |
|
|
31a31fae0f | |
|
|
1dcbe7f22a | |
|
|
9c6f22c2c3 | |
|
|
70b663992c | |
|
|
1827cdcf0f | |
|
|
19d647f574 | |
|
|
7c8baa8f4e | |
|
|
ed58ae139f | |
|
|
f2f696d341 | |
|
|
e01ba6e31e | |
|
|
e08fb267f8 | |
|
|
041f0d7e6f | |
|
|
b0ac3cd9b0 | |
|
|
13edd7f709 | |
|
|
52c1c9c0fd | |
|
|
e5b9fe97b4 | |
|
|
76eefb97e2 | |
|
|
2e31ecbbe7 | |
|
|
81f770bb22 | |
|
|
0dc6af3c1e | |
|
|
f116d1a03b |
|
|
@ -19,8 +19,10 @@
|
||||||
for tendiez.
|
for tendiez.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from ..log import get_logger
|
from piker.log import (
|
||||||
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
|
)
|
||||||
from .calc import (
|
from .calc import (
|
||||||
iter_by_dt,
|
iter_by_dt,
|
||||||
)
|
)
|
||||||
|
|
@ -51,7 +53,17 @@ from ._allocate import (
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
# ?TODO, enable console on import
|
||||||
|
# [ ] necessary? or `open_brokerd_dialog()` doing it is sufficient?
|
||||||
|
#
|
||||||
|
# bc might as well enable whenev imported by
|
||||||
|
# other sub-sys code (namely `.clearing`).
|
||||||
|
get_console_log(
|
||||||
|
level='warning',
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO, the `as <samename>` style?
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Account',
|
'Account',
|
||||||
'Allocator',
|
'Allocator',
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,16 @@ from ..clearing._messages import (
|
||||||
BrokerdPosition,
|
BrokerdPosition,
|
||||||
)
|
)
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker.log import get_logger
|
from piker.log import (
|
||||||
|
get_logger,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from piker.data._symcache import SymbologyCache
|
from piker.data._symcache import SymbologyCache
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Position(Struct):
|
class Position(Struct):
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ CLI front end for trades ledger and position tracking management.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
import polars as pl
|
import polars as pl
|
||||||
|
|
@ -29,7 +28,10 @@ import tractor
|
||||||
import trio
|
import trio
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
from ..log import get_logger
|
from piker.log import (
|
||||||
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
|
)
|
||||||
from ..service import (
|
from ..service import (
|
||||||
open_piker_runtime,
|
open_piker_runtime,
|
||||||
)
|
)
|
||||||
|
|
@ -45,6 +47,7 @@ from .calc import (
|
||||||
open_ledger_dfs,
|
open_ledger_dfs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
ledger = typer.Typer()
|
ledger = typer.Typer()
|
||||||
|
|
||||||
|
|
@ -79,7 +82,10 @@ def sync(
|
||||||
"-l",
|
"-l",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
log = get_logger(loglevel)
|
log = get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
pair: tuple[str, str]
|
pair: tuple[str, str]
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,16 @@ from types import ModuleType
|
||||||
|
|
||||||
from tractor.trionics import maybe_open_context
|
from tractor.trionics import maybe_open_context
|
||||||
|
|
||||||
|
from piker.log import (
|
||||||
|
get_logger,
|
||||||
|
)
|
||||||
from ._util import (
|
from ._util import (
|
||||||
log,
|
|
||||||
BrokerError,
|
BrokerError,
|
||||||
SymbolNotFound,
|
SymbolNotFound,
|
||||||
NoData,
|
NoData,
|
||||||
DataUnavailable,
|
DataUnavailable,
|
||||||
DataThrottle,
|
DataThrottle,
|
||||||
resproc,
|
resproc,
|
||||||
get_logger,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__: list[str] = [
|
__all__: list[str] = [
|
||||||
|
|
@ -43,7 +44,6 @@ __all__: list[str] = [
|
||||||
'DataUnavailable',
|
'DataUnavailable',
|
||||||
'DataThrottle',
|
'DataThrottle',
|
||||||
'resproc',
|
'resproc',
|
||||||
'get_logger',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
__brokers__: list[str] = [
|
__brokers__: list[str] = [
|
||||||
|
|
@ -65,6 +65,10 @@ __brokers__: list[str] = [
|
||||||
# bitso
|
# bitso
|
||||||
]
|
]
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_brokermod(brokername: str) -> ModuleType:
|
def get_brokermod(brokername: str) -> ModuleType:
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,18 @@ import exceptiongroup as eg
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
from piker.log import (
|
||||||
|
get_logger,
|
||||||
|
get_console_log,
|
||||||
|
)
|
||||||
from . import _util
|
from . import _util
|
||||||
from . import get_brokermod
|
from . import get_brokermod
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..data import _FeedsBus
|
from ..data import _FeedsBus
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
# `brokerd` enabled modules
|
# `brokerd` enabled modules
|
||||||
# TODO: move this def to the `.data` subpkg..
|
# TODO: move this def to the `.data` subpkg..
|
||||||
# NOTE: keeping this list as small as possible is part of our caps-sec
|
# NOTE: keeping this list as small as possible is part of our caps-sec
|
||||||
|
|
@ -59,7 +65,7 @@ _data_mods: str = [
|
||||||
async def _setup_persistent_brokerd(
|
async def _setup_persistent_brokerd(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
brokername: str,
|
brokername: str,
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -72,13 +78,14 @@ async def _setup_persistent_brokerd(
|
||||||
# since all hosted daemon tasks will reference this same
|
# since all hosted daemon tasks will reference this same
|
||||||
# log instance's (actor local) state and thus don't require
|
# log instance's (actor local) state and thus don't require
|
||||||
# any further (level) configuration on their own B)
|
# any further (level) configuration on their own B)
|
||||||
log = _util.get_console_log(
|
actor: tractor.Actor = tractor.current_actor()
|
||||||
loglevel or tractor.current_actor().loglevel,
|
tll: str = actor.loglevel
|
||||||
|
log = get_console_log(
|
||||||
|
level=loglevel or tll,
|
||||||
name=f'{_util.subsys}.{brokername}',
|
name=f'{_util.subsys}.{brokername}',
|
||||||
|
with_tractor_log=bool(tll),
|
||||||
)
|
)
|
||||||
|
assert log.name == _util.subsys
|
||||||
# set global for this actor to this new process-wide instance B)
|
|
||||||
_util.log = log
|
|
||||||
|
|
||||||
# further, set the log level on any broker broker specific
|
# further, set the log level on any broker broker specific
|
||||||
# logger instance.
|
# logger instance.
|
||||||
|
|
@ -97,7 +104,7 @@ async def _setup_persistent_brokerd(
|
||||||
# NOTE: see ep invocation details inside `.data.feed`.
|
# NOTE: see ep invocation details inside `.data.feed`.
|
||||||
try:
|
try:
|
||||||
async with (
|
async with (
|
||||||
tractor.trionics.collapse_eg(),
|
# tractor.trionics.collapse_eg(),
|
||||||
trio.open_nursery() as service_nursery
|
trio.open_nursery() as service_nursery
|
||||||
):
|
):
|
||||||
bus: _FeedsBus = feed.get_feed_bus(
|
bus: _FeedsBus = feed.get_feed_bus(
|
||||||
|
|
@ -193,7 +200,6 @@ def broker_init(
|
||||||
|
|
||||||
|
|
||||||
async def spawn_brokerd(
|
async def spawn_brokerd(
|
||||||
|
|
||||||
brokername: str,
|
brokername: str,
|
||||||
loglevel: str | None = None,
|
loglevel: str | None = None,
|
||||||
|
|
||||||
|
|
@ -201,8 +207,10 @@ async def spawn_brokerd(
|
||||||
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
from piker.service._util import log # use service mngr log
|
log.info(
|
||||||
log.info(f'Spawning {brokername} broker daemon')
|
f'Spawning broker-daemon,\n'
|
||||||
|
f'backend: {brokername!r}'
|
||||||
|
)
|
||||||
|
|
||||||
(
|
(
|
||||||
brokermode,
|
brokermode,
|
||||||
|
|
@ -249,7 +257,7 @@ async def spawn_brokerd(
|
||||||
async def maybe_spawn_brokerd(
|
async def maybe_spawn_brokerd(
|
||||||
|
|
||||||
brokername: str,
|
brokername: str,
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
|
|
||||||
**pikerd_kwargs,
|
**pikerd_kwargs,
|
||||||
|
|
||||||
|
|
@ -264,6 +272,11 @@ async def maybe_spawn_brokerd(
|
||||||
'''
|
'''
|
||||||
from piker.service import maybe_spawn_daemon
|
from piker.service import maybe_spawn_daemon
|
||||||
|
|
||||||
|
# if (
|
||||||
|
# loglevel != 'info'
|
||||||
|
# ):
|
||||||
|
# await tractor.pause()
|
||||||
|
|
||||||
async with maybe_spawn_daemon(
|
async with maybe_spawn_daemon(
|
||||||
|
|
||||||
f'brokerd.{brokername}',
|
f'brokerd.{brokername}',
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,13 @@ Handy cross-broker utils.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from functools import partial
|
# from functools import partial
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..log import (
|
from piker.log import (
|
||||||
get_logger,
|
|
||||||
get_console_log,
|
|
||||||
colorize_json,
|
colorize_json,
|
||||||
)
|
)
|
||||||
subsys: str = 'piker.brokers'
|
subsys: str = 'piker.brokers'
|
||||||
|
|
@ -35,12 +33,22 @@ subsys: str = 'piker.brokers'
|
||||||
# NOTE: level should be reset by any actor that is spawned
|
# NOTE: level should be reset by any actor that is spawned
|
||||||
# as well as given a (more) explicit name/key such
|
# as well as given a (more) explicit name/key such
|
||||||
# as `piker.brokers.binance` matching the subpkg.
|
# as `piker.brokers.binance` matching the subpkg.
|
||||||
log = get_logger(subsys)
|
# log = get_logger(subsys)
|
||||||
|
|
||||||
get_console_log = partial(
|
# ?TODO?? we could use this approach, but we need to be able
|
||||||
get_console_log,
|
# to pass multiple `name=` values so for example we can include the
|
||||||
name=subsys,
|
# emissions in `.accounting._pos` and others!
|
||||||
)
|
# [ ] maybe we could do the `log = get_logger()` above,
|
||||||
|
# then cycle through the list of subsys mods we depend on
|
||||||
|
# and then get all their loggers and pass them to
|
||||||
|
# `get_console_log(logger=)`??
|
||||||
|
# [ ] OR just write THIS `get_console_log()` as a hook which does
|
||||||
|
# that based on who calls it?.. i dunno
|
||||||
|
#
|
||||||
|
# get_console_log = partial(
|
||||||
|
# get_console_log,
|
||||||
|
# name=subsys,
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
class BrokerError(Exception):
|
class BrokerError(Exception):
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,9 @@ import trio
|
||||||
from piker.accounting import (
|
from piker.accounting import (
|
||||||
Asset,
|
Asset,
|
||||||
)
|
)
|
||||||
from piker.brokers._util import (
|
from piker.log import (
|
||||||
get_logger,
|
get_logger,
|
||||||
|
get_console_log,
|
||||||
)
|
)
|
||||||
from piker.data._web_bs import (
|
from piker.data._web_bs import (
|
||||||
open_autorecon_ws,
|
open_autorecon_ws,
|
||||||
|
|
@ -69,7 +70,9 @@ from .venues import (
|
||||||
)
|
)
|
||||||
from .api import Client
|
from .api import Client
|
||||||
|
|
||||||
log = get_logger('piker.brokers.binance')
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Fee schedule template, mostly for paper engine fees modelling.
|
# Fee schedule template, mostly for paper engine fees modelling.
|
||||||
|
|
@ -245,9 +248,16 @@ async def handle_order_requests(
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def open_trade_dialog(
|
async def open_trade_dialog(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
|
loglevel: str = 'warning',
|
||||||
|
|
||||||
) -> AsyncIterator[dict[str, Any]]:
|
) -> AsyncIterator[dict[str, Any]]:
|
||||||
|
|
||||||
|
# enable piker.clearing console log for *this* `brokerd` subactor
|
||||||
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: how do we set this from the EMS such that
|
# TODO: how do we set this from the EMS such that
|
||||||
# positions are loaded from the correct venue on the user
|
# positions are loaded from the correct venue on the user
|
||||||
# stream at startup? (that is in an attempt to support both
|
# stream at startup? (that is in an attempt to support both
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,9 @@ from piker.data._web_bs import (
|
||||||
open_autorecon_ws,
|
open_autorecon_ws,
|
||||||
NoBsWs,
|
NoBsWs,
|
||||||
)
|
)
|
||||||
|
from piker.log import get_logger
|
||||||
from piker.brokers._util import (
|
from piker.brokers._util import (
|
||||||
DataUnavailable,
|
DataUnavailable,
|
||||||
get_logger,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api import (
|
from .api import (
|
||||||
|
|
@ -78,7 +78,7 @@ from .venues import (
|
||||||
get_api_eps,
|
get_api_eps,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = get_logger('piker.brokers.binance')
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
class L1(Struct):
|
class L1(Struct):
|
||||||
|
|
@ -237,8 +237,8 @@ async def open_history_client(
|
||||||
|
|
||||||
async def get_ohlc(
|
async def get_ohlc(
|
||||||
timeframe: float,
|
timeframe: float,
|
||||||
end_dt: datetime | None = None,
|
end_dt: datetime|None = None,
|
||||||
start_dt: datetime | None = None,
|
start_dt: datetime|None = None,
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
np.ndarray,
|
np.ndarray,
|
||||||
|
|
@ -297,7 +297,7 @@ async def open_history_client(
|
||||||
async def get_mkt_info(
|
async def get_mkt_info(
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
||||||
) -> tuple[MktPair, Pair] | None:
|
) -> tuple[MktPair, Pair]|None:
|
||||||
|
|
||||||
# uppercase since kraken bs_mktid is always upper
|
# uppercase since kraken bs_mktid is always upper
|
||||||
if 'binance' not in fqme.lower():
|
if 'binance' not in fqme.lower():
|
||||||
|
|
@ -374,7 +374,7 @@ async def get_mkt_info(
|
||||||
if 'futes' in mkt_mode:
|
if 'futes' in mkt_mode:
|
||||||
assert isinstance(pair, FutesPair)
|
assert isinstance(pair, FutesPair)
|
||||||
|
|
||||||
dst: Asset | None = assets.get(pair.bs_dst_asset)
|
dst: Asset|None = assets.get(pair.bs_dst_asset)
|
||||||
if (
|
if (
|
||||||
not dst
|
not dst
|
||||||
# TODO: a known asset DNE list?
|
# TODO: a known asset DNE list?
|
||||||
|
|
@ -433,7 +433,7 @@ async def subscribe(
|
||||||
# might get ack from ws server, or maybe some
|
# might get ack from ws server, or maybe some
|
||||||
# other msg still in transit..
|
# other msg still in transit..
|
||||||
res = await ws.recv_msg()
|
res = await ws.recv_msg()
|
||||||
subid: str | None = res.get('id')
|
subid: str|None = res.get('id')
|
||||||
if subid:
|
if subid:
|
||||||
assert res['id'] == subid
|
assert res['id'] == subid
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,12 @@ import click
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
from ..cli import cli
|
from piker.cli import cli
|
||||||
from .. import watchlists as wl
|
from piker import watchlists as wl
|
||||||
from ..log import (
|
from piker.log import (
|
||||||
colorize_json,
|
colorize_json,
|
||||||
)
|
|
||||||
from ._util import (
|
|
||||||
log,
|
|
||||||
get_console_log,
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
)
|
)
|
||||||
from ..service import (
|
from ..service import (
|
||||||
maybe_spawn_brokerd,
|
maybe_spawn_brokerd,
|
||||||
|
|
@ -45,12 +43,15 @@ from ..brokers import (
|
||||||
get_brokermod,
|
get_brokermod,
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
DEFAULT_BROKER = 'binance'
|
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_BROKER = 'binance'
|
||||||
_config_dir = click.get_app_dir('piker')
|
_config_dir = click.get_app_dir('piker')
|
||||||
_watchlists_data_path = os.path.join(_config_dir, 'watchlists.json')
|
_watchlists_data_path = os.path.join(_config_dir, 'watchlists.json')
|
||||||
|
|
||||||
|
|
||||||
OK = '\033[92m'
|
OK = '\033[92m'
|
||||||
WARNING = '\033[93m'
|
WARNING = '\033[93m'
|
||||||
FAIL = '\033[91m'
|
FAIL = '\033[91m'
|
||||||
|
|
@ -345,7 +346,10 @@ def contracts(ctx, loglevel, broker, symbol, ids):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
brokermod = get_brokermod(broker)
|
brokermod = get_brokermod(broker)
|
||||||
get_console_log(loglevel)
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
contracts = trio.run(partial(core.contracts, brokermod, symbol))
|
contracts = trio.run(partial(core.contracts, brokermod, symbol))
|
||||||
if not ids:
|
if not ids:
|
||||||
|
|
@ -477,11 +481,12 @@ def search(
|
||||||
# the `piker --pdb` XD ..
|
# the `piker --pdb` XD ..
|
||||||
# -[ ] pull from the parent click ctx's values..dumdum
|
# -[ ] pull from the parent click ctx's values..dumdum
|
||||||
# assert pdb
|
# assert pdb
|
||||||
|
loglevel: str = config['loglevel']
|
||||||
|
|
||||||
# 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=loglevel,
|
||||||
debug_mode=pdb,
|
debug_mode=pdb,
|
||||||
):
|
):
|
||||||
return await func()
|
return await func()
|
||||||
|
|
@ -494,6 +499,7 @@ def search(
|
||||||
core.symbol_search,
|
core.symbol_search,
|
||||||
brokermods,
|
brokermods,
|
||||||
pattern,
|
pattern,
|
||||||
|
loglevel=loglevel,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,14 @@ from typing import (
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from ._util import log
|
from piker.log import get_logger
|
||||||
from . import get_brokermod
|
from . import get_brokermod
|
||||||
from ..service import maybe_spawn_brokerd
|
from ..service import maybe_spawn_brokerd
|
||||||
from . import open_cached_client
|
from . import open_cached_client
|
||||||
from ..accounting import MktPair
|
from ..accounting import MktPair
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
async def api(brokername: str, methname: str, **kwargs) -> dict:
|
async def api(brokername: str, methname: str, **kwargs) -> dict:
|
||||||
'''
|
'''
|
||||||
|
|
@ -147,6 +149,7 @@ async def search_w_brokerd(
|
||||||
async def symbol_search(
|
async def symbol_search(
|
||||||
brokermods: list[ModuleType],
|
brokermods: list[ModuleType],
|
||||||
pattern: str,
|
pattern: str,
|
||||||
|
loglevel: str = 'warning',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> dict[str, dict[str, dict[str, Any]]]:
|
) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
|
|
@ -176,6 +179,7 @@ async def symbol_search(
|
||||||
'_infect_asyncio',
|
'_infect_asyncio',
|
||||||
False,
|
False,
|
||||||
),
|
),
|
||||||
|
loglevel=loglevel
|
||||||
) as portal:
|
) as portal:
|
||||||
|
|
||||||
results.append((
|
results.append((
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,15 @@ import tractor
|
||||||
from tractor.experimental import msgpub
|
from tractor.experimental import msgpub
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
|
|
||||||
from ._util import (
|
from piker.log import(
|
||||||
log,
|
get_logger,
|
||||||
get_console_log,
|
get_console_log,
|
||||||
)
|
)
|
||||||
from . import get_brokermod
|
from . import get_brokermod
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name='piker.brokers.binance',
|
||||||
|
)
|
||||||
|
|
||||||
async def wait_for_network(
|
async def wait_for_network(
|
||||||
net_func: Callable,
|
net_func: Callable,
|
||||||
|
|
@ -243,7 +246,10 @@ async def start_quote_stream(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# XXX: why do we need this again?
|
# XXX: why do we need this again?
|
||||||
get_console_log(tractor.current_actor().loglevel)
|
get_console_log(
|
||||||
|
level=tractor.current_actor().loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
# pull global vars from local actor
|
# pull global vars from local actor
|
||||||
symbols = list(symbols)
|
symbols = list(symbols)
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,13 @@ import subprocess
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
from piker.brokers._util import get_logger
|
from piker.log import get_logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .api import Client
|
from .api import Client
|
||||||
import i3ipc
|
import i3ipc
|
||||||
|
|
||||||
log = get_logger('piker.brokers.ib')
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
_reset_tech: Literal[
|
_reset_tech: Literal[
|
||||||
'vnc',
|
'vnc',
|
||||||
|
|
@ -326,7 +326,6 @@ def i3ipc_fin_wins_titled(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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`.
|
||||||
|
|
@ -388,99 +387,3 @@ def i3ipc_xdotool_manual_click_hack() -> None:
|
||||||
])
|
])
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
log.exception('xdotool timed out?')
|
log.exception('xdotool timed out?')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def is_current_time_in_range(
|
|
||||||
start_dt: datetime,
|
|
||||||
end_dt: datetime,
|
|
||||||
) -> bool:
|
|
||||||
'''
|
|
||||||
Check if current time is within the datetime range.
|
|
||||||
|
|
||||||
Use any/the-same timezone as provided by `start_dt.tzinfo` value
|
|
||||||
in the range.
|
|
||||||
|
|
||||||
'''
|
|
||||||
now: datetime = datetime.now(start_dt.tzinfo)
|
|
||||||
return start_dt <= now <= end_dt
|
|
||||||
|
|
||||||
|
|
||||||
# TODO, put this into `._util` and call it from here!
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
# it's downstream meths..
|
|
||||||
#
|
|
||||||
# This is still likely useful to keep for now to parse the
|
|
||||||
# `.tradingHours: str` value manually if we ever decide
|
|
||||||
# to move off `ib_async` and implement our own `trio`/`anyio`
|
|
||||||
# based version Bp
|
|
||||||
#
|
|
||||||
# >attempt to parse the retarted ib "time stampy thing" they
|
|
||||||
# >do for "venue hours" with this.. written by
|
|
||||||
# >gpt5-"thinking",
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
def parse_trading_hours(
|
|
||||||
spec: str,
|
|
||||||
tz: TzInfo|None = None
|
|
||||||
) -> dict[
|
|
||||||
date,
|
|
||||||
tuple[datetime, datetime]
|
|
||||||
]|None:
|
|
||||||
'''
|
|
||||||
Parse venue hours like:
|
|
||||||
'YYYYMMDD:HHMM-YYYYMMDD:HHMM;YYYYMMDD:CLOSED;...'
|
|
||||||
|
|
||||||
Returns `dict[date] = (open_dt, close_dt)` or `None` if
|
|
||||||
closed.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if (
|
|
||||||
not isinstance(spec, str)
|
|
||||||
or
|
|
||||||
not spec
|
|
||||||
):
|
|
||||||
raise ValueError('spec must be a non-empty string')
|
|
||||||
|
|
||||||
out: dict[
|
|
||||||
date,
|
|
||||||
tuple[datetime, datetime]
|
|
||||||
]|None = {}
|
|
||||||
|
|
||||||
for part in (p.strip() for p in spec.split(';') if p.strip()):
|
|
||||||
if part.endswith(':CLOSED'):
|
|
||||||
day_s, _ = part.split(':', 1)
|
|
||||||
d = datetime.strptime(day_s, '%Y%m%d').date()
|
|
||||||
out[d] = None
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
start_s, end_s = part.split('-', 1)
|
|
||||||
start_dt = datetime.strptime(start_s, '%Y%m%d:%H%M')
|
|
||||||
end_dt = datetime.strptime(end_s, '%Y%m%d:%H%M')
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(f'invalid segment: {part}') from exc
|
|
||||||
|
|
||||||
if tz is not None:
|
|
||||||
start_dt = start_dt.replace(tzinfo=tz)
|
|
||||||
end_dt = end_dt.replace(tzinfo=tz)
|
|
||||||
|
|
||||||
out[start_dt.date()] = (start_dt, end_dt)
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
# ORIG desired usage,
|
|
||||||
#
|
|
||||||
# TODO, for non-drunk tomorrow,
|
|
||||||
# - call above fn and check that `output[today] is not None`
|
|
||||||
# trading_hrs: dict = parse_trading_hours(
|
|
||||||
# details.tradingHours
|
|
||||||
# )
|
|
||||||
# liq_hrs: dict = parse_trading_hours(
|
|
||||||
# details.liquidHours
|
|
||||||
# )
|
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,11 @@ import tractor
|
||||||
from tractor import to_asyncio
|
from tractor import to_asyncio
|
||||||
from tractor import trionics
|
from tractor import trionics
|
||||||
from pendulum import (
|
from pendulum import (
|
||||||
from_timestamp,
|
|
||||||
DateTime,
|
DateTime,
|
||||||
Duration,
|
Duration,
|
||||||
duration as mk_duration,
|
duration as mk_duration,
|
||||||
|
from_timestamp,
|
||||||
|
Interval,
|
||||||
)
|
)
|
||||||
from eventkit import Event
|
from eventkit import Event
|
||||||
from ib_insync import (
|
from ib_insync import (
|
||||||
|
|
@ -91,10 +92,15 @@ from .symbols import (
|
||||||
_exch_skip_list,
|
_exch_skip_list,
|
||||||
_futes_venues,
|
_futes_venues,
|
||||||
)
|
)
|
||||||
from ._util import (
|
from ...log import get_logger
|
||||||
log,
|
from .venues import (
|
||||||
# only for the ib_sync internal logging
|
is_venue_open,
|
||||||
get_logger,
|
sesh_times,
|
||||||
|
is_venue_closure,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
)
|
)
|
||||||
|
|
||||||
_bar_load_dtype: list[tuple[str, type]] = [
|
_bar_load_dtype: list[tuple[str, type]] = [
|
||||||
|
|
@ -180,7 +186,7 @@ class NonShittyIB(IB):
|
||||||
# override `ib_insync` internal loggers so we can see wtf
|
# override `ib_insync` internal loggers so we can see wtf
|
||||||
# it's doing..
|
# it's doing..
|
||||||
self._logger = get_logger(
|
self._logger = get_logger(
|
||||||
'ib_insync.ib',
|
name=__name__,
|
||||||
)
|
)
|
||||||
self._createEvents()
|
self._createEvents()
|
||||||
|
|
||||||
|
|
@ -188,7 +194,7 @@ class NonShittyIB(IB):
|
||||||
self.wrapper = NonShittyWrapper(self)
|
self.wrapper = NonShittyWrapper(self)
|
||||||
self.client = ib_client.Client(self.wrapper)
|
self.client = ib_client.Client(self.wrapper)
|
||||||
self.client._logger = get_logger(
|
self.client._logger = get_logger(
|
||||||
'ib_insync.client',
|
name='ib_insync.client',
|
||||||
)
|
)
|
||||||
|
|
||||||
# self.errorEvent += self._onError
|
# self.errorEvent += self._onError
|
||||||
|
|
@ -260,6 +266,16 @@ def remove_handler_on_err(
|
||||||
event.disconnect(handler)
|
event.disconnect(handler)
|
||||||
|
|
||||||
|
|
||||||
|
# (originally?) i thot that,
|
||||||
|
# > "EST in ISO 8601 format is required.."
|
||||||
|
#
|
||||||
|
# XXX, but see `ib_async`'s impl,
|
||||||
|
# - `ib_async.ib.IB.reqHistoricalDataAsync()`
|
||||||
|
# - `ib_async.util.formatIBDatetime()`
|
||||||
|
# below is EPOCH.
|
||||||
|
_iso8601_epoch_in_est: str = "1970-01-01T00:00:00.000000-05:00"
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
'''
|
'''
|
||||||
IB wrapped for our broker backend API.
|
IB wrapped for our broker backend API.
|
||||||
|
|
@ -333,9 +349,11 @@ class Client:
|
||||||
self,
|
self,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
||||||
# EST in ISO 8601 format is required... below is EPOCH
|
# EST in ISO 8601 format is required..
|
||||||
start_dt: datetime|str = "1970-01-01T00:00:00.000000-05:00",
|
# XXX, see `ib_async.ib.IB.reqHistoricalDataAsync()`
|
||||||
end_dt: datetime|str = "",
|
# below is EPOCH.
|
||||||
|
start_dt: datetime|None = None, # _iso8601_epoch_in_est,
|
||||||
|
end_dt: datetime|None = None,
|
||||||
|
|
||||||
# ohlc sample period in seconds
|
# ohlc sample period in seconds
|
||||||
sample_period_s: int = 1,
|
sample_period_s: int = 1,
|
||||||
|
|
@ -346,9 +364,17 @@ class Client:
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> tuple[BarDataList, np.ndarray, Duration]:
|
) -> tuple[
|
||||||
|
BarDataList,
|
||||||
|
np.ndarray,
|
||||||
|
Duration,
|
||||||
|
]:
|
||||||
'''
|
'''
|
||||||
Retreive OHLCV bars for a fqme over a range to the present.
|
Retreive the `fqme`'s OHLCV-bars for the time-range "until `end_dt`".
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- IB's api doesn't support a `start_dt` (which is why default
|
||||||
|
is null) so we only use it for bar-frame duration checking.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# See API docs here:
|
# See API docs here:
|
||||||
|
|
@ -363,13 +389,19 @@ class Client:
|
||||||
|
|
||||||
dt_duration: Duration = (
|
dt_duration: Duration = (
|
||||||
duration
|
duration
|
||||||
or default_dt_duration
|
or
|
||||||
|
default_dt_duration
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: maybe remove all this?
|
# TODO: maybe remove all this?
|
||||||
global _enters
|
global _enters
|
||||||
if not end_dt:
|
if end_dt is None:
|
||||||
end_dt = ''
|
end_dt: str = ''
|
||||||
|
|
||||||
|
else:
|
||||||
|
est_end_dt = end_dt.in_tz('EST')
|
||||||
|
if est_end_dt != end_dt:
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
_enters += 1
|
_enters += 1
|
||||||
|
|
||||||
|
|
@ -438,58 +470,116 @@ class Client:
|
||||||
+ query_info
|
+ query_info
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: we could maybe raise ``NoData`` instead if we
|
# TODO: we could maybe raise `NoData` instead if we
|
||||||
# rewrite the method in the first case?
|
# rewrite the method in the first case?
|
||||||
# right now there's no way to detect a timeout..
|
# right now there's no way to detect a timeout..
|
||||||
return [], np.empty(0), dt_duration
|
return [], np.empty(0), dt_duration
|
||||||
|
|
||||||
log.info(query_info)
|
log.info(query_info)
|
||||||
|
|
||||||
|
# ------ GAP-DETECTION ------
|
||||||
# NOTE XXX: ensure minimum duration in bars?
|
# NOTE XXX: ensure minimum duration in bars?
|
||||||
# => recursively call this method until we get at least as
|
# => recursively call this method until we get at least as
|
||||||
# many bars such that they sum in aggregate to the the
|
# many bars such that they sum in aggregate to the the
|
||||||
# desired total time (duration) at most.
|
# desired total time (duration) at most.
|
||||||
# - if you query over a gap and get no data
|
# - if you query over a gap and get no data
|
||||||
# that may short circuit the history
|
# that may short circuit the history
|
||||||
if (
|
if end_dt:
|
||||||
# XXX XXX XXX
|
|
||||||
# => WHY DID WE EVEN NEED THIS ORIGINALLY!? <=
|
|
||||||
# XXX XXX XXX
|
|
||||||
False
|
|
||||||
and end_dt
|
|
||||||
):
|
|
||||||
nparr: np.ndarray = bars_to_np(bars)
|
nparr: np.ndarray = bars_to_np(bars)
|
||||||
times: np.ndarray = nparr['time']
|
times: np.ndarray = nparr['time']
|
||||||
first: float = times[0]
|
first: float = times[0]
|
||||||
tdiff: float = times[-1] - first
|
last: float = times[-1]
|
||||||
|
# frame_dur: float = times[-1] - first
|
||||||
|
|
||||||
|
details: ContractDetails = (
|
||||||
|
await self.ib.reqContractDetailsAsync(contract)
|
||||||
|
)[0]
|
||||||
|
# convert to makt-native tz
|
||||||
|
tz: str = details.timeZoneId
|
||||||
|
end_dt = end_dt.in_tz(tz)
|
||||||
|
first_dt: DateTime = from_timestamp(first).in_tz(tz)
|
||||||
|
last_dt: DateTime = from_timestamp(last).in_tz(tz)
|
||||||
|
tdiff: int = (
|
||||||
|
last_dt
|
||||||
|
-
|
||||||
|
first_dt
|
||||||
|
).in_seconds() + sample_period_s
|
||||||
|
_open_now: bool = is_venue_open(
|
||||||
|
con_deats=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
# XXX, do gap detections.
|
||||||
|
has_closure_gap: bool = False
|
||||||
|
if (
|
||||||
|
last_dt.add(seconds=sample_period_s)
|
||||||
|
<
|
||||||
|
end_dt
|
||||||
|
):
|
||||||
|
open_time, close_time = sesh_times(details)
|
||||||
|
# XXX, always calc gap in mkt-venue-local timezone
|
||||||
|
gap: Interval = end_dt - last_dt
|
||||||
|
if not (
|
||||||
|
has_closure_gap := is_venue_closure(
|
||||||
|
gap=gap,
|
||||||
|
con_deats=details,
|
||||||
|
time_step_s=sample_period_s,
|
||||||
|
)):
|
||||||
|
log.warning(
|
||||||
|
f'Invalid non-closure gap for {fqme!r} ?!?\n'
|
||||||
|
f'is-open-now: {_open_now}\n'
|
||||||
|
f'\n'
|
||||||
|
f'{gap}\n'
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
f'Detected NON venue-closure GAP ??\n'
|
||||||
|
f'{gap}\n'
|
||||||
|
)
|
||||||
|
breakpoint()
|
||||||
|
else:
|
||||||
|
assert has_closure_gap
|
||||||
|
log.debug(
|
||||||
|
f'Detected venue closure gap (weekend),\n'
|
||||||
|
f'{gap}\n'
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
# len(bars) * sample_period_s) < dt_duration.in_seconds()
|
start_dt is None
|
||||||
tdiff < dt_duration.in_seconds()
|
and (
|
||||||
# and False
|
tdiff
|
||||||
|
<
|
||||||
|
dt_duration.in_seconds()
|
||||||
|
)
|
||||||
|
and
|
||||||
|
not has_closure_gap
|
||||||
):
|
):
|
||||||
end_dt: DateTime = from_timestamp(first)
|
log.error(
|
||||||
log.warning(
|
|
||||||
f'Frame result was shorter then {dt_duration}!?\n'
|
f'Frame result was shorter then {dt_duration}!?\n'
|
||||||
'Recursing for more bars:\n'
|
|
||||||
f'end_dt: {end_dt}\n'
|
f'end_dt: {end_dt}\n'
|
||||||
f'dt_duration: {dt_duration}\n'
|
f'dt_duration: {dt_duration}\n'
|
||||||
|
# f'\n'
|
||||||
|
# f'Recursing for more bars:\n'
|
||||||
)
|
)
|
||||||
(
|
# XXX, debug!
|
||||||
r_bars,
|
breakpoint()
|
||||||
r_arr,
|
# XXX ? TODO? recursively try to re-request?
|
||||||
r_duration,
|
# => i think *NO* right?
|
||||||
) = await self.bars(
|
#
|
||||||
fqme,
|
# (
|
||||||
start_dt=start_dt,
|
# r_bars,
|
||||||
end_dt=end_dt,
|
# r_arr,
|
||||||
sample_period_s=sample_period_s,
|
# r_duration,
|
||||||
|
# ) = await self.bars(
|
||||||
|
# fqme,
|
||||||
|
# start_dt=start_dt,
|
||||||
|
# end_dt=end_dt,
|
||||||
|
# sample_period_s=sample_period_s,
|
||||||
|
|
||||||
# TODO: make a table for Duration to
|
# # TODO: make a table for Duration to
|
||||||
# the ib str values in order to use this?
|
# # the ib str values in order to use this?
|
||||||
# duration=duration,
|
# # duration=duration,
|
||||||
)
|
# )
|
||||||
r_bars.extend(bars)
|
# r_bars.extend(bars)
|
||||||
bars = r_bars
|
# bars = r_bars
|
||||||
|
|
||||||
nparr: np.ndarray = bars_to_np(bars)
|
nparr: np.ndarray = bars_to_np(bars)
|
||||||
|
|
||||||
|
|
@ -784,9 +874,16 @@ class Client:
|
||||||
# crypto$
|
# crypto$
|
||||||
elif exch == 'PAXOS': # btc.paxos
|
elif exch == 'PAXOS': # btc.paxos
|
||||||
con = Crypto(
|
con = Crypto(
|
||||||
symbol=symbol,
|
symbol=symbol.upper(),
|
||||||
currency=currency,
|
currency='USD',
|
||||||
|
exchange='PAXOS',
|
||||||
)
|
)
|
||||||
|
# XXX, on `ib_insync` 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',
|
||||||
|
# > exchange='PAXOS', currency='USD',
|
||||||
|
# > localSymbol='BTC.USD', tradingClass='BTC')
|
||||||
|
|
||||||
# stonks
|
# stonks
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,10 @@ from ib_insync.objects import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from piker import config
|
from piker import config
|
||||||
|
from piker.log import (
|
||||||
|
get_logger,
|
||||||
|
get_console_log,
|
||||||
|
)
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker.accounting import (
|
from piker.accounting import (
|
||||||
Position,
|
Position,
|
||||||
|
|
@ -77,7 +81,6 @@ from piker.clearing._messages import (
|
||||||
BrokerdFill,
|
BrokerdFill,
|
||||||
BrokerdError,
|
BrokerdError,
|
||||||
)
|
)
|
||||||
from ._util import log
|
|
||||||
from .api import (
|
from .api import (
|
||||||
_accounts2clients,
|
_accounts2clients,
|
||||||
get_config,
|
get_config,
|
||||||
|
|
@ -95,6 +98,10 @@ from .ledger import (
|
||||||
update_ledger_from_api_trades,
|
update_ledger_from_api_trades,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def pack_position(
|
def pack_position(
|
||||||
pos: IbPosition,
|
pos: IbPosition,
|
||||||
|
|
@ -536,9 +543,15 @@ class IbAcnt(Struct):
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def open_trade_dialog(
|
async def open_trade_dialog(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
|
loglevel: str = 'warning',
|
||||||
|
|
||||||
) -> AsyncIterator[dict[str, Any]]:
|
) -> AsyncIterator[dict[str, Any]]:
|
||||||
|
|
||||||
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
# task local msg dialog tracking
|
# task local msg dialog tracking
|
||||||
flows = OrderDialogs()
|
flows = OrderDialogs()
|
||||||
accounts_def = config.load_accounts(['ib'])
|
accounts_def = config.load_accounts(['ib'])
|
||||||
|
|
|
||||||
|
|
@ -56,11 +56,11 @@ from piker.brokers._util import (
|
||||||
NoData,
|
NoData,
|
||||||
DataUnavailable,
|
DataUnavailable,
|
||||||
)
|
)
|
||||||
|
from piker.log import get_logger
|
||||||
from .api import (
|
from .api import (
|
||||||
# _adhoc_futes_set,
|
# _adhoc_futes_set,
|
||||||
Client,
|
Client,
|
||||||
con2fqme,
|
con2fqme,
|
||||||
log,
|
|
||||||
load_aio_clients,
|
load_aio_clients,
|
||||||
MethodProxy,
|
MethodProxy,
|
||||||
open_client_proxies,
|
open_client_proxies,
|
||||||
|
|
@ -69,15 +69,18 @@ from .api import (
|
||||||
Contract,
|
Contract,
|
||||||
RequestError,
|
RequestError,
|
||||||
)
|
)
|
||||||
|
from .venues import is_venue_open
|
||||||
from ._util import (
|
from ._util import (
|
||||||
data_reset_hack,
|
data_reset_hack,
|
||||||
is_current_time_in_range,
|
|
||||||
)
|
)
|
||||||
from .symbols import get_mkt_info
|
from .symbols import get_mkt_info
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from trio._core._run import Task
|
from trio._core._run import Task
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
# XXX NOTE: See available types table docs:
|
# XXX NOTE: See available types table docs:
|
||||||
# https://interactivebrokers.github.io/tws-api/tick_types.html
|
# https://interactivebrokers.github.io/tws-api/tick_types.html
|
||||||
|
|
@ -203,7 +206,8 @@ async def open_history_client(
|
||||||
latency = time.time() - query_start
|
latency = time.time() - query_start
|
||||||
if (
|
if (
|
||||||
not timedout
|
not timedout
|
||||||
# and latency <= max_timeout
|
# and
|
||||||
|
# latency <= max_timeout
|
||||||
):
|
):
|
||||||
count += 1
|
count += 1
|
||||||
mean += latency / count
|
mean += latency / count
|
||||||
|
|
@ -219,8 +223,10 @@ async def open_history_client(
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
end_dt
|
end_dt
|
||||||
and head_dt
|
and
|
||||||
and end_dt <= head_dt
|
head_dt
|
||||||
|
and
|
||||||
|
end_dt <= head_dt
|
||||||
):
|
):
|
||||||
raise DataUnavailable(
|
raise DataUnavailable(
|
||||||
f'First timestamp is {head_dt}\n'
|
f'First timestamp is {head_dt}\n'
|
||||||
|
|
@ -278,7 +284,7 @@ async def open_history_client(
|
||||||
start_dt
|
start_dt
|
||||||
):
|
):
|
||||||
# TODO! rm this once we're more confident it never hits!
|
# TODO! rm this once we're more confident it never hits!
|
||||||
breakpoint()
|
# breakpoint()
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f'OHLC-bars array start is gt `start_dt` limit !!\n'
|
f'OHLC-bars array start is gt `start_dt` limit !!\n'
|
||||||
f'start_dt: {start_dt}\n'
|
f'start_dt: {start_dt}\n'
|
||||||
|
|
@ -298,7 +304,7 @@ async def open_history_client(
|
||||||
# TODO: it seems like we can do async queries for ohlc
|
# TODO: it seems like we can do async queries for ohlc
|
||||||
# but getting the order right still isn't working and I'm not
|
# but getting the order right still isn't working and I'm not
|
||||||
# quite sure why.. needs some tinkering and probably
|
# quite sure why.. needs some tinkering and probably
|
||||||
# a lookthrough of the ``ib_insync`` machinery, for eg. maybe
|
# a lookthrough of the `ib_insync` machinery, for eg. maybe
|
||||||
# we have to do the batch queries on the `asyncio` side?
|
# we have to do the batch queries on the `asyncio` side?
|
||||||
yield (
|
yield (
|
||||||
get_hist,
|
get_hist,
|
||||||
|
|
@ -421,14 +427,13 @@ _failed_resets: int = 0
|
||||||
|
|
||||||
|
|
||||||
async def get_bars(
|
async def get_bars(
|
||||||
|
|
||||||
proxy: MethodProxy,
|
proxy: MethodProxy,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
timeframe: int,
|
timeframe: int,
|
||||||
|
|
||||||
# blank to start which tells ib to look up the latest datum
|
# blank to start which tells ib to look up the latest datum
|
||||||
end_dt: str = '',
|
end_dt: datetime|None = None,
|
||||||
start_dt: str|None = '',
|
start_dt: datetime|None = None,
|
||||||
|
|
||||||
# TODO: make this more dynamic based on measured frame rx latency?
|
# TODO: make this more dynamic based on measured frame rx latency?
|
||||||
# how long before we trigger a feed reset (seconds)
|
# how long before we trigger a feed reset (seconds)
|
||||||
|
|
@ -482,7 +487,8 @@ async def get_bars(
|
||||||
dt_duration,
|
dt_duration,
|
||||||
) = await proxy.bars(
|
) = await proxy.bars(
|
||||||
fqme=fqme,
|
fqme=fqme,
|
||||||
# XXX TODO! lol we're not using this..
|
# XXX TODO! LOL we're not using this and IB dun
|
||||||
|
# support it anyway..
|
||||||
# start_dt=start_dt,
|
# start_dt=start_dt,
|
||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
sample_period_s=timeframe,
|
sample_period_s=timeframe,
|
||||||
|
|
@ -734,7 +740,7 @@ async def _setup_quote_stream(
|
||||||
# '294', # Trade rate / minute
|
# '294', # Trade rate / minute
|
||||||
# '295', # Vlm rate / minute
|
# '295', # Vlm rate / minute
|
||||||
),
|
),
|
||||||
contract: Contract | None = None,
|
contract: Contract|None = None,
|
||||||
|
|
||||||
) -> trio.abc.ReceiveChannel:
|
) -> trio.abc.ReceiveChannel:
|
||||||
'''
|
'''
|
||||||
|
|
@ -756,7 +762,12 @@ async def _setup_quote_stream(
|
||||||
# XXX since this is an `asyncio.Task`, we must use
|
# XXX since this is an `asyncio.Task`, we must use
|
||||||
# tractor.pause_from_sync()
|
# tractor.pause_from_sync()
|
||||||
|
|
||||||
caccount_name, client = get_preferred_data_client(accts2clients)
|
(
|
||||||
|
_account_name,
|
||||||
|
client,
|
||||||
|
) = get_preferred_data_client(
|
||||||
|
accts2clients,
|
||||||
|
)
|
||||||
contract = (
|
contract = (
|
||||||
contract
|
contract
|
||||||
or
|
or
|
||||||
|
|
@ -1091,14 +1102,9 @@ async def stream_quotes(
|
||||||
)
|
)
|
||||||
|
|
||||||
# is venue active rn?
|
# is venue active rn?
|
||||||
venue_is_open: bool = any(
|
venue_is_open: bool = is_venue_open(
|
||||||
is_current_time_in_range(
|
con_deats=details,
|
||||||
start_dt=sesh.start,
|
|
||||||
end_dt=sesh.end,
|
|
||||||
)
|
|
||||||
for sesh in details.tradingSessions()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
init_msg = FeedInit(mkt_info=mkt)
|
init_msg = FeedInit(mkt_info=mkt)
|
||||||
|
|
||||||
# NOTE, tell sampler (via config) to skip vlm summing for dst
|
# NOTE, tell sampler (via config) to skip vlm summing for dst
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ from ib_insync import (
|
||||||
CommissionReport,
|
CommissionReport,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from piker.log import get_logger
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker.data import (
|
from piker.data import (
|
||||||
SymbologyCache,
|
SymbologyCache,
|
||||||
|
|
@ -57,7 +58,6 @@ from piker.accounting import (
|
||||||
iter_by_dt,
|
iter_by_dt,
|
||||||
)
|
)
|
||||||
from ._flex_reports import parse_flex_dt
|
from ._flex_reports import parse_flex_dt
|
||||||
from ._util import log
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .api import (
|
from .api import (
|
||||||
|
|
@ -65,6 +65,9 @@ if TYPE_CHECKING:
|
||||||
MethodProxy,
|
MethodProxy,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
tx_sort: Callable = partial(
|
tx_sort: Callable = partial(
|
||||||
iter_by_dt,
|
iter_by_dt,
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,7 @@ from piker.accounting import (
|
||||||
from piker._cacheables import (
|
from piker._cacheables import (
|
||||||
async_lifo_cache,
|
async_lifo_cache,
|
||||||
)
|
)
|
||||||
|
from piker.log import get_logger
|
||||||
from ._util import (
|
|
||||||
log,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .api import (
|
from .api import (
|
||||||
|
|
@ -53,6 +50,10 @@ if TYPE_CHECKING:
|
||||||
Client,
|
Client,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
_futes_venues = (
|
_futes_venues = (
|
||||||
'GLOBEX',
|
'GLOBEX',
|
||||||
'NYMEX',
|
'NYMEX',
|
||||||
|
|
@ -134,7 +135,7 @@ _adhoc_fiat_set = set((
|
||||||
|
|
||||||
# manually discovered tick discrepancies,
|
# manually discovered tick discrepancies,
|
||||||
# onl god knows how or why they'd cuck these up..
|
# onl god knows how or why they'd cuck these up..
|
||||||
_adhoc_mkt_infos: dict[int | str, dict] = {
|
_adhoc_mkt_infos: dict[int|str, dict] = {
|
||||||
'vtgn.nasdaq': {'price_tick': Decimal('0.01')},
|
'vtgn.nasdaq': {'price_tick': Decimal('0.01')},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,8 +489,7 @@ def con2fqme(
|
||||||
@async_lifo_cache()
|
@async_lifo_cache()
|
||||||
async def get_mkt_info(
|
async def get_mkt_info(
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
proxy: MethodProxy|None = None,
|
||||||
proxy: MethodProxy | None = None,
|
|
||||||
|
|
||||||
) -> tuple[MktPair, ibis.ContractDetails]:
|
) -> tuple[MktPair, ibis.ContractDetails]:
|
||||||
|
|
||||||
|
|
@ -550,7 +550,7 @@ async def get_mkt_info(
|
||||||
size_tick: Decimal = Decimal(
|
size_tick: Decimal = Decimal(
|
||||||
str(details.minSize).rstrip('0')
|
str(details.minSize).rstrip('0')
|
||||||
)
|
)
|
||||||
# |-> TODO: there is also the Contract.sizeIncrement, bt wtf is it?
|
# ?TODO, there is also the Contract.sizeIncrement, bt wtf is it?
|
||||||
|
|
||||||
# NOTE: this is duplicate from the .broker.norm_trade_records()
|
# NOTE: this is duplicate from the .broker.norm_trade_records()
|
||||||
# routine, we should factor all this parsing somewhere..
|
# routine, we should factor all this parsing somewhere..
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
# piker: trading gear for hackers
|
||||||
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
(Multi-)venue mgmt helpers.
|
||||||
|
|
||||||
|
IB generally supports all "legacy" trading venues, those mostly owned
|
||||||
|
by ICE and friends.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
from datetime import ( # noqa
|
||||||
|
datetime,
|
||||||
|
date,
|
||||||
|
tzinfo as TzInfo,
|
||||||
|
)
|
||||||
|
from typing import (
|
||||||
|
Iterator,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
|
import exchange_calendars as xcals
|
||||||
|
from pendulum import (
|
||||||
|
now,
|
||||||
|
Duration,
|
||||||
|
Interval,
|
||||||
|
Time,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ib_insync import (
|
||||||
|
TradingSession,
|
||||||
|
ContractDetails,
|
||||||
|
)
|
||||||
|
from exchange_calendars.exchange_calendars import (
|
||||||
|
ExchangeCalendar,
|
||||||
|
)
|
||||||
|
from pandas import (
|
||||||
|
# DatetimeIndex,
|
||||||
|
TimeDelta,
|
||||||
|
Timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_weekend(
|
||||||
|
period: Interval,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Predicate to for a period being within
|
||||||
|
days 6->0 (sat->sun).
|
||||||
|
|
||||||
|
'''
|
||||||
|
has_weekend: bool = False
|
||||||
|
for dt in period:
|
||||||
|
if dt.day_of_week in [0, 6]: # 0=Sunday, 6=Saturday
|
||||||
|
has_weekend = True
|
||||||
|
break
|
||||||
|
|
||||||
|
return has_weekend
|
||||||
|
|
||||||
|
|
||||||
|
def has_holiday(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
period: Interval,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Using the `exchange_calendars` lib detect if a time-gap `period`
|
||||||
|
is contained in a known "cash hours" closure.
|
||||||
|
|
||||||
|
'''
|
||||||
|
tz: str = con_deats.timeZoneId
|
||||||
|
exch: str = con_deats.contract.primaryExchange
|
||||||
|
cal: ExchangeCalendar = xcals.get_calendar(exch)
|
||||||
|
end: datetime = period.end
|
||||||
|
# _start: datetime = period.start
|
||||||
|
# ?TODO, can rm ya?
|
||||||
|
# => not that useful?
|
||||||
|
# dti: DatetimeIndex = cal.sessions_in_range(
|
||||||
|
# _start.date(),
|
||||||
|
# end.date(),
|
||||||
|
# )
|
||||||
|
prev_close: Timestamp = cal.previous_close(
|
||||||
|
end.date()
|
||||||
|
).tz_convert(tz)
|
||||||
|
prev_open: Timestamp = cal.previous_open(
|
||||||
|
end.date()
|
||||||
|
).tz_convert(tz)
|
||||||
|
# now do relative from prev_ values ^
|
||||||
|
# to get the next open which should match
|
||||||
|
# "contain" the end of the gap.
|
||||||
|
next_open: Timestamp = cal.next_open(
|
||||||
|
prev_open,
|
||||||
|
).tz_convert(tz)
|
||||||
|
next_open: Timestamp = cal.next_open(
|
||||||
|
prev_open,
|
||||||
|
).tz_convert(tz)
|
||||||
|
_next_close: Timestamp = cal.next_close(
|
||||||
|
prev_close
|
||||||
|
).tz_convert(tz)
|
||||||
|
cash_gap: TimeDelta = next_open - prev_close
|
||||||
|
is_holiday_gap = (
|
||||||
|
cash_gap
|
||||||
|
>
|
||||||
|
period
|
||||||
|
)
|
||||||
|
# XXX, debug
|
||||||
|
# breakpoint()
|
||||||
|
return is_holiday_gap
|
||||||
|
|
||||||
|
|
||||||
|
def is_current_time_in_range(
|
||||||
|
sesh: Interval,
|
||||||
|
when: datetime|None = None,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Check if current time is within the datetime range.
|
||||||
|
|
||||||
|
Use any/the-same timezone as provided by `start_dt.tzinfo` value
|
||||||
|
in the range.
|
||||||
|
|
||||||
|
'''
|
||||||
|
when: datetime = when or now()
|
||||||
|
return when in sesh
|
||||||
|
|
||||||
|
|
||||||
|
def iter_sessions(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
) -> Iterator[Interval]:
|
||||||
|
'''
|
||||||
|
Yield `pendulum.Interval`s for all
|
||||||
|
`ibas.ContractDetails.tradingSessions() -> TradingSession`s.
|
||||||
|
|
||||||
|
'''
|
||||||
|
sesh: TradingSession
|
||||||
|
for sesh in con_deats.tradingSessions():
|
||||||
|
yield Interval(*sesh)
|
||||||
|
|
||||||
|
|
||||||
|
def sesh_times(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
) -> tuple[Time, Time]:
|
||||||
|
'''
|
||||||
|
Based on the earliest trading session provided by the IB API,
|
||||||
|
get the (day-agnostic) times for the start/end.
|
||||||
|
|
||||||
|
'''
|
||||||
|
earliest_sesh: Interval = next(iter_sessions(con_deats))
|
||||||
|
return (
|
||||||
|
earliest_sesh.start.time(),
|
||||||
|
earliest_sesh.end.time(),
|
||||||
|
)
|
||||||
|
# ^?TODO, use `.diff()` to get point-in-time-agnostic period?
|
||||||
|
# https://pendulum.eustace.io/docs/#difference
|
||||||
|
|
||||||
|
|
||||||
|
def is_venue_open(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
when: datetime|Duration|None = None,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Check if market-venue is open during `when`, which defaults to
|
||||||
|
"now".
|
||||||
|
|
||||||
|
'''
|
||||||
|
sesh: Interval
|
||||||
|
for sesh in iter_sessions(con_deats):
|
||||||
|
if is_current_time_in_range(
|
||||||
|
sesh=sesh,
|
||||||
|
when=when,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_venue_closure(
|
||||||
|
gap: Interval,
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
time_step_s: int,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Check if a provided time-`gap` is just an (expected) trading
|
||||||
|
venue closure period.
|
||||||
|
|
||||||
|
'''
|
||||||
|
open: Time
|
||||||
|
close: Time
|
||||||
|
open, close = sesh_times(con_deats)
|
||||||
|
|
||||||
|
# ensure times are in mkt-native timezone
|
||||||
|
tz: str = con_deats.timeZoneId
|
||||||
|
start = gap.start.in_tz(tz)
|
||||||
|
start_t = start.time()
|
||||||
|
end = gap.end.in_tz(tz)
|
||||||
|
end_t = end.time()
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
start_t in (
|
||||||
|
close,
|
||||||
|
close.subtract(seconds=time_step_s)
|
||||||
|
)
|
||||||
|
and
|
||||||
|
end_t in (
|
||||||
|
open,
|
||||||
|
open.add(seconds=time_step_s),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or
|
||||||
|
has_weekend(gap)
|
||||||
|
or
|
||||||
|
has_holiday(
|
||||||
|
con_deats=con_deats,
|
||||||
|
period=gap,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# breakpoint()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# TODO, put this into `._util` and call it from here!
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# it's downstream meths..
|
||||||
|
#
|
||||||
|
# This is still likely useful to keep for now to parse the
|
||||||
|
# `.tradingHours: str` value manually if we ever decide
|
||||||
|
# to move off `ib_async` and implement our own `trio`/`anyio`
|
||||||
|
# based version Bp
|
||||||
|
#
|
||||||
|
# >attempt to parse the retarted ib "time stampy thing" they
|
||||||
|
# >do for "venue hours" with this.. written by
|
||||||
|
# >gpt5-"thinking",
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
def parse_trading_hours(
|
||||||
|
spec: str,
|
||||||
|
tz: TzInfo|None = None
|
||||||
|
) -> dict[
|
||||||
|
date,
|
||||||
|
tuple[datetime, datetime]
|
||||||
|
]|None:
|
||||||
|
'''
|
||||||
|
Parse venue hours like:
|
||||||
|
'YYYYMMDD:HHMM-YYYYMMDD:HHMM;YYYYMMDD:CLOSED;...'
|
||||||
|
|
||||||
|
Returns `dict[date] = (open_dt, close_dt)` or `None` if
|
||||||
|
closed.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if (
|
||||||
|
not isinstance(spec, str)
|
||||||
|
or
|
||||||
|
not spec
|
||||||
|
):
|
||||||
|
raise ValueError('spec must be a non-empty string')
|
||||||
|
|
||||||
|
out: dict[
|
||||||
|
date,
|
||||||
|
tuple[datetime, datetime]
|
||||||
|
]|None = {}
|
||||||
|
|
||||||
|
for part in (p.strip() for p in spec.split(';') if p.strip()):
|
||||||
|
if part.endswith(':CLOSED'):
|
||||||
|
day_s, _ = part.split(':', 1)
|
||||||
|
d = datetime.strptime(day_s, '%Y%m%d').date()
|
||||||
|
out[d] = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_s, end_s = part.split('-', 1)
|
||||||
|
start_dt = datetime.strptime(start_s, '%Y%m%d:%H%M')
|
||||||
|
end_dt = datetime.strptime(end_s, '%Y%m%d:%H%M')
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(f'invalid segment: {part}') from exc
|
||||||
|
|
||||||
|
if tz is not None:
|
||||||
|
start_dt = start_dt.replace(tzinfo=tz)
|
||||||
|
end_dt = end_dt.replace(tzinfo=tz)
|
||||||
|
|
||||||
|
out[start_dt.date()] = (start_dt, end_dt)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ORIG desired usage,
|
||||||
|
#
|
||||||
|
# TODO, for non-drunk tomorrow,
|
||||||
|
# - call above fn and check that `output[today] is not None`
|
||||||
|
# trading_hrs: dict = parse_trading_hours(
|
||||||
|
# details.tradingHours
|
||||||
|
# )
|
||||||
|
# liq_hrs: dict = parse_trading_hours(
|
||||||
|
# details.liquidHours
|
||||||
|
# )
|
||||||
|
|
@ -62,9 +62,12 @@ from piker.clearing._messages import (
|
||||||
from piker.brokers import (
|
from piker.brokers import (
|
||||||
open_cached_client,
|
open_cached_client,
|
||||||
)
|
)
|
||||||
|
from piker.log import (
|
||||||
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
|
)
|
||||||
from piker.data import open_symcache
|
from piker.data import open_symcache
|
||||||
from .api import (
|
from .api import (
|
||||||
log,
|
|
||||||
Client,
|
Client,
|
||||||
BrokerError,
|
BrokerError,
|
||||||
)
|
)
|
||||||
|
|
@ -78,6 +81,8 @@ from .ledger import (
|
||||||
verify_balances,
|
verify_balances,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
MsgUnion = Union[
|
MsgUnion = Union[
|
||||||
BrokerdCancel,
|
BrokerdCancel,
|
||||||
BrokerdError,
|
BrokerdError,
|
||||||
|
|
@ -431,9 +436,15 @@ def trades2pps(
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def open_trade_dialog(
|
async def open_trade_dialog(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
|
loglevel: str = 'warning',
|
||||||
|
|
||||||
) -> AsyncIterator[dict[str, Any]]:
|
) -> AsyncIterator[dict[str, Any]]:
|
||||||
|
|
||||||
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
# TODO: maybe bind these together and deliver
|
# TODO: maybe bind these together and deliver
|
||||||
# a tuple from `.open_cached_client()`?
|
# a tuple from `.open_cached_client()`?
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,19 @@ from . import open_cached_client
|
||||||
from piker._cacheables import async_lifo_cache
|
from piker._cacheables import async_lifo_cache
|
||||||
from .. import config
|
from .. import config
|
||||||
from ._util import resproc, BrokerError, SymbolNotFound
|
from ._util import resproc, BrokerError, SymbolNotFound
|
||||||
from ..log import (
|
from piker.log import (
|
||||||
colorize_json,
|
colorize_json,
|
||||||
)
|
|
||||||
from ._util import (
|
|
||||||
log,
|
|
||||||
get_console_log,
|
get_console_log,
|
||||||
)
|
)
|
||||||
|
from piker.log import (
|
||||||
|
get_logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_use_practice_account = False
|
_use_practice_account = False
|
||||||
_refresh_token_ep = 'https://{}login.questrade.com/oauth2/'
|
_refresh_token_ep = 'https://{}login.questrade.com/oauth2/'
|
||||||
|
|
@ -1205,7 +1211,10 @@ async def stream_quotes(
|
||||||
# feed_type: str = 'stock',
|
# feed_type: str = 'stock',
|
||||||
) -> AsyncGenerator[str, Dict[str, Any]]:
|
) -> AsyncGenerator[str, 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)
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
async with open_cached_client('questrade') as client:
|
async with open_cached_client('questrade') as client:
|
||||||
if feed_type == 'stock':
|
if feed_type == 'stock':
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,16 @@ import asks
|
||||||
from ._util import (
|
from ._util import (
|
||||||
resproc,
|
resproc,
|
||||||
BrokerError,
|
BrokerError,
|
||||||
log,
|
|
||||||
)
|
)
|
||||||
from ..calc import percent_change
|
from piker.calc import percent_change
|
||||||
|
from piker.log import (
|
||||||
|
get_logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_service_ep = 'https://api.robinhood.com'
|
_service_ep = 'https://api.robinhood.com'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ async def relay_orders_from_sync_code(
|
||||||
async def open_ems(
|
async def open_ems(
|
||||||
fqme: str,
|
fqme: str,
|
||||||
mode: str = 'live',
|
mode: str = 'live',
|
||||||
loglevel: str = 'error',
|
loglevel: str = 'warning',
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
OrderClient, # client
|
OrderClient, # client
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ from tractor import trionics
|
||||||
from ._util import (
|
from ._util import (
|
||||||
log, # sub-sys logger
|
log, # sub-sys logger
|
||||||
get_console_log,
|
get_console_log,
|
||||||
|
subsys,
|
||||||
)
|
)
|
||||||
from ..accounting._mktinfo import (
|
from ..accounting._mktinfo import (
|
||||||
unpack_fqme,
|
unpack_fqme,
|
||||||
|
|
@ -136,7 +137,7 @@ class DarkBook(Struct):
|
||||||
tuple[
|
tuple[
|
||||||
Callable[[float], bool], # predicate
|
Callable[[float], bool], # predicate
|
||||||
tuple[str, ...], # tickfilter
|
tuple[str, ...], # tickfilter
|
||||||
dict | Order, # cmd / msg type
|
dict|Order, # cmd / msg type
|
||||||
|
|
||||||
# live submission constraint parameters
|
# live submission constraint parameters
|
||||||
float, # percent_away max price diff
|
float, # percent_away max price diff
|
||||||
|
|
@ -278,7 +279,7 @@ async def clear_dark_triggers(
|
||||||
|
|
||||||
# remove exec-condition from set
|
# remove exec-condition from set
|
||||||
log.info(f'Removing trigger for {oid}')
|
log.info(f'Removing trigger for {oid}')
|
||||||
trigger: tuple | None = execs.pop(oid, None)
|
trigger: tuple|None = execs.pop(oid, None)
|
||||||
if not trigger:
|
if not trigger:
|
||||||
log.warning(
|
log.warning(
|
||||||
f'trigger for {oid} was already removed!?'
|
f'trigger for {oid} was already removed!?'
|
||||||
|
|
@ -336,8 +337,8 @@ async def open_brokerd_dialog(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
portal: tractor.Portal,
|
portal: tractor.Portal,
|
||||||
exec_mode: str,
|
exec_mode: str,
|
||||||
fqme: str | None = None,
|
fqme: str|None = None,
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
tractor.MsgStream,
|
tractor.MsgStream,
|
||||||
|
|
@ -351,9 +352,21 @@ async def open_brokerd_dialog(
|
||||||
broker backend, configuration, or client code usage.
|
broker backend, configuration, or client code usage.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name='clearing',
|
||||||
|
)
|
||||||
|
# enable `.accounting` console since normally used by
|
||||||
|
# each `brokerd`.
|
||||||
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name='piker.accounting',
|
||||||
|
)
|
||||||
broker: str = brokermod.name
|
broker: str = brokermod.name
|
||||||
|
|
||||||
def mk_paper_ep():
|
def mk_paper_ep(
|
||||||
|
loglevel: str,
|
||||||
|
):
|
||||||
from . import _paper_engine as paper_mod
|
from . import _paper_engine as paper_mod
|
||||||
|
|
||||||
nonlocal brokermod, exec_mode
|
nonlocal brokermod, exec_mode
|
||||||
|
|
@ -405,17 +418,21 @@ async def open_brokerd_dialog(
|
||||||
|
|
||||||
if (
|
if (
|
||||||
trades_endpoint is not None
|
trades_endpoint is not None
|
||||||
or exec_mode != 'paper'
|
or
|
||||||
|
exec_mode != 'paper'
|
||||||
):
|
):
|
||||||
# open live brokerd trades endpoint
|
# open live brokerd trades endpoint
|
||||||
open_trades_endpoint = portal.open_context(
|
open_trades_endpoint = portal.open_context(
|
||||||
trades_endpoint,
|
trades_endpoint,
|
||||||
|
loglevel=loglevel,
|
||||||
)
|
)
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def maybe_open_paper_ep():
|
async def maybe_open_paper_ep():
|
||||||
if exec_mode == 'paper':
|
if exec_mode == 'paper':
|
||||||
async with mk_paper_ep() as msg:
|
async with mk_paper_ep(
|
||||||
|
loglevel=loglevel,
|
||||||
|
) as msg:
|
||||||
yield msg
|
yield msg
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -426,7 +443,9 @@ async def open_brokerd_dialog(
|
||||||
# runtime indication that the backend can't support live
|
# runtime indication that the backend can't support live
|
||||||
# order ctrl yet, so boot the paperboi B0
|
# order ctrl yet, so boot the paperboi B0
|
||||||
if first == 'paper':
|
if first == 'paper':
|
||||||
async with mk_paper_ep() as msg:
|
async with mk_paper_ep(
|
||||||
|
loglevel=loglevel,
|
||||||
|
) as msg:
|
||||||
yield msg
|
yield msg
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
|
@ -761,12 +780,16 @@ _router: Router = None
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def _setup_persistent_emsd(
|
async def _setup_persistent_emsd(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
if loglevel:
|
if loglevel:
|
||||||
get_console_log(loglevel)
|
_log = get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=subsys,
|
||||||
|
)
|
||||||
|
assert _log.name == 'piker.clearing'
|
||||||
|
|
||||||
global _router
|
global _router
|
||||||
|
|
||||||
|
|
@ -822,7 +845,7 @@ async def translate_and_relay_brokerd_events(
|
||||||
f'Rx brokerd trade msg:\n'
|
f'Rx brokerd trade msg:\n'
|
||||||
f'{fmsg}'
|
f'{fmsg}'
|
||||||
)
|
)
|
||||||
status_msg: Status | None = None
|
status_msg: Status|None = None
|
||||||
|
|
||||||
match brokerd_msg:
|
match brokerd_msg:
|
||||||
# BrokerdPosition
|
# BrokerdPosition
|
||||||
|
|
@ -1283,7 +1306,7 @@ async def process_client_order_cmds(
|
||||||
and status.resp == 'dark_open'
|
and status.resp == 'dark_open'
|
||||||
):
|
):
|
||||||
# remove from dark book clearing
|
# remove from dark book clearing
|
||||||
entry: tuple | None = dark_book.triggers[fqme].pop(oid, None)
|
entry: tuple|None = dark_book.triggers[fqme].pop(oid, None)
|
||||||
if entry:
|
if entry:
|
||||||
(
|
(
|
||||||
pred,
|
pred,
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,9 @@ from piker.data import (
|
||||||
open_symcache,
|
open_symcache,
|
||||||
)
|
)
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from ._util import (
|
from piker.log import (
|
||||||
log, # sub-sys logger
|
|
||||||
get_console_log,
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
)
|
)
|
||||||
from ._messages import (
|
from ._messages import (
|
||||||
BrokerdCancel,
|
BrokerdCancel,
|
||||||
|
|
@ -73,6 +73,8 @@ from ._messages import (
|
||||||
BrokerdError,
|
BrokerdError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
class PaperBoi(Struct):
|
class PaperBoi(Struct):
|
||||||
'''
|
'''
|
||||||
|
|
@ -550,16 +552,18 @@ _sells: defaultdict[
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def open_trade_dialog(
|
async def open_trade_dialog(
|
||||||
|
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
broker: str,
|
broker: str,
|
||||||
fqme: str | None = None, # if empty, we only boot broker mode
|
fqme: str|None = None, # if empty, we only boot broker mode
|
||||||
loglevel: str = 'warning',
|
loglevel: str = 'warning',
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# enable piker.clearing console log for *this* subactor
|
# enable piker.clearing console log for *this* `brokerd` subactor
|
||||||
get_console_log(loglevel)
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
symcache: SymbologyCache
|
symcache: SymbologyCache
|
||||||
async with open_symcache(get_brokermod(broker)) as symcache:
|
async with open_symcache(get_brokermod(broker)) as symcache:
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,14 @@ from ..log import (
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
subsys: str = 'piker.clearing'
|
subsys: str = 'piker.clearing'
|
||||||
|
|
||||||
log = get_logger(subsys)
|
log = get_logger(
|
||||||
|
name='piker.clearing',
|
||||||
|
)
|
||||||
|
|
||||||
# TODO, oof doesn't this ignore the `loglevel` then???
|
# TODO, oof doesn't this ignore the `loglevel` then???
|
||||||
get_console_log = partial(
|
get_console_log = partial(
|
||||||
get_console_log,
|
get_console_log,
|
||||||
name=subsys,
|
name='clearing',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@ def load_trans_eps(
|
||||||
|
|
||||||
if (
|
if (
|
||||||
network
|
network
|
||||||
and not maddrs
|
and
|
||||||
|
not maddrs
|
||||||
):
|
):
|
||||||
# load network section and (attempt to) connect all endpoints
|
# load network section and (attempt to) connect all endpoints
|
||||||
# which are reachable B)
|
# which are reachable B)
|
||||||
|
|
@ -112,31 +113,27 @@ def load_trans_eps(
|
||||||
default=None,
|
default=None,
|
||||||
help='Multiaddrs to bind or contact',
|
help='Multiaddrs to bind or contact',
|
||||||
)
|
)
|
||||||
# @click.option(
|
|
||||||
# '--tsdb',
|
|
||||||
# is_flag=True,
|
|
||||||
# help='Enable local ``marketstore`` instance'
|
|
||||||
# )
|
|
||||||
# @click.option(
|
|
||||||
# '--es',
|
|
||||||
# is_flag=True,
|
|
||||||
# help='Enable local ``elasticsearch`` instance'
|
|
||||||
# )
|
|
||||||
def pikerd(
|
def pikerd(
|
||||||
maddr: list[str] | None,
|
maddr: list[str] | None,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
tl: bool,
|
tl: bool,
|
||||||
pdb: bool,
|
pdb: bool,
|
||||||
# tsdb: bool,
|
|
||||||
# es: bool,
|
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Spawn the piker broker-daemon.
|
Start the "root service actor", `pikerd`, run it until
|
||||||
|
cancellation.
|
||||||
|
|
||||||
|
This "root daemon" operates as the top most service-mngr and
|
||||||
|
subsys-as-subactor supervisor, think of it as the "init proc" of
|
||||||
|
any of any `piker` application or daemon-process tree.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# from tractor.devx import maybe_open_crash_handler
|
# from tractor.devx import maybe_open_crash_handler
|
||||||
# with maybe_open_crash_handler(pdb=False):
|
# with maybe_open_crash_handler(pdb=False):
|
||||||
log = get_console_log(loglevel, name='cli')
|
log = get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
with_tractor_log=tl,
|
||||||
|
)
|
||||||
|
|
||||||
if pdb:
|
if pdb:
|
||||||
log.warning((
|
log.warning((
|
||||||
|
|
@ -237,6 +234,14 @@ def cli(
|
||||||
regaddr: str,
|
regaddr: str,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
'''
|
||||||
|
The "root" `piker`-cmd CLI endpoint.
|
||||||
|
|
||||||
|
NOTE, this def generally relies on and requires a sub-cmd to be
|
||||||
|
provided by the user, OW only a `--help` msg (listing said
|
||||||
|
subcmds) will be dumped to console.
|
||||||
|
|
||||||
|
'''
|
||||||
if configdir is not None:
|
if configdir is not None:
|
||||||
assert os.path.isdir(configdir), f"`{configdir}` is not a valid path"
|
assert os.path.isdir(configdir), f"`{configdir}` is not a valid path"
|
||||||
config._override_config_dir(configdir)
|
config._override_config_dir(configdir)
|
||||||
|
|
@ -295,17 +300,50 @@ def cli(
|
||||||
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
||||||
@click.argument('ports', nargs=-1, required=False)
|
@click.argument('ports', nargs=-1, required=False)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def services(config, tl, ports):
|
def services(
|
||||||
|
config,
|
||||||
|
tl: bool,
|
||||||
|
ports: list[int],
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
List all `piker` "service deamons" to the console in
|
||||||
|
a `json`-table which maps each actor's UID in the form,
|
||||||
|
|
||||||
from ..service import (
|
`{service_name}.{subservice_name}.{UUID}`
|
||||||
|
|
||||||
|
to its (primary) IPC server address.
|
||||||
|
|
||||||
|
(^TODO, should be its multiaddr form once we support it)
|
||||||
|
|
||||||
|
Note that by convention actors which operate as "headless"
|
||||||
|
processes (those without GUIs/graphics, and which generally
|
||||||
|
parent some noteworthy subsystem) are normally suffixed by
|
||||||
|
a "d" such as,
|
||||||
|
|
||||||
|
- pikerd: the root runtime supervisor
|
||||||
|
- brokerd: a broker-backend order ctl daemon
|
||||||
|
- emsd: the internal dark-clearing and order routing daemon
|
||||||
|
- datad: a data-provider-backend data feed daemon
|
||||||
|
- samplerd: the real-time data sampling and clock-syncing daemon
|
||||||
|
|
||||||
|
"Headed units" are normally just given an obvious app-like name
|
||||||
|
with subactors indexed by `.` such as,
|
||||||
|
- chart: the primary modal charting iface, a Qt app
|
||||||
|
- chart.fsp_0: a financial-sig-proc cascade instance which
|
||||||
|
delivers graphics to a parent `chart` app.
|
||||||
|
- polars_boi: some (presumably) `polars` using console app.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from piker.service import (
|
||||||
open_piker_runtime,
|
open_piker_runtime,
|
||||||
_default_registry_port,
|
_default_registry_port,
|
||||||
_default_registry_host,
|
_default_registry_host,
|
||||||
)
|
)
|
||||||
|
|
||||||
host = _default_registry_host
|
# !TODO, mk this to work with UDS!
|
||||||
|
host: str = _default_registry_host
|
||||||
if not ports:
|
if not ports:
|
||||||
ports = [_default_registry_port]
|
ports: list[int] = [_default_registry_port]
|
||||||
|
|
||||||
addr = tractor._addr.wrap_address(
|
addr = tractor._addr.wrap_address(
|
||||||
addr=(host, ports[0])
|
addr=(host, ports[0])
|
||||||
|
|
@ -316,7 +354,11 @@ def services(config, tl, ports):
|
||||||
async with (
|
async with (
|
||||||
open_piker_runtime(
|
open_piker_runtime(
|
||||||
name='service_query',
|
name='service_query',
|
||||||
loglevel=config['loglevel'] if tl else None,
|
loglevel=(
|
||||||
|
config['loglevel']
|
||||||
|
if tl
|
||||||
|
else None
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tractor.get_registry(
|
tractor.get_registry(
|
||||||
addr=addr,
|
addr=addr,
|
||||||
|
|
@ -336,7 +378,15 @@ def services(config, tl, ports):
|
||||||
|
|
||||||
|
|
||||||
def _load_clis() -> None:
|
def _load_clis() -> None:
|
||||||
# from ..service import elastic # noqa
|
'''
|
||||||
|
Dynamically load and register all subsys CLI endpoints (at call
|
||||||
|
time).
|
||||||
|
|
||||||
|
NOTE, obviously this is normally expected to be called at
|
||||||
|
`import` time and implicitly relies on our use of various
|
||||||
|
`click`/`typer` decorator APIs.
|
||||||
|
|
||||||
|
'''
|
||||||
from ..brokers import cli # noqa
|
from ..brokers import cli # noqa
|
||||||
from ..ui import cli # noqa
|
from ..ui import cli # noqa
|
||||||
from ..watchlists import cli # noqa
|
from ..watchlists import cli # noqa
|
||||||
|
|
@ -346,5 +396,5 @@ def _load_clis() -> None:
|
||||||
from ..accounting import cli # noqa
|
from ..accounting import cli # noqa
|
||||||
|
|
||||||
|
|
||||||
# load downstream cli modules
|
# load all subsytem cli eps
|
||||||
_load_clis()
|
_load_clis()
|
||||||
|
|
|
||||||
|
|
@ -336,10 +336,18 @@ async def register_with_sampler(
|
||||||
|
|
||||||
open_index_stream: bool = True, # open a 2way stream for sample step msgs?
|
open_index_stream: bool = True, # open a 2way stream for sample step msgs?
|
||||||
sub_for_broadcasts: bool = True, # sampler side to send step updates?
|
sub_for_broadcasts: bool = True, # sampler side to send step updates?
|
||||||
|
loglevel: str|None = None,
|
||||||
|
|
||||||
) -> set[int]:
|
) -> set[int]:
|
||||||
|
|
||||||
get_console_log(tractor.current_actor().loglevel)
|
get_console_log(
|
||||||
|
level=(
|
||||||
|
loglevel
|
||||||
|
or
|
||||||
|
tractor.current_actor().loglevel
|
||||||
|
),
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
incr_was_started: bool = False
|
incr_was_started: bool = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -476,6 +484,7 @@ async def spawn_samplerd(
|
||||||
register_with_sampler,
|
register_with_sampler,
|
||||||
period_s=1,
|
period_s=1,
|
||||||
sub_for_broadcasts=False,
|
sub_for_broadcasts=False,
|
||||||
|
loglevel=loglevel,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -484,7 +493,6 @@ async def spawn_samplerd(
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def maybe_open_samplerd(
|
async def maybe_open_samplerd(
|
||||||
|
|
||||||
loglevel: str|None = None,
|
loglevel: str|None = None,
|
||||||
**pikerd_kwargs,
|
**pikerd_kwargs,
|
||||||
|
|
||||||
|
|
@ -513,10 +521,10 @@ async def open_sample_stream(
|
||||||
shms_by_period: dict[float, dict]|None = None,
|
shms_by_period: dict[float, dict]|None = None,
|
||||||
open_index_stream: bool = True,
|
open_index_stream: bool = True,
|
||||||
sub_for_broadcasts: bool = True,
|
sub_for_broadcasts: bool = True,
|
||||||
|
loglevel: str|None = None,
|
||||||
|
|
||||||
cache_key: str|None = None,
|
# cache_key: str|None = None,
|
||||||
allow_new_sampler: bool = True,
|
# allow_new_sampler: bool = True,
|
||||||
|
|
||||||
ensure_is_active: bool = False,
|
ensure_is_active: bool = False,
|
||||||
|
|
||||||
) -> AsyncIterator[dict[str, float]]:
|
) -> AsyncIterator[dict[str, float]]:
|
||||||
|
|
@ -551,7 +559,9 @@ async def open_sample_stream(
|
||||||
# XXX: this should be singleton on a host,
|
# XXX: this should be singleton on a host,
|
||||||
# a lone broker-daemon per provider should be
|
# a lone broker-daemon per provider should be
|
||||||
# created for all practical purposes
|
# created for all practical purposes
|
||||||
maybe_open_samplerd() as portal,
|
maybe_open_samplerd(
|
||||||
|
loglevel=loglevel,
|
||||||
|
) as portal,
|
||||||
|
|
||||||
portal.open_context(
|
portal.open_context(
|
||||||
register_with_sampler,
|
register_with_sampler,
|
||||||
|
|
@ -560,6 +570,7 @@ async def open_sample_stream(
|
||||||
'shms_by_period': shms_by_period,
|
'shms_by_period': shms_by_period,
|
||||||
'open_index_stream': open_index_stream,
|
'open_index_stream': open_index_stream,
|
||||||
'sub_for_broadcasts': sub_for_broadcasts,
|
'sub_for_broadcasts': sub_for_broadcasts,
|
||||||
|
'loglevel': loglevel,
|
||||||
},
|
},
|
||||||
) as (ctx, shm_periods)
|
) as (ctx, shm_periods)
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ from ..log import (
|
||||||
)
|
)
|
||||||
subsys: str = 'piker.data'
|
subsys: str = 'piker.data'
|
||||||
|
|
||||||
log = get_logger(subsys)
|
log = get_logger(
|
||||||
|
name=subsys,
|
||||||
|
)
|
||||||
|
|
||||||
get_console_log = partial(
|
get_console_log = partial(
|
||||||
get_console_log,
|
get_console_log,
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ from ._util import (
|
||||||
log,
|
log,
|
||||||
get_console_log,
|
get_console_log,
|
||||||
)
|
)
|
||||||
from .flows import Flume
|
|
||||||
from .validate import (
|
from .validate import (
|
||||||
FeedInit,
|
FeedInit,
|
||||||
validate_backend,
|
validate_backend,
|
||||||
|
|
@ -77,6 +76,7 @@ from ._sampling import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .flows import Flume
|
||||||
from tractor._addr import Address
|
from tractor._addr import Address
|
||||||
from tractor.msg.types import Aid
|
from tractor.msg.types import Aid
|
||||||
|
|
||||||
|
|
@ -239,7 +239,6 @@ async def allocate_persistent_feed(
|
||||||
|
|
||||||
brokername: str,
|
brokername: str,
|
||||||
symstr: str,
|
symstr: str,
|
||||||
|
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
start_stream: bool = True,
|
start_stream: bool = True,
|
||||||
init_timeout: float = 616,
|
init_timeout: float = 616,
|
||||||
|
|
@ -278,7 +277,7 @@ async def allocate_persistent_feed(
|
||||||
# ``stream_quotes()``, a required broker backend endpoint.
|
# ``stream_quotes()``, a required broker backend endpoint.
|
||||||
init_msgs: (
|
init_msgs: (
|
||||||
list[FeedInit] # new
|
list[FeedInit] # new
|
||||||
| dict[str, dict[str, str]] # legacy / deprecated
|
|dict[str, dict[str, str]] # legacy / deprecated
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: probably make a struct msg type for this as well
|
# TODO: probably make a struct msg type for this as well
|
||||||
|
|
@ -348,11 +347,14 @@ async def allocate_persistent_feed(
|
||||||
izero_rt,
|
izero_rt,
|
||||||
rt_shm,
|
rt_shm,
|
||||||
) = await bus.nursery.start(
|
) = await bus.nursery.start(
|
||||||
manage_history,
|
partial(
|
||||||
mod,
|
manage_history,
|
||||||
mkt,
|
mod=mod,
|
||||||
some_data_ready,
|
mkt=mkt,
|
||||||
feed_is_live,
|
some_data_ready=some_data_ready,
|
||||||
|
feed_is_live=feed_is_live,
|
||||||
|
loglevel=loglevel,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# yield back control to starting nursery once we receive either
|
# yield back control to starting nursery once we receive either
|
||||||
|
|
@ -362,6 +364,8 @@ async def allocate_persistent_feed(
|
||||||
)
|
)
|
||||||
await some_data_ready.wait()
|
await some_data_ready.wait()
|
||||||
|
|
||||||
|
# XXX, avoid cycle; it imports this mod.
|
||||||
|
from .flows import Flume
|
||||||
flume = Flume(
|
flume = Flume(
|
||||||
|
|
||||||
# TODO: we have to use this for now since currently the
|
# TODO: we have to use this for now since currently the
|
||||||
|
|
@ -458,7 +462,6 @@ async def allocate_persistent_feed(
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def open_feed_bus(
|
async def open_feed_bus(
|
||||||
|
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
brokername: str,
|
brokername: str,
|
||||||
symbols: list[str], # normally expected to the broker-specific fqme
|
symbols: list[str], # normally expected to the broker-specific fqme
|
||||||
|
|
@ -479,13 +482,16 @@ async def open_feed_bus(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if loglevel is None:
|
if loglevel is None:
|
||||||
loglevel = tractor.current_actor().loglevel
|
loglevel: str = tractor.current_actor().loglevel
|
||||||
|
|
||||||
# XXX: required to propagate ``tractor`` loglevel to piker
|
# XXX: required to propagate ``tractor`` loglevel to piker
|
||||||
# logging
|
# logging
|
||||||
get_console_log(
|
get_console_log(
|
||||||
loglevel
|
level=(loglevel
|
||||||
or tractor.current_actor().loglevel
|
or
|
||||||
|
tractor.current_actor().loglevel
|
||||||
|
),
|
||||||
|
name=__name__,
|
||||||
)
|
)
|
||||||
|
|
||||||
# local state sanity checks
|
# local state sanity checks
|
||||||
|
|
@ -500,7 +506,6 @@ async def open_feed_bus(
|
||||||
sub_registered = trio.Event()
|
sub_registered = trio.Event()
|
||||||
|
|
||||||
flumes: dict[str, Flume] = {}
|
flumes: dict[str, Flume] = {}
|
||||||
|
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
|
|
||||||
# if no cached feed for this symbol has been created for this
|
# if no cached feed for this symbol has been created for this
|
||||||
|
|
@ -684,6 +689,7 @@ class Feed(Struct):
|
||||||
'''
|
'''
|
||||||
mods: dict[str, ModuleType] = {}
|
mods: dict[str, ModuleType] = {}
|
||||||
portals: dict[ModuleType, tractor.Portal] = {}
|
portals: dict[ModuleType, tractor.Portal] = {}
|
||||||
|
|
||||||
flumes: dict[
|
flumes: dict[
|
||||||
str, # FQME
|
str, # FQME
|
||||||
Flume,
|
Flume,
|
||||||
|
|
@ -797,7 +803,7 @@ 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,
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
|
|
@ -813,6 +819,11 @@ async def maybe_open_feed(
|
||||||
'''
|
'''
|
||||||
fqme = fqmes[0]
|
fqme = fqmes[0]
|
||||||
|
|
||||||
|
# if (
|
||||||
|
# loglevel != 'info'
|
||||||
|
# ):
|
||||||
|
# await tractor.pause()
|
||||||
|
|
||||||
async with trionics.maybe_open_context(
|
async with trionics.maybe_open_context(
|
||||||
acm_func=open_feed,
|
acm_func=open_feed,
|
||||||
kwargs={
|
kwargs={
|
||||||
|
|
@ -879,9 +890,12 @@ async def open_feed(
|
||||||
providers.setdefault(mod, []).append(bs_fqme)
|
providers.setdefault(mod, []).append(bs_fqme)
|
||||||
feed.mods[mod.name] = mod
|
feed.mods[mod.name] = mod
|
||||||
|
|
||||||
|
if (
|
||||||
|
loglevel != 'info'
|
||||||
|
):
|
||||||
|
await tractor.pause()
|
||||||
# one actor per brokerd for now
|
# one actor per brokerd for now
|
||||||
brokerd_ctxs = []
|
brokerd_ctxs = []
|
||||||
|
|
||||||
for brokermod, bfqmes in providers.items():
|
for brokermod, bfqmes in providers.items():
|
||||||
|
|
||||||
# if no `brokerd` for this backend exists yet we spawn
|
# if no `brokerd` for this backend exists yet we spawn
|
||||||
|
|
@ -951,6 +965,8 @@ async def open_feed(
|
||||||
|
|
||||||
assert len(feed.mods) == len(feed.portals)
|
assert len(feed.mods) == len(feed.portals)
|
||||||
|
|
||||||
|
# XXX, avoid cycle; it imports this mod.
|
||||||
|
from .flows import Flume
|
||||||
async with (
|
async with (
|
||||||
trionics.gather_contexts(bus_ctxs) as ctxs,
|
trionics.gather_contexts(bus_ctxs) as ctxs,
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from functools import partial
|
||||||
from typing import (
|
from typing import (
|
||||||
AsyncIterator,
|
AsyncIterator,
|
||||||
Callable,
|
Callable,
|
||||||
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -33,12 +34,12 @@ import tractor
|
||||||
from tractor.msg import NamespacePath
|
from tractor.msg import NamespacePath
|
||||||
|
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from ..log import get_logger, get_console_log
|
from ..log import (
|
||||||
from .. import data
|
get_logger,
|
||||||
from ..data.feed import (
|
get_console_log,
|
||||||
Flume,
|
|
||||||
Feed,
|
|
||||||
)
|
)
|
||||||
|
from .. import data
|
||||||
|
from ..data.flows import Flume
|
||||||
from ..data._sharedmem import ShmArray
|
from ..data._sharedmem import ShmArray
|
||||||
from ..data._sampling import (
|
from ..data._sampling import (
|
||||||
_default_delay_s,
|
_default_delay_s,
|
||||||
|
|
@ -52,6 +53,9 @@ from ._api import (
|
||||||
)
|
)
|
||||||
from ..toolz import Profiler
|
from ..toolz import Profiler
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..data.feed import Feed
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -169,8 +173,10 @@ class Cascade(Struct):
|
||||||
if not synced:
|
if not synced:
|
||||||
fsp: Fsp = self.fsp
|
fsp: Fsp = self.fsp
|
||||||
log.warning(
|
log.warning(
|
||||||
'***DESYNCED FSP***\n'
|
f'***DESYNCED fsp***\n'
|
||||||
f'{fsp.ns_path}@{src_shm.token}\n'
|
f'------------------\n'
|
||||||
|
f'ns-path: {fsp.ns_path!r}\n'
|
||||||
|
f'shm-token: {src_shm.token}\n'
|
||||||
f'step_diff: {step_diff}\n'
|
f'step_diff: {step_diff}\n'
|
||||||
f'len_diff: {len_diff}\n'
|
f'len_diff: {len_diff}\n'
|
||||||
)
|
)
|
||||||
|
|
@ -398,7 +404,6 @@ async def connect_streams(
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def cascade(
|
async def cascade(
|
||||||
|
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
|
|
||||||
# data feed key
|
# data feed key
|
||||||
|
|
@ -412,7 +417,7 @@ async def cascade(
|
||||||
shm_registry: dict[str, _Token],
|
shm_registry: dict[str, _Token],
|
||||||
|
|
||||||
zero_on_step: bool = False,
|
zero_on_step: bool = False,
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -426,7 +431,17 @@ async def cascade(
|
||||||
)
|
)
|
||||||
|
|
||||||
if loglevel:
|
if loglevel:
|
||||||
get_console_log(loglevel)
|
log = get_console_log(
|
||||||
|
loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
# XXX TODO!
|
||||||
|
# figure out why this writes a dict to,
|
||||||
|
# `tractor._state._runtime_vars['_root_mailbox']`
|
||||||
|
# XD .. wtf
|
||||||
|
# TODO, solve this as reported in,
|
||||||
|
# https://www.pikers.dev/pikers/piker/issues/70
|
||||||
|
# await tractor.pause()
|
||||||
|
|
||||||
src: Flume = Flume.from_msg(src_flume_addr)
|
src: Flume = Flume.from_msg(src_flume_addr)
|
||||||
dst: Flume = Flume.from_msg(
|
dst: Flume = Flume.from_msg(
|
||||||
|
|
@ -469,7 +484,8 @@ async def cascade(
|
||||||
# open a data feed stream with requested broker
|
# open a data feed stream with requested broker
|
||||||
feed: Feed
|
feed: Feed
|
||||||
async with data.feed.maybe_open_feed(
|
async with data.feed.maybe_open_feed(
|
||||||
[fqme],
|
fqmes=[fqme],
|
||||||
|
loglevel=loglevel,
|
||||||
|
|
||||||
# TODO throttle tick outputs from *this* daemon since
|
# TODO throttle tick outputs from *this* daemon since
|
||||||
# it'll emit tons of ticks due to the throttle only
|
# it'll emit tons of ticks due to the throttle only
|
||||||
|
|
@ -567,7 +583,8 @@ async def cascade(
|
||||||
# on every step msg received from the global `samplerd`
|
# on every step msg received from the global `samplerd`
|
||||||
# service.
|
# service.
|
||||||
async with open_sample_stream(
|
async with open_sample_stream(
|
||||||
float(delay_s)
|
period_s=float(delay_s),
|
||||||
|
loglevel=loglevel,
|
||||||
) as istream:
|
) as istream:
|
||||||
|
|
||||||
profiler(f'{func_name}: sample stream up')
|
profiler(f'{func_name}: sample stream up')
|
||||||
|
|
|
||||||
71
piker/log.py
71
piker/log.py
|
|
@ -37,35 +37,84 @@ _proj_name: str = 'piker'
|
||||||
|
|
||||||
|
|
||||||
def get_logger(
|
def get_logger(
|
||||||
name: str = None,
|
name: str|None = None,
|
||||||
|
**tractor_log_kwargs,
|
||||||
) -> logging.Logger:
|
) -> logging.Logger:
|
||||||
'''
|
'''
|
||||||
Return the package log or a sub-log for `name` if provided.
|
Return the package log or a sub-logger if a `name=` is provided,
|
||||||
|
which defaults to the calling module's pkg-namespace path.
|
||||||
|
|
||||||
|
See `tractor.log.get_logger()` for details.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
pkg_name: str = _proj_name
|
||||||
|
if (
|
||||||
|
name
|
||||||
|
and
|
||||||
|
pkg_name in name
|
||||||
|
):
|
||||||
|
name: str = name.lstrip(f'{_proj_name}.')
|
||||||
|
|
||||||
return tractor.log.get_logger(
|
return tractor.log.get_logger(
|
||||||
name=name,
|
name=name,
|
||||||
_root_name=_proj_name,
|
pkg_name=pkg_name,
|
||||||
|
**tractor_log_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_console_log(
|
def get_console_log(
|
||||||
level: str | None = None,
|
level: str|None = None,
|
||||||
name: str | None = None,
|
name: str|None = None,
|
||||||
|
pkg_name: str|None = None,
|
||||||
|
with_tractor_log: bool = False,
|
||||||
|
# ?TODO, support a "log-spec" style `str|dict[str, str]` which
|
||||||
|
# dictates both the sublogger-key and a level?
|
||||||
|
# -> see similar idea in `modden`'s usage.
|
||||||
|
**tractor_log_kwargs,
|
||||||
|
|
||||||
) -> logging.Logger:
|
) -> logging.Logger:
|
||||||
'''
|
'''
|
||||||
Get the package logger and enable a handler which writes to stderr.
|
Get the package logger and enable a handler which writes to
|
||||||
|
stderr.
|
||||||
|
|
||||||
Yeah yeah, i know we can use ``DictConfig``. You do it...
|
Yeah yeah, i know we can use `DictConfig`.
|
||||||
|
You do it.. Bp
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
pkg_name: str = _proj_name
|
||||||
|
if (
|
||||||
|
name
|
||||||
|
and
|
||||||
|
pkg_name in name
|
||||||
|
):
|
||||||
|
name: str = name.lstrip(f'{_proj_name}.')
|
||||||
|
|
||||||
|
tll: str|None = None
|
||||||
|
if (
|
||||||
|
with_tractor_log is not False
|
||||||
|
):
|
||||||
|
tll = level
|
||||||
|
|
||||||
|
elif maybe_actor := tractor.current_actor(
|
||||||
|
err_on_no_runtime=False,
|
||||||
|
):
|
||||||
|
tll = maybe_actor.loglevel
|
||||||
|
|
||||||
|
if tll:
|
||||||
|
t_log = tractor.log.get_console_log(
|
||||||
|
level=tll,
|
||||||
|
name='tractor', # <- XXX, force root tractor log!
|
||||||
|
**tractor_log_kwargs,
|
||||||
|
)
|
||||||
|
# TODO/ allow only enabling certain tractor sub-logs?
|
||||||
|
assert t_log.name == 'tractor'
|
||||||
|
|
||||||
return tractor.log.get_console_log(
|
return tractor.log.get_console_log(
|
||||||
level,
|
level=level,
|
||||||
name=name,
|
name=name,
|
||||||
_root_name=_proj_name,
|
pkg_name=pkg_name,
|
||||||
) # our root logger
|
**tractor_log_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def colorize_json(
|
def colorize_json(
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional,
|
|
||||||
Any,
|
Any,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
)
|
)
|
||||||
|
|
@ -32,9 +31,12 @@ from contextlib import (
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from ._util import (
|
from piker.log import (
|
||||||
get_console_log,
|
get_console_log,
|
||||||
)
|
)
|
||||||
|
from ._util import (
|
||||||
|
subsys,
|
||||||
|
)
|
||||||
from ._mngr import (
|
from ._mngr import (
|
||||||
Services,
|
Services,
|
||||||
)
|
)
|
||||||
|
|
@ -59,7 +61,7 @@ async def open_piker_runtime(
|
||||||
registry_addrs: list[tuple[str, int]] = [],
|
registry_addrs: list[tuple[str, int]] = [],
|
||||||
|
|
||||||
enable_modules: list[str] = [],
|
enable_modules: list[str] = [],
|
||||||
loglevel: Optional[str] = None,
|
loglevel: str|None = None,
|
||||||
|
|
||||||
# XXX NOTE XXX: you should pretty much never want debug mode
|
# XXX NOTE XXX: you should pretty much never want debug mode
|
||||||
# for data daemons when running in production.
|
# for data daemons when running in production.
|
||||||
|
|
@ -69,7 +71,7 @@ async def open_piker_runtime(
|
||||||
# and spawn the service tree distributed per that.
|
# and spawn the service tree distributed per that.
|
||||||
start_method: str = 'trio',
|
start_method: str = 'trio',
|
||||||
|
|
||||||
tractor_runtime_overrides: dict | None = None,
|
tractor_runtime_overrides: dict|None = None,
|
||||||
**tractor_kwargs,
|
**tractor_kwargs,
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
|
|
@ -97,7 +99,8 @@ async def open_piker_runtime(
|
||||||
# setting it as the root actor on localhost.
|
# setting it as the root actor on localhost.
|
||||||
registry_addrs = (
|
registry_addrs = (
|
||||||
registry_addrs
|
registry_addrs
|
||||||
or [_default_reg_addr]
|
or
|
||||||
|
[_default_reg_addr]
|
||||||
)
|
)
|
||||||
|
|
||||||
if ems := tractor_kwargs.pop('enable_modules', None):
|
if ems := tractor_kwargs.pop('enable_modules', None):
|
||||||
|
|
@ -163,8 +166,7 @@ _root_modules: list[str] = [
|
||||||
@acm
|
@acm
|
||||||
async def open_pikerd(
|
async def open_pikerd(
|
||||||
registry_addrs: list[tuple[str, int]],
|
registry_addrs: list[tuple[str, int]],
|
||||||
|
loglevel: str|None = None,
|
||||||
loglevel: str | None = None,
|
|
||||||
|
|
||||||
# XXX: you should pretty much never want debug mode
|
# XXX: you should pretty much never want debug mode
|
||||||
# for data daemons when running in production.
|
# for data daemons when running in production.
|
||||||
|
|
@ -192,7 +194,6 @@ async def open_pikerd(
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
open_piker_runtime(
|
open_piker_runtime(
|
||||||
|
|
||||||
name=_root_dname,
|
name=_root_dname,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
debug_mode=debug_mode,
|
debug_mode=debug_mode,
|
||||||
|
|
@ -273,7 +274,10 @@ async def maybe_open_pikerd(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if loglevel:
|
if loglevel:
|
||||||
get_console_log(loglevel)
|
get_console_log(
|
||||||
|
name=subsys,
|
||||||
|
level=loglevel
|
||||||
|
)
|
||||||
|
|
||||||
# subtle, we must have the runtime up here or portal lookup will fail
|
# subtle, we must have the runtime up here or portal lookup will fail
|
||||||
query_name = kwargs.pop(
|
query_name = kwargs.pop(
|
||||||
|
|
|
||||||
|
|
@ -49,13 +49,15 @@ from requests.exceptions import (
|
||||||
ReadTimeout,
|
ReadTimeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ._mngr import Services
|
from piker.log import (
|
||||||
from ._util import (
|
|
||||||
log, # sub-sys logger
|
|
||||||
get_console_log,
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
)
|
)
|
||||||
|
from ._mngr import Services
|
||||||
from .. import config
|
from .. import config
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
class DockerNotStarted(Exception):
|
class DockerNotStarted(Exception):
|
||||||
'Prolly you dint start da daemon bruh'
|
'Prolly you dint start da daemon bruh'
|
||||||
|
|
@ -336,13 +338,16 @@ class Container:
|
||||||
async def open_ahabd(
|
async def open_ahabd(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
endpoint: str, # ns-pointer str-msg-type
|
endpoint: str, # ns-pointer str-msg-type
|
||||||
loglevel: str | None = None,
|
loglevel: str = 'cancel',
|
||||||
|
|
||||||
**ep_kwargs,
|
**ep_kwargs,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
log = get_console_log(loglevel or 'cancel')
|
log = get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name='piker.service',
|
||||||
|
)
|
||||||
|
|
||||||
async with open_docker() as client:
|
async with open_docker() as client:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,9 @@ from contextlib import (
|
||||||
import tractor
|
import tractor
|
||||||
from trio.lowlevel import current_task
|
from trio.lowlevel import current_task
|
||||||
|
|
||||||
from ._util import (
|
from piker.log import (
|
||||||
log, # sub-sys logger
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
)
|
)
|
||||||
from ._mngr import (
|
from ._mngr import (
|
||||||
Services,
|
Services,
|
||||||
|
|
@ -39,16 +40,17 @@ from ._mngr import (
|
||||||
from ._actor_runtime import maybe_open_pikerd
|
from ._actor_runtime import maybe_open_pikerd
|
||||||
from ._registry import find_service
|
from ._registry import find_service
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def maybe_spawn_daemon(
|
async def maybe_spawn_daemon(
|
||||||
|
|
||||||
service_name: str,
|
service_name: str,
|
||||||
service_task_target: Callable,
|
service_task_target: Callable,
|
||||||
|
|
||||||
spawn_args: dict[str, Any],
|
spawn_args: dict[str, Any],
|
||||||
|
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
singleton: bool = False,
|
singleton: bool = False,
|
||||||
|
|
||||||
**pikerd_kwargs,
|
**pikerd_kwargs,
|
||||||
|
|
@ -66,6 +68,12 @@ async def maybe_spawn_daemon(
|
||||||
clients.
|
clients.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
log = get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
assert log.name == 'piker.service'
|
||||||
|
|
||||||
# serialize access to this section to avoid
|
# serialize access to this section to avoid
|
||||||
# 2 or more tasks racing to create a daemon
|
# 2 or more tasks racing to create a daemon
|
||||||
lock = Services.locks[service_name]
|
lock = Services.locks[service_name]
|
||||||
|
|
@ -152,8 +160,7 @@ async def maybe_spawn_daemon(
|
||||||
|
|
||||||
|
|
||||||
async def spawn_emsd(
|
async def spawn_emsd(
|
||||||
|
loglevel: str|None = None,
|
||||||
loglevel: str | None = None,
|
|
||||||
**extra_tractor_kwargs
|
**extra_tractor_kwargs
|
||||||
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
@ -190,9 +197,8 @@ async def spawn_emsd(
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def maybe_open_emsd(
|
async def maybe_open_emsd(
|
||||||
|
|
||||||
brokername: str,
|
brokername: str,
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
|
|
||||||
**pikerd_kwargs,
|
**pikerd_kwargs,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ from tractor import (
|
||||||
Portal,
|
Portal,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ._util import (
|
from piker.log import get_logger
|
||||||
log, # sub-sys logger
|
|
||||||
)
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO: we need remote wrapping and a general soln:
|
# TODO: we need remote wrapping and a general soln:
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,29 @@ from typing import (
|
||||||
)
|
)
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
from tractor import Portal
|
from tractor import (
|
||||||
|
msg,
|
||||||
from ._util import (
|
Actor,
|
||||||
log, # sub-sys logger
|
Portal,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from piker.log import get_logger
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
# TODO? default path-space for UDS registry?
|
||||||
|
# [ ] needs to be Xplatform tho!
|
||||||
|
# _default_registry_path: Path = (
|
||||||
|
# Path(os.environ['XDG_RUNTIME_DIR'])
|
||||||
|
# /'piker'
|
||||||
|
# )
|
||||||
|
|
||||||
_default_registry_host: str = '127.0.0.1'
|
_default_registry_host: str = '127.0.0.1'
|
||||||
_default_registry_port: int = 6116
|
_default_registry_port: int = 6116
|
||||||
_default_reg_addr: tuple[str, int] = (
|
_default_reg_addr: tuple[
|
||||||
|
str,
|
||||||
|
int, # |str TODO, once we support UDS, see above.
|
||||||
|
] = (
|
||||||
_default_registry_host,
|
_default_registry_host,
|
||||||
_default_registry_port,
|
_default_registry_port,
|
||||||
)
|
)
|
||||||
|
|
@ -75,16 +89,22 @@ async def open_registry(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
global _tractor_kwargs
|
global _tractor_kwargs
|
||||||
actor = tractor.current_actor()
|
actor: Actor = tractor.current_actor()
|
||||||
uid = actor.uid
|
aid: msg.Aid = actor.aid
|
||||||
preset_reg_addrs: list[tuple[str, int]] = Registry.addrs
|
uid: tuple[str, str] = aid.uid
|
||||||
|
preset_reg_addrs: list[
|
||||||
|
tuple[str, int]
|
||||||
|
] = Registry.addrs
|
||||||
if (
|
if (
|
||||||
preset_reg_addrs
|
preset_reg_addrs
|
||||||
and addrs
|
and
|
||||||
|
addrs
|
||||||
):
|
):
|
||||||
if preset_reg_addrs != addrs:
|
if preset_reg_addrs != addrs:
|
||||||
# if any(addr in preset_reg_addrs for addr in addrs):
|
# if any(addr in preset_reg_addrs for addr in addrs):
|
||||||
diff: set[tuple[str, int]] = set(preset_reg_addrs) - set(addrs)
|
diff: set[
|
||||||
|
tuple[str, int]
|
||||||
|
] = set(preset_reg_addrs) - set(addrs)
|
||||||
if diff:
|
if diff:
|
||||||
log.warning(
|
log.warning(
|
||||||
f'`{uid}` requested only subset of registrars: {addrs}\n'
|
f'`{uid}` requested only subset of registrars: {addrs}\n'
|
||||||
|
|
@ -98,7 +118,6 @@ async def open_registry(
|
||||||
)
|
)
|
||||||
|
|
||||||
was_set: bool = False
|
was_set: bool = False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not tractor.is_root_process()
|
not tractor.is_root_process()
|
||||||
and
|
and
|
||||||
|
|
@ -115,16 +134,23 @@ async def open_registry(
|
||||||
f"`{uid}` registry should already exist but doesn't?"
|
f"`{uid}` registry should already exist but doesn't?"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if not Registry.addrs:
|
||||||
not Registry.addrs
|
|
||||||
):
|
|
||||||
was_set = True
|
was_set = True
|
||||||
Registry.addrs = addrs or [_default_reg_addr]
|
Registry.addrs = (
|
||||||
|
addrs
|
||||||
|
or
|
||||||
|
[_default_reg_addr]
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: only spot this seems currently used is inside
|
# NOTE: only spot this seems currently used is inside
|
||||||
# `.ui._exec` which is the (eventual qtloops) bootstrapping
|
# `.ui._exec` which is the (eventual qtloops) bootstrapping
|
||||||
# with guest mode.
|
# with guest mode.
|
||||||
_tractor_kwargs['registry_addrs'] = Registry.addrs
|
reg_addrs: list[tuple[str, str|int]] = Registry.addrs
|
||||||
|
# !TODO, a struct-API to stringently allow this only in special
|
||||||
|
# cases?
|
||||||
|
# -> better would be to have some way to (atomically) rewrite
|
||||||
|
# and entire `RuntimeVars`?? ideas welcome obvi..
|
||||||
|
_tractor_kwargs['registry_addrs'] = reg_addrs
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield Registry.addrs
|
yield Registry.addrs
|
||||||
|
|
@ -149,7 +175,7 @@ async def find_service(
|
||||||
| None
|
| None
|
||||||
):
|
):
|
||||||
# try:
|
# try:
|
||||||
reg_addrs: list[tuple[str, int]]
|
reg_addrs: list[tuple[str, int|str]]
|
||||||
async with open_registry(
|
async with open_registry(
|
||||||
addrs=(
|
addrs=(
|
||||||
registry_addrs
|
registry_addrs
|
||||||
|
|
@ -172,15 +198,13 @@ async def find_service(
|
||||||
only_first=first_only, # if set only returns single ref
|
only_first=first_only, # if set only returns single ref
|
||||||
) as maybe_portals:
|
) as maybe_portals:
|
||||||
if not maybe_portals:
|
if not maybe_portals:
|
||||||
# log.info(
|
log.info(
|
||||||
print(
|
|
||||||
f'Could NOT find service {service_name!r} -> {maybe_portals!r}'
|
f'Could NOT find service {service_name!r} -> {maybe_portals!r}'
|
||||||
)
|
)
|
||||||
yield None
|
yield None
|
||||||
return
|
return
|
||||||
|
|
||||||
# log.info(
|
log.info(
|
||||||
print(
|
|
||||||
f'Found service {service_name!r} -> {maybe_portals}'
|
f'Found service {service_name!r} -> {maybe_portals}'
|
||||||
)
|
)
|
||||||
yield maybe_portals
|
yield maybe_portals
|
||||||
|
|
@ -195,8 +219,7 @@ async def find_service(
|
||||||
|
|
||||||
async def check_for_service(
|
async def check_for_service(
|
||||||
service_name: str,
|
service_name: str,
|
||||||
|
) -> None|tuple[str, int]:
|
||||||
) -> None | tuple[str, int]:
|
|
||||||
'''
|
'''
|
||||||
Service daemon "liveness" predicate.
|
Service daemon "liveness" predicate.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
Sub-sys module commons.
|
Sub-sys module commons (if any ?? Bp).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from ..log import (
|
|
||||||
get_logger,
|
|
||||||
get_console_log,
|
|
||||||
)
|
|
||||||
subsys: str = 'piker.service'
|
subsys: str = 'piker.service'
|
||||||
|
|
||||||
log = get_logger(subsys)
|
# ?TODO, if we were going to keep a `get_console_log()` in here to be
|
||||||
|
# invoked at `import`-time, how do we dynamically hand in the
|
||||||
get_console_log = partial(
|
# `level=` value? seems too early in the runtime to be injected
|
||||||
get_console_log,
|
# right?
|
||||||
name=subsys,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import asynccontextmanager as acm
|
from contextlib import asynccontextmanager as acm
|
||||||
|
from pprint import pformat
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
|
@ -26,12 +27,17 @@ import asks
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import docker
|
import docker
|
||||||
from ._ahab import DockerContainer
|
from ._ahab import DockerContainer
|
||||||
|
from . import (
|
||||||
|
Services,
|
||||||
|
)
|
||||||
|
|
||||||
from ._util import log # sub-sys logger
|
from piker.log import (
|
||||||
from ._util import (
|
|
||||||
get_console_log,
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
# container level config
|
# container level config
|
||||||
_config = {
|
_config = {
|
||||||
|
|
@ -67,7 +73,10 @@ def start_elasticsearch(
|
||||||
elastic
|
elastic
|
||||||
|
|
||||||
'''
|
'''
|
||||||
get_console_log('info', name=__name__)
|
get_console_log(
|
||||||
|
level='info',
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
dcntr: DockerContainer = client.containers.run(
|
dcntr: DockerContainer = client.containers.run(
|
||||||
'piker:elastic',
|
'piker:elastic',
|
||||||
|
|
|
||||||
|
|
@ -52,17 +52,18 @@ import pendulum
|
||||||
# TODO: import this for specific error set expected by mkts client
|
# TODO: import this for specific error set expected by mkts client
|
||||||
# import purerpc
|
# import purerpc
|
||||||
|
|
||||||
from ..data.feed import maybe_open_feed
|
from piker.data.feed import maybe_open_feed
|
||||||
from . import Services
|
from . import Services
|
||||||
from ._util import (
|
from piker.log import (
|
||||||
log, # sub-sys logger
|
|
||||||
get_console_log,
|
get_console_log,
|
||||||
|
get_logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import docker
|
import docker
|
||||||
from ._ahab import DockerContainer
|
from ._ahab import DockerContainer
|
||||||
|
|
||||||
|
log = get_logger(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
# ahabd-supervisor and container level config
|
# ahabd-supervisor and container level config
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,11 @@ def ldshm(
|
||||||
f'Something is wrong with time period for {shm}:\n{times}'
|
f'Something is wrong with time period for {shm}:\n{times}'
|
||||||
)
|
)
|
||||||
period_s: float = float(max(d1, d2, med))
|
period_s: float = float(max(d1, d2, med))
|
||||||
|
log.info(
|
||||||
|
f'Processing shm buffer:\n'
|
||||||
|
f' file: {shmfile.name}\n'
|
||||||
|
f' period: {period_s}s\n'
|
||||||
|
)
|
||||||
|
|
||||||
null_segs: tuple = tsp.get_null_segs(
|
null_segs: tuple = tsp.get_null_segs(
|
||||||
frame=shm.array,
|
frame=shm.array,
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,10 @@ from ..log import (
|
||||||
# for "time series processing"
|
# for "time series processing"
|
||||||
subsys: str = 'piker.tsp'
|
subsys: str = 'piker.tsp'
|
||||||
|
|
||||||
log = get_logger(subsys)
|
log = get_logger(name=__name__)
|
||||||
get_console_log = partial(
|
get_console_log = partial(
|
||||||
get_console_log,
|
get_console_log,
|
||||||
name=subsys,
|
name=subsys, # activate for subsys-pkg "downward"
|
||||||
)
|
)
|
||||||
|
|
||||||
# NOTE: union type-defs to handle generic `numpy` and `polars` types
|
# NOTE: union type-defs to handle generic `numpy` and `polars` types
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ import tractor
|
||||||
|
|
||||||
from piker.data._formatters import BGM
|
from piker.data._formatters import BGM
|
||||||
from piker.storage import log
|
from piker.storage import log
|
||||||
|
from piker.toolz.profile import (
|
||||||
|
Profiler,
|
||||||
|
pg_profile_enabled,
|
||||||
|
ms_slower_then,
|
||||||
|
)
|
||||||
from piker.ui._style import get_fonts
|
from piker.ui._style import get_fonts
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -92,12 +97,22 @@ async def markup_gaps(
|
||||||
# gap's duration.
|
# gap's duration.
|
||||||
show_txt: bool = False,
|
show_txt: bool = False,
|
||||||
|
|
||||||
|
# A/B comparison: render individual arrows alongside batch
|
||||||
|
# for visual comparison
|
||||||
|
show_individual_arrows: bool = False,
|
||||||
|
|
||||||
) -> dict[int, dict]:
|
) -> dict[int, dict]:
|
||||||
'''
|
'''
|
||||||
Remote annotate time-gaps in a dt-fielded ts (normally OHLC)
|
Remote annotate time-gaps in a dt-fielded ts (normally OHLC)
|
||||||
with rectangles.
|
with rectangles.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
profiler = Profiler(
|
||||||
|
msg=f'markup_gaps() for {gaps.height} gaps',
|
||||||
|
disabled=False,
|
||||||
|
ms_threshold=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
# XXX: force chart redraw FIRST to ensure PlotItem coordinate
|
# XXX: force chart redraw FIRST to ensure PlotItem coordinate
|
||||||
# system is properly initialized before we position annotations!
|
# system is properly initialized before we position annotations!
|
||||||
# Without this, annotations may be misaligned on first creation
|
# Without this, annotations may be misaligned on first creation
|
||||||
|
|
@ -106,6 +121,19 @@ async def markup_gaps(
|
||||||
fqme=fqme,
|
fqme=fqme,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
)
|
)
|
||||||
|
profiler('first `.redraw()` before annot creation')
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
f'markup_gaps() called:\n'
|
||||||
|
f' fqme: {fqme}\n'
|
||||||
|
f' timeframe: {timeframe}s\n'
|
||||||
|
f' gaps.height: {gaps.height}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# collect all annotation specs for batch submission
|
||||||
|
rect_specs: list[dict] = []
|
||||||
|
arrow_specs: list[dict] = []
|
||||||
|
text_specs: list[dict] = []
|
||||||
|
|
||||||
aids: dict[int] = {}
|
aids: dict[int] = {}
|
||||||
for i in range(gaps.height):
|
for i in range(gaps.height):
|
||||||
|
|
@ -217,56 +245,38 @@ async def markup_gaps(
|
||||||
# 1: 'wine', # down-gap
|
# 1: 'wine', # down-gap
|
||||||
# }[sgn]
|
# }[sgn]
|
||||||
|
|
||||||
rect_kwargs: dict[str, Any] = dict(
|
# collect rect spec (no fqme/timeframe, added by batch
|
||||||
fqme=fqme,
|
# API)
|
||||||
timeframe=timeframe,
|
rect_spec: dict[str, Any] = dict(
|
||||||
|
meth='set_view_pos',
|
||||||
start_pos=lc,
|
start_pos=lc,
|
||||||
end_pos=ro,
|
end_pos=ro,
|
||||||
color=color,
|
color=color,
|
||||||
|
update_label=False,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
)
|
)
|
||||||
|
rect_specs.append(rect_spec)
|
||||||
|
|
||||||
# add up/down rects
|
|
||||||
aid: int|None = await actl.add_rect(**rect_kwargs)
|
|
||||||
if aid is None:
|
|
||||||
log.error(
|
|
||||||
f'Failed to add rect for,\n'
|
|
||||||
f'{rect_kwargs!r}\n'
|
|
||||||
f'\n'
|
|
||||||
f'Skipping to next gap!\n'
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
assert aid
|
|
||||||
aids[aid] = rect_kwargs
|
|
||||||
direction: str = (
|
direction: str = (
|
||||||
'down' if down_gap
|
'down' if down_gap
|
||||||
else 'up'
|
else 'up'
|
||||||
)
|
)
|
||||||
# TODO! mk this a `msgspec.Struct` which we deserialize
|
|
||||||
# on the server side!
|
# collect arrow spec
|
||||||
# XXX: send timestamp for server-side index lookup
|
|
||||||
# to ensure alignment with current shm state
|
|
||||||
gap_time: float = row['time'][0]
|
gap_time: float = row['time'][0]
|
||||||
arrow_kwargs: dict[str, Any] = dict(
|
arrow_spec: dict[str, Any] = dict(
|
||||||
fqme=fqme,
|
|
||||||
timeframe=timeframe,
|
|
||||||
x=iend, # fallback if timestamp lookup fails
|
x=iend, # fallback if timestamp lookup fails
|
||||||
y=cls,
|
y=cls,
|
||||||
time=gap_time, # for server-side index lookup
|
time=gap_time, # for server-side index lookup
|
||||||
color=color,
|
color=color,
|
||||||
alpha=169,
|
alpha=169,
|
||||||
pointing=direction,
|
pointing=direction,
|
||||||
# TODO: expose these as params to markup_gaps()?
|
|
||||||
headLen=10,
|
headLen=10,
|
||||||
headWidth=2.222,
|
headWidth=2.222,
|
||||||
pxMode=True,
|
pxMode=True,
|
||||||
)
|
)
|
||||||
|
arrow_specs.append(arrow_spec)
|
||||||
aid: int = await actl.add_arrow(
|
|
||||||
**arrow_kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# add duration label to RHS of arrow
|
# add duration label to RHS of arrow
|
||||||
if up_gap:
|
if up_gap:
|
||||||
|
|
@ -278,15 +288,12 @@ async def markup_gaps(
|
||||||
assert flat
|
assert flat
|
||||||
anchor = (0, 0) # up from bottom
|
anchor = (0, 0) # up from bottom
|
||||||
|
|
||||||
# use a slightly smaller font for gap label txt.
|
# collect text spec if enabled
|
||||||
font, small_font = get_fonts()
|
|
||||||
font_size: int = small_font.px_size - 1
|
|
||||||
assert isinstance(font_size, int)
|
|
||||||
|
|
||||||
if show_txt:
|
if show_txt:
|
||||||
text_aid: int = await actl.add_text(
|
font, small_font = get_fonts()
|
||||||
fqme=fqme,
|
font_size: int = small_font.px_size - 1
|
||||||
timeframe=timeframe,
|
|
||||||
|
text_spec: dict[str, Any] = dict(
|
||||||
text=gap_label,
|
text=gap_label,
|
||||||
x=iend + 1, # fallback if timestamp lookup fails
|
x=iend + 1, # fallback if timestamp lookup fails
|
||||||
y=cls,
|
y=cls,
|
||||||
|
|
@ -295,12 +302,46 @@ async def markup_gaps(
|
||||||
anchor=anchor,
|
anchor=anchor,
|
||||||
font_size=font_size,
|
font_size=font_size,
|
||||||
)
|
)
|
||||||
aids[text_aid] = {'text': gap_label}
|
text_specs.append(text_spec)
|
||||||
|
|
||||||
# tell chart to redraw all its
|
# submit all annotations in single batch IPC msg
|
||||||
# graphics view layers Bo
|
log.info(
|
||||||
|
f'Submitting batch annotations:\n'
|
||||||
|
f' rects: {len(rect_specs)}\n'
|
||||||
|
f' arrows: {len(arrow_specs)}\n'
|
||||||
|
f' texts: {len(text_specs)}\n'
|
||||||
|
)
|
||||||
|
profiler('built all annotation specs')
|
||||||
|
|
||||||
|
result: dict[str, list[int]] = await actl.add_batch(
|
||||||
|
fqme=fqme,
|
||||||
|
timeframe=timeframe,
|
||||||
|
rects=rect_specs,
|
||||||
|
arrows=arrow_specs,
|
||||||
|
texts=text_specs,
|
||||||
|
show_individual_arrows=show_individual_arrows,
|
||||||
|
)
|
||||||
|
profiler('batch `.add_batch()` IPC call complete')
|
||||||
|
|
||||||
|
# build aids dict from batch results
|
||||||
|
for aid in result['rects']:
|
||||||
|
aids[aid] = {'type': 'rect'}
|
||||||
|
for aid in result['arrows']:
|
||||||
|
aids[aid] = {'type': 'arrow'}
|
||||||
|
for aid in result['texts']:
|
||||||
|
aids[aid] = {'type': 'text'}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
f'Batch submission complete: {len(aids)} annotation(s) '
|
||||||
|
f'created'
|
||||||
|
)
|
||||||
|
profiler('built aids result dict')
|
||||||
|
|
||||||
|
# tell chart to redraw all its graphics view layers
|
||||||
await actl.redraw(
|
await actl.redraw(
|
||||||
fqme=fqme,
|
fqme=fqme,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
)
|
)
|
||||||
|
profiler('final `.redraw()` after annot creation')
|
||||||
|
|
||||||
return aids
|
return aids
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,10 @@ from ..data._sharedmem import (
|
||||||
maybe_open_shm_array,
|
maybe_open_shm_array,
|
||||||
ShmArray,
|
ShmArray,
|
||||||
)
|
)
|
||||||
from ..data._source import def_iohlcv_fields
|
from piker.data._source import (
|
||||||
from ..data._sampling import (
|
def_iohlcv_fields,
|
||||||
|
)
|
||||||
|
from piker.data._sampling import (
|
||||||
open_sample_stream,
|
open_sample_stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -96,7 +98,9 @@ if TYPE_CHECKING:
|
||||||
# from .feed import _FeedsBus
|
# from .feed import _FeedsBus
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# `ShmArray` buffer sizing configuration:
|
# `ShmArray` buffer sizing configuration:
|
||||||
|
|
@ -550,7 +554,7 @@ async def start_backfill(
|
||||||
)
|
)
|
||||||
# ?TODO, check against venue closure hours
|
# ?TODO, check against venue closure hours
|
||||||
# if/when provided by backend?
|
# if/when provided by backend?
|
||||||
await tractor.pause()
|
# await tractor.pause()
|
||||||
|
|
||||||
expected_dur: Interval = (
|
expected_dur: Interval = (
|
||||||
last_start_dt.subtract(
|
last_start_dt.subtract(
|
||||||
|
|
@ -1320,6 +1324,7 @@ async def manage_history(
|
||||||
mkt: MktPair,
|
mkt: MktPair,
|
||||||
some_data_ready: trio.Event,
|
some_data_ready: trio.Event,
|
||||||
feed_is_live: trio.Event,
|
feed_is_live: trio.Event,
|
||||||
|
loglevel: str = 'warning',
|
||||||
timeframe: float = 60, # in seconds
|
timeframe: float = 60, # in seconds
|
||||||
wait_for_live_timeout: float = 0.5,
|
wait_for_live_timeout: float = 0.5,
|
||||||
|
|
||||||
|
|
@ -1497,6 +1502,7 @@ async def manage_history(
|
||||||
# data feed layer that needs to consume it).
|
# data feed layer that needs to consume it).
|
||||||
open_index_stream=True,
|
open_index_stream=True,
|
||||||
sub_for_broadcasts=False,
|
sub_for_broadcasts=False,
|
||||||
|
loglevel=loglevel,
|
||||||
|
|
||||||
) as sample_stream:
|
) as sample_stream:
|
||||||
# register 1s and 1m buffers with the global
|
# register 1s and 1m buffers with the global
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,11 @@ from pyqtgraph import (
|
||||||
Point,
|
Point,
|
||||||
functions as fn,
|
functions as fn,
|
||||||
Color,
|
Color,
|
||||||
|
GraphicsObject,
|
||||||
)
|
)
|
||||||
|
from pyqtgraph.Qt import internals
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
QtCore,
|
QtCore,
|
||||||
|
|
@ -35,6 +38,10 @@ from piker.ui.qt import (
|
||||||
QRectF,
|
QRectF,
|
||||||
QGraphicsPathItem,
|
QGraphicsPathItem,
|
||||||
)
|
)
|
||||||
|
from piker.ui._style import hcolor
|
||||||
|
from piker.log import get_logger
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def mk_marker_path(
|
def mk_marker_path(
|
||||||
|
|
@ -104,7 +111,7 @@ def mk_marker_path(
|
||||||
|
|
||||||
class LevelMarker(QGraphicsPathItem):
|
class LevelMarker(QGraphicsPathItem):
|
||||||
'''
|
'''
|
||||||
An arrow marker path graphich which redraws itself
|
An arrow marker path graphic which redraws itself
|
||||||
to the specified view coordinate level on each paint cycle.
|
to the specified view coordinate level on each paint cycle.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -251,9 +258,9 @@ def qgo_draw_markers(
|
||||||
|
|
||||||
) -> float:
|
) -> float:
|
||||||
'''
|
'''
|
||||||
Paint markers in ``pg.GraphicsItem`` style by first
|
Paint markers in ``pg.GraphicsItem`` style by first removing the
|
||||||
removing the view transform for the painter, drawing the markers
|
view transform for the painter, drawing the markers in scene
|
||||||
in scene coords, then restoring the view coords.
|
coords, then restoring the view coords.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# paint markers in native coordinate system
|
# paint markers in native coordinate system
|
||||||
|
|
@ -295,3 +302,449 @@ def qgo_draw_markers(
|
||||||
|
|
||||||
p.setTransform(orig_tr)
|
p.setTransform(orig_tr)
|
||||||
return max(sizes)
|
return max(sizes)
|
||||||
|
|
||||||
|
|
||||||
|
class GapAnnotations(GraphicsObject):
|
||||||
|
'''
|
||||||
|
Batch-rendered gap annotations using Qt's efficient drawing
|
||||||
|
APIs.
|
||||||
|
|
||||||
|
Instead of creating individual `QGraphicsItem` instances per
|
||||||
|
gap (which is very slow for 1000+ gaps), this class stores all
|
||||||
|
gap rectangles and arrows in numpy-backed arrays and renders
|
||||||
|
them in single batch paint calls.
|
||||||
|
|
||||||
|
Performance: ~1000x faster than individual items for large gap
|
||||||
|
counts.
|
||||||
|
|
||||||
|
Based on patterns from:
|
||||||
|
- `pyqtgraph.BarGraphItem` (batch rect rendering)
|
||||||
|
- `pyqtgraph.ScatterPlotItem` (fragment rendering)
|
||||||
|
- `piker.ui._curve.FlowGraphic` (single path pattern)
|
||||||
|
|
||||||
|
'''
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
gap_specs: list[dict],
|
||||||
|
array: np.ndarray|None = None,
|
||||||
|
color: str = 'dad_blue',
|
||||||
|
alpha: int = 169,
|
||||||
|
arrow_size: float = 10.0,
|
||||||
|
fqme: str|None = None,
|
||||||
|
timeframe: float|None = None,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
gap_specs: list of dicts with keys:
|
||||||
|
- start_pos: (x, y) tuple for left corner of rect
|
||||||
|
- end_pos: (x, y) tuple for right corner of rect
|
||||||
|
- arrow_x: x position for arrow
|
||||||
|
- arrow_y: y position for arrow
|
||||||
|
- pointing: 'up' or 'down' for arrow direction
|
||||||
|
- start_time: (optional) timestamp for repositioning
|
||||||
|
- end_time: (optional) timestamp for repositioning
|
||||||
|
|
||||||
|
array: optional OHLC numpy array for repositioning on
|
||||||
|
backfill updates (when abs-index changes)
|
||||||
|
|
||||||
|
fqme: symbol name for these gaps (for logging/debugging)
|
||||||
|
|
||||||
|
timeframe: period in seconds that these gaps were
|
||||||
|
detected on (used to skip reposition when
|
||||||
|
called with wrong timeframe's array)
|
||||||
|
|
||||||
|
'''
|
||||||
|
super().__init__()
|
||||||
|
self._gap_specs = gap_specs
|
||||||
|
self._array = array
|
||||||
|
self._fqme = fqme
|
||||||
|
self._timeframe = timeframe
|
||||||
|
n_gaps = len(gap_specs)
|
||||||
|
|
||||||
|
# shared pen/brush matching original SelectRect/ArrowItem style
|
||||||
|
base_color = pg.mkColor(hcolor(color))
|
||||||
|
|
||||||
|
# rect pen: base color, fully opaque for outline
|
||||||
|
self._rect_pen = pg.mkPen(base_color, width=1)
|
||||||
|
|
||||||
|
# rect brush: base color with alpha=66 (SelectRect default)
|
||||||
|
rect_fill = pg.mkColor(hcolor(color))
|
||||||
|
rect_fill.setAlpha(66)
|
||||||
|
self._rect_brush = pg.functions.mkBrush(rect_fill)
|
||||||
|
|
||||||
|
# arrow pen: same as rects
|
||||||
|
self._arrow_pen = pg.mkPen(base_color, width=1)
|
||||||
|
|
||||||
|
# arrow brush: base color with user-specified alpha (default 169)
|
||||||
|
arrow_fill = pg.mkColor(hcolor(color))
|
||||||
|
arrow_fill.setAlpha(alpha)
|
||||||
|
self._arrow_brush = pg.functions.mkBrush(arrow_fill)
|
||||||
|
|
||||||
|
# allocate rect array using Qt's efficient storage
|
||||||
|
self._rectarray = internals.PrimitiveArray(
|
||||||
|
QtCore.QRectF,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
self._rectarray.resize(n_gaps)
|
||||||
|
rect_memory = self._rectarray.ndarray()
|
||||||
|
|
||||||
|
# fill rect array from gap specs
|
||||||
|
for (
|
||||||
|
i,
|
||||||
|
spec,
|
||||||
|
) in enumerate(gap_specs):
|
||||||
|
(
|
||||||
|
start_x,
|
||||||
|
start_y,
|
||||||
|
) = spec['start_pos']
|
||||||
|
(
|
||||||
|
end_x,
|
||||||
|
end_y,
|
||||||
|
) = spec['end_pos']
|
||||||
|
|
||||||
|
# QRectF expects (x, y, width, height)
|
||||||
|
rect_memory[i, 0] = start_x
|
||||||
|
rect_memory[i, 1] = min(start_y, end_y)
|
||||||
|
rect_memory[i, 2] = end_x - start_x
|
||||||
|
rect_memory[i, 3] = abs(end_y - start_y)
|
||||||
|
|
||||||
|
# build single QPainterPath for all arrows
|
||||||
|
self._arrow_path = QtGui.QPainterPath()
|
||||||
|
self._arrow_size = arrow_size
|
||||||
|
|
||||||
|
for spec in gap_specs:
|
||||||
|
arrow_x = spec['arrow_x']
|
||||||
|
arrow_y = spec['arrow_y']
|
||||||
|
pointing = spec['pointing']
|
||||||
|
|
||||||
|
# create arrow polygon
|
||||||
|
if pointing == 'down':
|
||||||
|
# arrow points downward
|
||||||
|
arrow_poly = QtGui.QPolygonF([
|
||||||
|
QPointF(arrow_x, arrow_y), # tip
|
||||||
|
QPointF(
|
||||||
|
arrow_x - arrow_size/2,
|
||||||
|
arrow_y - arrow_size,
|
||||||
|
), # left
|
||||||
|
QPointF(
|
||||||
|
arrow_x + arrow_size/2,
|
||||||
|
arrow_y - arrow_size,
|
||||||
|
), # right
|
||||||
|
])
|
||||||
|
else: # up
|
||||||
|
# arrow points upward
|
||||||
|
arrow_poly = QtGui.QPolygonF([
|
||||||
|
QPointF(arrow_x, arrow_y), # tip
|
||||||
|
QPointF(
|
||||||
|
arrow_x - arrow_size/2,
|
||||||
|
arrow_y + arrow_size,
|
||||||
|
), # left
|
||||||
|
QPointF(
|
||||||
|
arrow_x + arrow_size/2,
|
||||||
|
arrow_y + arrow_size,
|
||||||
|
), # right
|
||||||
|
])
|
||||||
|
|
||||||
|
self._arrow_path.addPolygon(arrow_poly)
|
||||||
|
self._arrow_path.closeSubpath()
|
||||||
|
|
||||||
|
# cache bounding rect
|
||||||
|
self._br: QRectF|None = None
|
||||||
|
|
||||||
|
def boundingRect(self) -> QRectF:
|
||||||
|
'''
|
||||||
|
Compute bounding rect from rect array and arrow path.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if self._br is not None:
|
||||||
|
return self._br
|
||||||
|
|
||||||
|
# get rect bounds
|
||||||
|
rect_memory = self._rectarray.ndarray()
|
||||||
|
if len(rect_memory) == 0:
|
||||||
|
self._br = QRectF()
|
||||||
|
return self._br
|
||||||
|
|
||||||
|
x_min = rect_memory[:, 0].min()
|
||||||
|
y_min = rect_memory[:, 1].min()
|
||||||
|
x_max = (rect_memory[:, 0] + rect_memory[:, 2]).max()
|
||||||
|
y_max = (rect_memory[:, 1] + rect_memory[:, 3]).max()
|
||||||
|
|
||||||
|
# expand for arrow path
|
||||||
|
arrow_br = self._arrow_path.boundingRect()
|
||||||
|
x_min = min(x_min, arrow_br.left())
|
||||||
|
y_min = min(y_min, arrow_br.top())
|
||||||
|
x_max = max(x_max, arrow_br.right())
|
||||||
|
y_max = max(y_max, arrow_br.bottom())
|
||||||
|
|
||||||
|
self._br = QRectF(
|
||||||
|
x_min,
|
||||||
|
y_min,
|
||||||
|
x_max - x_min,
|
||||||
|
y_max - y_min,
|
||||||
|
)
|
||||||
|
return self._br
|
||||||
|
|
||||||
|
def paint(
|
||||||
|
self,
|
||||||
|
p: QtGui.QPainter,
|
||||||
|
opt: QtWidgets.QStyleOptionGraphicsItem,
|
||||||
|
w: QtWidgets.QWidget,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Batch render all rects and arrows in minimal paint calls.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# draw all rects in single batch call (data coordinates)
|
||||||
|
p.setPen(self._rect_pen)
|
||||||
|
p.setBrush(self._rect_brush)
|
||||||
|
drawargs = self._rectarray.drawargs()
|
||||||
|
p.drawRects(*drawargs)
|
||||||
|
|
||||||
|
# draw arrows in scene/pixel coordinates so they maintain
|
||||||
|
# size regardless of zoom level
|
||||||
|
orig_tr = p.transform()
|
||||||
|
p.resetTransform()
|
||||||
|
|
||||||
|
# rebuild arrow path in scene coordinates
|
||||||
|
arrow_path_scene = QtGui.QPainterPath()
|
||||||
|
|
||||||
|
# arrow geometry matching pg.ArrowItem defaults
|
||||||
|
# headLen=10, headWidth=2.222
|
||||||
|
# headWidth is the half-width (center to edge distance)
|
||||||
|
head_len = self._arrow_size
|
||||||
|
head_width = head_len * 0.2222 # 2.222 at size=10
|
||||||
|
|
||||||
|
for spec in self._gap_specs:
|
||||||
|
if 'arrow_x' not in spec:
|
||||||
|
continue
|
||||||
|
|
||||||
|
arrow_x = spec['arrow_x']
|
||||||
|
arrow_y = spec['arrow_y']
|
||||||
|
pointing = spec['pointing']
|
||||||
|
|
||||||
|
# transform data coords to scene coords
|
||||||
|
scene_pt = orig_tr.map(QPointF(arrow_x, arrow_y))
|
||||||
|
sx = scene_pt.x()
|
||||||
|
sy = scene_pt.y()
|
||||||
|
|
||||||
|
# create arrow polygon in scene/pixel coords
|
||||||
|
# matching pg.ArrowItem geometry but rotated for up/down
|
||||||
|
if pointing == 'down':
|
||||||
|
# tip points downward (negative y direction)
|
||||||
|
arrow_poly = QtGui.QPolygonF([
|
||||||
|
QPointF(sx, sy), # tip
|
||||||
|
QPointF(
|
||||||
|
sx - head_width,
|
||||||
|
sy - head_len,
|
||||||
|
), # left base
|
||||||
|
QPointF(
|
||||||
|
sx + head_width,
|
||||||
|
sy - head_len,
|
||||||
|
), # right base
|
||||||
|
])
|
||||||
|
else: # up
|
||||||
|
# tip points upward (positive y direction)
|
||||||
|
arrow_poly = QtGui.QPolygonF([
|
||||||
|
QPointF(sx, sy), # tip
|
||||||
|
QPointF(
|
||||||
|
sx - head_width,
|
||||||
|
sy + head_len,
|
||||||
|
), # left base
|
||||||
|
QPointF(
|
||||||
|
sx + head_width,
|
||||||
|
sy + head_len,
|
||||||
|
), # right base
|
||||||
|
])
|
||||||
|
|
||||||
|
arrow_path_scene.addPolygon(arrow_poly)
|
||||||
|
arrow_path_scene.closeSubpath()
|
||||||
|
|
||||||
|
p.setPen(self._arrow_pen)
|
||||||
|
p.setBrush(self._arrow_brush)
|
||||||
|
p.drawPath(arrow_path_scene)
|
||||||
|
|
||||||
|
# restore original transform
|
||||||
|
p.setTransform(orig_tr)
|
||||||
|
|
||||||
|
def reposition(
|
||||||
|
self,
|
||||||
|
array: np.ndarray|None = None,
|
||||||
|
fqme: str|None = None,
|
||||||
|
timeframe: float|None = None,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Reposition all annotations based on timestamps.
|
||||||
|
|
||||||
|
Used when viz is updated (eg during backfill) and abs-index
|
||||||
|
range changes - we need to lookup new indices from timestamps.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# skip reposition if timeframe doesn't match
|
||||||
|
# (e.g., 1s gaps being repositioned with 60s array)
|
||||||
|
if (
|
||||||
|
timeframe is not None
|
||||||
|
and
|
||||||
|
self._timeframe is not None
|
||||||
|
and
|
||||||
|
timeframe != self._timeframe
|
||||||
|
):
|
||||||
|
log.debug(
|
||||||
|
f'Skipping reposition for {self._fqme} gaps:\n'
|
||||||
|
f' gap timeframe: {self._timeframe}s\n'
|
||||||
|
f' array timeframe: {timeframe}s\n'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if array is None:
|
||||||
|
array = self._array
|
||||||
|
|
||||||
|
if array is None:
|
||||||
|
log.warning(
|
||||||
|
'GapAnnotations.reposition() called but no array '
|
||||||
|
'provided'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# collect all unique timestamps we need to lookup
|
||||||
|
timestamps: set[float] = set()
|
||||||
|
for spec in self._gap_specs:
|
||||||
|
if spec.get('start_time') is not None:
|
||||||
|
timestamps.add(spec['start_time'])
|
||||||
|
if spec.get('end_time') is not None:
|
||||||
|
timestamps.add(spec['end_time'])
|
||||||
|
if spec.get('time') is not None:
|
||||||
|
timestamps.add(spec['time'])
|
||||||
|
|
||||||
|
# vectorized timestamp -> row lookup using binary search
|
||||||
|
time_to_row: dict[float, dict] = {}
|
||||||
|
if timestamps:
|
||||||
|
import numpy as np
|
||||||
|
time_arr = array['time']
|
||||||
|
ts_array = np.array(list(timestamps))
|
||||||
|
|
||||||
|
search_indices = np.searchsorted(
|
||||||
|
time_arr,
|
||||||
|
ts_array,
|
||||||
|
)
|
||||||
|
|
||||||
|
# vectorized bounds check and exact match verification
|
||||||
|
valid_mask = (
|
||||||
|
(search_indices < len(array))
|
||||||
|
& (time_arr[search_indices] == ts_array)
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_indices = search_indices[valid_mask]
|
||||||
|
valid_timestamps = ts_array[valid_mask]
|
||||||
|
matched_rows = array[valid_indices]
|
||||||
|
|
||||||
|
time_to_row = {
|
||||||
|
float(ts): {
|
||||||
|
'index': float(row['index']),
|
||||||
|
'open': float(row['open']),
|
||||||
|
'close': float(row['close']),
|
||||||
|
}
|
||||||
|
for ts, row in zip(
|
||||||
|
valid_timestamps,
|
||||||
|
matched_rows,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# rebuild rect array from gap specs with new indices
|
||||||
|
rect_memory = self._rectarray.ndarray()
|
||||||
|
|
||||||
|
for (
|
||||||
|
i,
|
||||||
|
spec,
|
||||||
|
) in enumerate(self._gap_specs):
|
||||||
|
start_time = spec.get('start_time')
|
||||||
|
end_time = spec.get('end_time')
|
||||||
|
|
||||||
|
if (
|
||||||
|
start_time is None
|
||||||
|
or end_time is None
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_row = time_to_row.get(start_time)
|
||||||
|
end_row = time_to_row.get(end_time)
|
||||||
|
|
||||||
|
if (
|
||||||
|
start_row is None
|
||||||
|
or end_row is None
|
||||||
|
):
|
||||||
|
log.warning(
|
||||||
|
f'Timestamp lookup failed for gap[{i}] during '
|
||||||
|
f'reposition:\n'
|
||||||
|
f' fqme: {fqme}\n'
|
||||||
|
f' timeframe: {timeframe}s\n'
|
||||||
|
f' start_time: {start_time}\n'
|
||||||
|
f' end_time: {end_time}\n'
|
||||||
|
f' array time range: '
|
||||||
|
f'{array["time"][0]} -> {array["time"][-1]}\n'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_idx = start_row['index']
|
||||||
|
end_idx = end_row['index']
|
||||||
|
start_close = start_row['close']
|
||||||
|
end_open = end_row['open']
|
||||||
|
|
||||||
|
from_idx: float = 0.16 - 0.06
|
||||||
|
start_x = start_idx + 1 - from_idx
|
||||||
|
end_x = end_idx + from_idx
|
||||||
|
|
||||||
|
# update rect in array
|
||||||
|
rect_memory[i, 0] = start_x
|
||||||
|
rect_memory[i, 1] = min(start_close, end_open)
|
||||||
|
rect_memory[i, 2] = end_x - start_x
|
||||||
|
rect_memory[i, 3] = abs(end_open - start_close)
|
||||||
|
|
||||||
|
# rebuild arrow path with new indices
|
||||||
|
self._arrow_path.clear()
|
||||||
|
|
||||||
|
for spec in self._gap_specs:
|
||||||
|
time_val = spec.get('time')
|
||||||
|
if time_val is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
arrow_row = time_to_row.get(time_val)
|
||||||
|
if arrow_row is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
arrow_x = arrow_row['index']
|
||||||
|
arrow_y = arrow_row['close']
|
||||||
|
pointing = spec['pointing']
|
||||||
|
|
||||||
|
# create arrow polygon
|
||||||
|
if pointing == 'down':
|
||||||
|
arrow_poly = QtGui.QPolygonF([
|
||||||
|
QPointF(arrow_x, arrow_y),
|
||||||
|
QPointF(
|
||||||
|
arrow_x - self._arrow_size/2,
|
||||||
|
arrow_y - self._arrow_size,
|
||||||
|
),
|
||||||
|
QPointF(
|
||||||
|
arrow_x + self._arrow_size/2,
|
||||||
|
arrow_y - self._arrow_size,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
else: # up
|
||||||
|
arrow_poly = QtGui.QPolygonF([
|
||||||
|
QPointF(arrow_x, arrow_y),
|
||||||
|
QPointF(
|
||||||
|
arrow_x - self._arrow_size/2,
|
||||||
|
arrow_y + self._arrow_size,
|
||||||
|
),
|
||||||
|
QPointF(
|
||||||
|
arrow_x + self._arrow_size/2,
|
||||||
|
arrow_y + self._arrow_size,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
self._arrow_path.addPolygon(arrow_poly)
|
||||||
|
self._arrow_path.closeSubpath()
|
||||||
|
|
||||||
|
# invalidate bounding rect cache
|
||||||
|
self._br = None
|
||||||
|
self.prepareGeometryChange()
|
||||||
|
self.update()
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ from . import _search
|
||||||
from ..accounting import unpack_fqme
|
from ..accounting import unpack_fqme
|
||||||
from ..data._symcache import open_symcache
|
from ..data._symcache import open_symcache
|
||||||
from ..data.feed import install_brokerd_search
|
from ..data.feed import install_brokerd_search
|
||||||
from ..log import get_logger
|
from ..log import (
|
||||||
|
get_logger,
|
||||||
|
get_console_log,
|
||||||
|
)
|
||||||
from ..service import maybe_spawn_brokerd
|
from ..service import maybe_spawn_brokerd
|
||||||
from ._exec import run_qtractor
|
from ._exec import run_qtractor
|
||||||
|
|
||||||
|
|
@ -87,6 +90,13 @@ async def _async_main(
|
||||||
Provision the "main" widget with initial symbol data and root nursery.
|
Provision the "main" widget with initial symbol data and root nursery.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# enable chart's console logging
|
||||||
|
if loglevel:
|
||||||
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
# set as singleton
|
# set as singleton
|
||||||
_chart._godw = main_widget
|
_chart._godw = main_widget
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -413,9 +413,18 @@ class Cursor(pg.GraphicsObject):
|
||||||
self,
|
self,
|
||||||
item: pg.GraphicsObject,
|
item: pg.GraphicsObject,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert getattr(item, 'delete'), f"{item} must define a ``.delete()``"
|
assert getattr(
|
||||||
|
item,
|
||||||
|
'delete',
|
||||||
|
), f"{item} must define a ``.delete()``"
|
||||||
self._hovered.add(item)
|
self._hovered.add(item)
|
||||||
|
|
||||||
|
def is_hovered(
|
||||||
|
self,
|
||||||
|
item: pg.GraphicsObject,
|
||||||
|
) -> bool:
|
||||||
|
return item in self._hovered
|
||||||
|
|
||||||
def add_plot(
|
def add_plot(
|
||||||
self,
|
self,
|
||||||
plot: ChartPlotWidget, # noqa
|
plot: ChartPlotWidget, # noqa
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ from piker.ui.qt import QLineF
|
||||||
from ..data._sharedmem import (
|
from ..data._sharedmem import (
|
||||||
ShmArray,
|
ShmArray,
|
||||||
)
|
)
|
||||||
from ..data.feed import Flume
|
from ..data.flows import Flume
|
||||||
from ..data._formatters import (
|
from ..data._formatters import (
|
||||||
IncrementalFormatter,
|
IncrementalFormatter,
|
||||||
OHLCBarsFmtr, # Plain OHLC renderer
|
OHLCBarsFmtr, # Plain OHLC renderer
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ this module ties together quote and computational (fsp) streams with
|
||||||
graphics update methods via our custom ``pyqtgraph`` charting api.
|
graphics update methods via our custom ``pyqtgraph`` charting api.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from functools import partial
|
||||||
import itertools
|
import itertools
|
||||||
from math import floor
|
from math import floor
|
||||||
import time
|
import time
|
||||||
|
|
@ -208,6 +209,7 @@ class DisplayState(Struct):
|
||||||
async def increment_history_view(
|
async def increment_history_view(
|
||||||
# min_istream: tractor.MsgStream,
|
# min_istream: tractor.MsgStream,
|
||||||
ds: DisplayState,
|
ds: DisplayState,
|
||||||
|
loglevel: str = 'warning',
|
||||||
):
|
):
|
||||||
hist_chart: ChartPlotWidget = ds.hist_chart
|
hist_chart: ChartPlotWidget = ds.hist_chart
|
||||||
hist_viz: Viz = ds.hist_viz
|
hist_viz: Viz = ds.hist_viz
|
||||||
|
|
@ -229,7 +231,10 @@ async def increment_history_view(
|
||||||
hist_viz.reset_graphics()
|
hist_viz.reset_graphics()
|
||||||
# hist_viz.update_graphics(force_redraw=True)
|
# hist_viz.update_graphics(force_redraw=True)
|
||||||
|
|
||||||
async with open_sample_stream(1.) as min_istream:
|
async with open_sample_stream(
|
||||||
|
period_s=1.,
|
||||||
|
loglevel=loglevel,
|
||||||
|
) as min_istream:
|
||||||
async for msg in min_istream:
|
async for msg in min_istream:
|
||||||
|
|
||||||
profiler = Profiler(
|
profiler = Profiler(
|
||||||
|
|
@ -310,7 +315,6 @@ async def increment_history_view(
|
||||||
|
|
||||||
|
|
||||||
async def graphics_update_loop(
|
async def graphics_update_loop(
|
||||||
|
|
||||||
dss: dict[str, DisplayState],
|
dss: dict[str, DisplayState],
|
||||||
nurse: trio.Nursery,
|
nurse: trio.Nursery,
|
||||||
godwidget: GodWidget,
|
godwidget: GodWidget,
|
||||||
|
|
@ -319,6 +323,7 @@ async def graphics_update_loop(
|
||||||
|
|
||||||
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {},
|
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {},
|
||||||
vlm_charts: dict[str, ChartPlotWidget] = {},
|
vlm_charts: dict[str, ChartPlotWidget] = {},
|
||||||
|
loglevel: str = 'warning',
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -462,9 +467,12 @@ async def graphics_update_loop(
|
||||||
# })
|
# })
|
||||||
|
|
||||||
nurse.start_soon(
|
nurse.start_soon(
|
||||||
increment_history_view,
|
partial(
|
||||||
# min_istream,
|
increment_history_view,
|
||||||
ds,
|
# min_istream,
|
||||||
|
ds=ds,
|
||||||
|
loglevel=loglevel,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
|
@ -511,14 +519,19 @@ async def graphics_update_loop(
|
||||||
fast_chart.linked.isHidden()
|
fast_chart.linked.isHidden()
|
||||||
or not rt_pi.isVisible()
|
or not rt_pi.isVisible()
|
||||||
):
|
):
|
||||||
print(f'{fqme} skipping update for HIDDEN CHART')
|
log.debug(
|
||||||
|
f'{fqme} skipping update for HIDDEN CHART'
|
||||||
|
)
|
||||||
fast_chart.pause_all_feeds()
|
fast_chart.pause_all_feeds()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ic = fast_chart.view._in_interact
|
ic = fast_chart.view._in_interact
|
||||||
if ic:
|
if ic:
|
||||||
fast_chart.pause_all_feeds()
|
fast_chart.pause_all_feeds()
|
||||||
print(f'{fqme} PAUSING DURING INTERACTION')
|
log.debug(
|
||||||
|
f'Pausing chart updaates during interaction\n'
|
||||||
|
f'fqme: {fqme!r}'
|
||||||
|
)
|
||||||
await ic.wait()
|
await ic.wait()
|
||||||
fast_chart.resume_all_feeds()
|
fast_chart.resume_all_feeds()
|
||||||
|
|
||||||
|
|
@ -1591,15 +1604,18 @@ async def display_symbol_data(
|
||||||
# start update loop task
|
# start update loop task
|
||||||
dss: dict[str, DisplayState] = {}
|
dss: dict[str, DisplayState] = {}
|
||||||
ln.start_soon(
|
ln.start_soon(
|
||||||
graphics_update_loop,
|
partial(
|
||||||
dss,
|
graphics_update_loop,
|
||||||
ln,
|
dss=dss,
|
||||||
godwidget,
|
nurse=ln,
|
||||||
feed,
|
godwidget=godwidget,
|
||||||
# min_istream,
|
feed=feed,
|
||||||
|
# min_istream,
|
||||||
|
|
||||||
pis,
|
pis=pis,
|
||||||
vlm_charts,
|
vlm_charts=vlm_charts,
|
||||||
|
loglevel=loglevel,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# boot order-mode
|
# boot order-mode
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ class ArrowEditor(Struct):
|
||||||
'''
|
'''
|
||||||
uid: str = arrow._uid
|
uid: str = arrow._uid
|
||||||
arrows: list[pg.ArrowItem] = self._arrows[uid]
|
arrows: list[pg.ArrowItem] = self._arrows[uid]
|
||||||
log.info(
|
log.debug(
|
||||||
f'Removing arrow from views\n'
|
f'Removing arrow from views\n'
|
||||||
f'uid: {uid!r}\n'
|
f'uid: {uid!r}\n'
|
||||||
f'{arrow!r}\n'
|
f'{arrow!r}\n'
|
||||||
|
|
@ -286,7 +286,9 @@ class LineEditor(Struct):
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line.show_labels()
|
line.show_labels()
|
||||||
line.hide_markers()
|
line.hide_markers()
|
||||||
log.debug(f'Level active for level: {line.value()}')
|
log.debug(
|
||||||
|
f'Line active @ level: {line.value()!r}'
|
||||||
|
)
|
||||||
# TODO: other flashy things to indicate the order is active
|
# TODO: other flashy things to indicate the order is active
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
@ -329,7 +331,11 @@ class LineEditor(Struct):
|
||||||
if line in hovered:
|
if line in hovered:
|
||||||
hovered.remove(line)
|
hovered.remove(line)
|
||||||
|
|
||||||
log.debug(f'deleting {line} with oid: {uuid}')
|
log.debug(
|
||||||
|
f'Deleting level-line\n'
|
||||||
|
f'line: {line!r}\n'
|
||||||
|
f'oid: {uuid!r}\n'
|
||||||
|
)
|
||||||
line.delete()
|
line.delete()
|
||||||
|
|
||||||
# make sure the xhair doesn't get left off
|
# make sure the xhair doesn't get left off
|
||||||
|
|
@ -337,7 +343,11 @@ class LineEditor(Struct):
|
||||||
cursor.show_xhair()
|
cursor.show_xhair()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.warning(f'Could not find line for {line}')
|
log.warning(
|
||||||
|
f'Could not find line for removal ??\n'
|
||||||
|
f'\n'
|
||||||
|
f'{line!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
@ -569,11 +579,11 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
if update_label:
|
if update_label:
|
||||||
self.init_label(view_rect)
|
self.init_label(view_rect)
|
||||||
|
|
||||||
print(
|
log.debug(
|
||||||
'SelectRect modify:\n'
|
f'SelectRect modify,\n'
|
||||||
f'QRectF: {view_rect}\n'
|
f'QRectF: {view_rect}\n'
|
||||||
f'start_pos: {start_pos}\n'
|
f'start_pos: {start_pos!r}\n'
|
||||||
f'end_pos: {end_pos}\n'
|
f'end_pos: {end_pos!r}\n'
|
||||||
)
|
)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
|
|
@ -640,8 +650,11 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
dmn=dmn,
|
dmn=dmn,
|
||||||
))
|
))
|
||||||
|
|
||||||
# print(f'x2, y2: {(x2, y2)}')
|
# tracing
|
||||||
# print(f'xmn, ymn: {(xmn, ymx)}')
|
# log.info(
|
||||||
|
# f'x2, y2: {(x2, y2)}\n'
|
||||||
|
# f'xmn, ymn: {(xmn, ymx)}\n'
|
||||||
|
# )
|
||||||
|
|
||||||
label_anchor = Point(
|
label_anchor = Point(
|
||||||
xmx + 2,
|
xmx + 2,
|
||||||
|
|
|
||||||
|
|
@ -183,13 +183,17 @@ async def open_fsp_sidepane(
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def open_fsp_actor_cluster(
|
async def open_fsp_actor_cluster(
|
||||||
names: list[str] = ['fsp_0', 'fsp_1'],
|
names: list[str] = [
|
||||||
|
'fsp_0',
|
||||||
|
'fsp_1',
|
||||||
|
],
|
||||||
|
|
||||||
) -> AsyncGenerator[
|
) -> AsyncGenerator[
|
||||||
int,
|
int,
|
||||||
dict[str, tractor.Portal]
|
dict[str, tractor.Portal]
|
||||||
]:
|
]:
|
||||||
|
|
||||||
|
# TODO! change to .experimental!
|
||||||
from tractor._clustering import open_actor_cluster
|
from tractor._clustering import open_actor_cluster
|
||||||
|
|
||||||
# profiler = Profiler(
|
# profiler = Profiler(
|
||||||
|
|
@ -197,7 +201,7 @@ async def open_fsp_actor_cluster(
|
||||||
# disabled=False
|
# disabled=False
|
||||||
# )
|
# )
|
||||||
async with open_actor_cluster(
|
async with open_actor_cluster(
|
||||||
count=2,
|
count=len(names),
|
||||||
names=names,
|
names=names,
|
||||||
modules=['piker.fsp._engine'],
|
modules=['piker.fsp._engine'],
|
||||||
|
|
||||||
|
|
@ -497,7 +501,8 @@ class FspAdmin:
|
||||||
|
|
||||||
portal: tractor.Portal = (
|
portal: tractor.Portal = (
|
||||||
self.cluster.get(worker_name)
|
self.cluster.get(worker_name)
|
||||||
or self.rr_next_portal()
|
or
|
||||||
|
self.rr_next_portal()
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: this should probably be turned into a
|
# TODO: this should probably be turned into a
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ from pyqtgraph import (
|
||||||
functions as fn,
|
functions as fn,
|
||||||
)
|
)
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
|
|
@ -72,7 +73,10 @@ if TYPE_CHECKING:
|
||||||
GodWidget,
|
GodWidget,
|
||||||
)
|
)
|
||||||
from ._dataviz import Viz
|
from ._dataviz import Viz
|
||||||
from .order_mode import OrderMode
|
from .order_mode import (
|
||||||
|
OrderMode,
|
||||||
|
Dialog,
|
||||||
|
)
|
||||||
from ._display import DisplayState
|
from ._display import DisplayState
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -130,7 +134,12 @@ async def handle_viewmode_kb_inputs(
|
||||||
|
|
||||||
async for kbmsg in recv_chan:
|
async for kbmsg in recv_chan:
|
||||||
event, etype, key, mods, text = kbmsg.to_tuple()
|
event, etype, key, mods, text = kbmsg.to_tuple()
|
||||||
log.debug(f'key: {key}, mods: {mods}, text: {text}')
|
log.debug(
|
||||||
|
f'View-mode kb-msg received,\n'
|
||||||
|
f'mods: {mods!r}\n'
|
||||||
|
f'key: {key!r}\n'
|
||||||
|
f'text: {text!r}\n'
|
||||||
|
)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
period = now - last
|
period = now - last
|
||||||
|
|
||||||
|
|
@ -158,8 +167,12 @@ async def handle_viewmode_kb_inputs(
|
||||||
# have no previous keys or we do and the min_tap period is
|
# have no previous keys or we do and the min_tap period is
|
||||||
# met
|
# met
|
||||||
if (
|
if (
|
||||||
not fast_key_seq or
|
not fast_key_seq
|
||||||
period <= min_tap and fast_key_seq
|
or (
|
||||||
|
period <= min_tap
|
||||||
|
and
|
||||||
|
fast_key_seq
|
||||||
|
)
|
||||||
):
|
):
|
||||||
fast_key_seq.append(text)
|
fast_key_seq.append(text)
|
||||||
log.debug(f'fast keys seqs {fast_key_seq}')
|
log.debug(f'fast keys seqs {fast_key_seq}')
|
||||||
|
|
@ -174,7 +187,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
# UI REPL-shell, with ctrl-p (for "pause")
|
# UI REPL-shell, with ctrl-p (for "pause")
|
||||||
if (
|
if (
|
||||||
ctrl
|
ctrl
|
||||||
and key in {
|
and
|
||||||
|
key in {
|
||||||
Qt.Key_P,
|
Qt.Key_P,
|
||||||
}
|
}
|
||||||
):
|
):
|
||||||
|
|
@ -184,7 +198,6 @@ async def handle_viewmode_kb_inputs(
|
||||||
vlm_chart = chart.linked.subplots['volume'] # noqa
|
vlm_chart = chart.linked.subplots['volume'] # noqa
|
||||||
vlm_viz = vlm_chart.main_viz # noqa
|
vlm_viz = vlm_chart.main_viz # noqa
|
||||||
dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa
|
dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa
|
||||||
import tractor
|
|
||||||
await tractor.pause()
|
await tractor.pause()
|
||||||
view.interact_graphics_cycle()
|
view.interact_graphics_cycle()
|
||||||
|
|
||||||
|
|
@ -192,7 +205,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
# shown data `Viz`s for the current chart app.
|
# shown data `Viz`s for the current chart app.
|
||||||
if (
|
if (
|
||||||
ctrl
|
ctrl
|
||||||
and key in {
|
and
|
||||||
|
key in {
|
||||||
Qt.Key_R,
|
Qt.Key_R,
|
||||||
}
|
}
|
||||||
):
|
):
|
||||||
|
|
@ -231,7 +245,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
key == Qt.Key_Escape
|
key == Qt.Key_Escape
|
||||||
or (
|
or (
|
||||||
ctrl
|
ctrl
|
||||||
and key == Qt.Key_C
|
and
|
||||||
|
key == Qt.Key_C
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
# ctrl-c as cancel
|
# ctrl-c as cancel
|
||||||
|
|
@ -242,17 +257,35 @@ async def handle_viewmode_kb_inputs(
|
||||||
# cancel order or clear graphics
|
# cancel order or clear graphics
|
||||||
if (
|
if (
|
||||||
key == Qt.Key_C
|
key == Qt.Key_C
|
||||||
or key == Qt.Key_Delete
|
or
|
||||||
|
key == Qt.Key_Delete
|
||||||
):
|
):
|
||||||
|
# log.info('Handling <c> hotkey!')
|
||||||
|
try:
|
||||||
|
dialogs: list[Dialog] = order_mode.cancel_orders_under_cursor()
|
||||||
|
except BaseException:
|
||||||
|
log.exception('Failed to cancel orders !?\n')
|
||||||
|
await tractor.pause()
|
||||||
|
|
||||||
order_mode.cancel_orders_under_cursor()
|
if not dialogs:
|
||||||
|
log.warning(
|
||||||
|
'No orders were cancelled?\n'
|
||||||
|
'Is there an order-line under the cursor?\n'
|
||||||
|
'If you think there IS your DE might be "hiding the mouse" before '
|
||||||
|
'we rx the keyboard input via Qt..\n'
|
||||||
|
'=> Check your DE and/or TWM settings to be sure! <=\n'
|
||||||
|
)
|
||||||
|
# ^TODO?, some way to detect if there's lines and
|
||||||
|
# the DE is cuckin with things?
|
||||||
|
# await tractor.pause()
|
||||||
|
|
||||||
# View modes
|
# View modes
|
||||||
if (
|
if (
|
||||||
ctrl
|
ctrl
|
||||||
and (
|
and (
|
||||||
key == Qt.Key_Equal
|
key == Qt.Key_Equal
|
||||||
or key == Qt.Key_I
|
or
|
||||||
|
key == Qt.Key_I
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
view.wheelEvent(
|
view.wheelEvent(
|
||||||
|
|
@ -264,7 +297,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
ctrl
|
ctrl
|
||||||
and (
|
and (
|
||||||
key == Qt.Key_Minus
|
key == Qt.Key_Minus
|
||||||
or key == Qt.Key_O
|
or
|
||||||
|
key == Qt.Key_O
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
view.wheelEvent(
|
view.wheelEvent(
|
||||||
|
|
@ -275,7 +309,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
|
|
||||||
elif (
|
elif (
|
||||||
not ctrl
|
not ctrl
|
||||||
and key == Qt.Key_R
|
and
|
||||||
|
key == Qt.Key_R
|
||||||
):
|
):
|
||||||
# NOTE: seems that if we don't yield a Qt render
|
# NOTE: seems that if we don't yield a Qt render
|
||||||
# cycle then the m4 downsampled curves will show here
|
# cycle then the m4 downsampled curves will show here
|
||||||
|
|
@ -477,7 +512,8 @@ async def handle_viewmode_mouse(
|
||||||
# view.raiseContextMenu(event)
|
# view.raiseContextMenu(event)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
view.order_mode.active and
|
view.order_mode.active
|
||||||
|
and
|
||||||
button == QtCore.Qt.LeftButton
|
button == QtCore.Qt.LeftButton
|
||||||
):
|
):
|
||||||
# when in order mode, submit execution
|
# when in order mode, submit execution
|
||||||
|
|
@ -781,7 +817,8 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# Scale or translate based on mouse button
|
# Scale or translate based on mouse button
|
||||||
if btn & (
|
if btn & (
|
||||||
QtCore.Qt.LeftButton | QtCore.Qt.MidButton
|
QtCore.Qt.LeftButton
|
||||||
|
| QtCore.Qt.MidButton
|
||||||
):
|
):
|
||||||
# zoom y-axis ONLY when click-n-drag on it
|
# zoom y-axis ONLY when click-n-drag on it
|
||||||
# if axis == 1:
|
# if axis == 1:
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ from piker.ui.qt import (
|
||||||
QtGui,
|
QtGui,
|
||||||
QGraphicsPathItem,
|
QGraphicsPathItem,
|
||||||
QStyleOptionGraphicsItem,
|
QStyleOptionGraphicsItem,
|
||||||
QGraphicsItem,
|
|
||||||
QGraphicsScene,
|
QGraphicsScene,
|
||||||
QWidget,
|
QWidget,
|
||||||
QPointF,
|
QPointF,
|
||||||
|
|
@ -52,10 +51,13 @@ from ._anchors import (
|
||||||
from ..calc import humanize
|
from ..calc import humanize
|
||||||
from ._label import Label
|
from ._label import Label
|
||||||
from ._style import hcolor, _font
|
from ._style import hcolor, _font
|
||||||
|
from ..log import get_logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._cursor import Cursor
|
from ._cursor import Cursor
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO: probably worth investigating if we can
|
# TODO: probably worth investigating if we can
|
||||||
# make .boundingRect() faster:
|
# make .boundingRect() faster:
|
||||||
|
|
@ -347,7 +349,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# TODO: enter labels edit mode
|
# TODO: enter labels edit mode
|
||||||
print(f'double click {ev}')
|
log.debug(f'double click {ev}')
|
||||||
|
|
||||||
def paint(
|
def paint(
|
||||||
self,
|
self,
|
||||||
|
|
@ -461,10 +463,19 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# hovered
|
# hovered
|
||||||
if (
|
if (
|
||||||
not ev.isExit()
|
not ev.isExit()
|
||||||
and ev.acceptDrags(QtCore.Qt.LeftButton)
|
and
|
||||||
|
ev.acceptDrags(QtCore.Qt.LeftButton)
|
||||||
):
|
):
|
||||||
# if already hovered we don't need to run again
|
# if already hovered we don't need to run again
|
||||||
if self.mouseHovering is True:
|
if (
|
||||||
|
self.mouseHovering is True
|
||||||
|
and
|
||||||
|
cur.is_hovered(self)
|
||||||
|
):
|
||||||
|
log.debug(
|
||||||
|
f'Already hovering ??\n'
|
||||||
|
f'cur._hovered: {cur._hovered!r}\n'
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.only_show_markers_on_hover:
|
if self.only_show_markers_on_hover:
|
||||||
|
|
@ -481,6 +492,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
cur._y_label_update = False
|
cur._y_label_update = False
|
||||||
|
|
||||||
# add us to cursor state
|
# add us to cursor state
|
||||||
|
log.debug(f'Adding line {self!r}\n')
|
||||||
cur.add_hovered(self)
|
cur.add_hovered(self)
|
||||||
|
|
||||||
if self._hide_xhair_on_hover:
|
if self._hide_xhair_on_hover:
|
||||||
|
|
@ -508,6 +520,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
self.currentPen = self.pen
|
self.currentPen = self.pen
|
||||||
|
|
||||||
|
log.debug(f'Removing line {self!r}\n')
|
||||||
cur._hovered.remove(self)
|
cur._hovered.remove(self)
|
||||||
|
|
||||||
if self.only_show_markers_on_hover:
|
if self.only_show_markers_on_hover:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ a chart from some other actor.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
asynccontextmanager as acm,
|
asynccontextmanager as acm,
|
||||||
|
contextmanager as cm,
|
||||||
AsyncExitStack,
|
AsyncExitStack,
|
||||||
)
|
)
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
@ -46,6 +47,7 @@ from piker.log import get_logger
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker.service import find_service
|
from piker.service import find_service
|
||||||
from piker.brokers import SymbolNotFound
|
from piker.brokers import SymbolNotFound
|
||||||
|
from piker.toolz import Profiler
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
QGraphicsItem,
|
QGraphicsItem,
|
||||||
)
|
)
|
||||||
|
|
@ -98,6 +100,8 @@ def rm_annot(
|
||||||
annot: ArrowEditor|SelectRect|pg.TextItem
|
annot: ArrowEditor|SelectRect|pg.TextItem
|
||||||
) -> bool:
|
) -> bool:
|
||||||
global _editors
|
global _editors
|
||||||
|
from piker.ui._annotate import GapAnnotations
|
||||||
|
|
||||||
match annot:
|
match annot:
|
||||||
case pg.ArrowItem():
|
case pg.ArrowItem():
|
||||||
editor = _editors[annot._uid]
|
editor = _editors[annot._uid]
|
||||||
|
|
@ -122,9 +126,35 @@ def rm_annot(
|
||||||
scene.removeItem(annot)
|
scene.removeItem(annot)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
case GapAnnotations():
|
||||||
|
scene = annot.scene()
|
||||||
|
if scene:
|
||||||
|
scene.removeItem(annot)
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@cm
|
||||||
|
def no_qt_updates(*items):
|
||||||
|
'''
|
||||||
|
Disable Qt widget/item updates during context to batch
|
||||||
|
render operations and only trigger single repaint on exit.
|
||||||
|
|
||||||
|
Accepts both QWidgets and QGraphicsItems.
|
||||||
|
|
||||||
|
'''
|
||||||
|
for item in items:
|
||||||
|
if hasattr(item, 'setUpdatesEnabled'):
|
||||||
|
item.setUpdatesEnabled(False)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
for item in items:
|
||||||
|
if hasattr(item, 'setUpdatesEnabled'):
|
||||||
|
item.setUpdatesEnabled(True)
|
||||||
|
|
||||||
|
|
||||||
async def serve_rc_annots(
|
async def serve_rc_annots(
|
||||||
ipc_key: str,
|
ipc_key: str,
|
||||||
annot_req_stream: MsgStream,
|
annot_req_stream: MsgStream,
|
||||||
|
|
@ -429,6 +459,333 @@ async def serve_rc_annots(
|
||||||
aids.add(aid)
|
aids.add(aid)
|
||||||
await annot_req_stream.send(aid)
|
await annot_req_stream.send(aid)
|
||||||
|
|
||||||
|
case {
|
||||||
|
'cmd': 'batch',
|
||||||
|
'fqme': fqme,
|
||||||
|
'timeframe': timeframe,
|
||||||
|
'rects': list(rect_specs),
|
||||||
|
'arrows': list(arrow_specs),
|
||||||
|
'texts': list(text_specs),
|
||||||
|
'show_individual_arrows': bool(show_individual_arrows),
|
||||||
|
}:
|
||||||
|
# batch submission handler - process multiple
|
||||||
|
# annotations in single IPC round-trip
|
||||||
|
ds: DisplayState = _dss[fqme]
|
||||||
|
try:
|
||||||
|
chart: ChartPlotWidget = {
|
||||||
|
60: ds.hist_chart,
|
||||||
|
1: ds.chart,
|
||||||
|
}[timeframe]
|
||||||
|
except KeyError:
|
||||||
|
msg: str = (
|
||||||
|
f'No chart for timeframe={timeframe}s, '
|
||||||
|
f'skipping batch annotation'
|
||||||
|
)
|
||||||
|
log.error(msg)
|
||||||
|
await annot_req_stream.send({'error': msg})
|
||||||
|
continue
|
||||||
|
|
||||||
|
cv: ChartView = chart.cv
|
||||||
|
viz: Viz = chart.get_viz(fqme)
|
||||||
|
shm = viz.shm
|
||||||
|
arr = shm.array
|
||||||
|
|
||||||
|
result: dict[str, list[int]] = {
|
||||||
|
'rects': [],
|
||||||
|
'arrows': [],
|
||||||
|
'texts': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
profiler = Profiler(
|
||||||
|
msg=(
|
||||||
|
f'Batch annotate {len(rect_specs)} gaps '
|
||||||
|
f'on {fqme}@{timeframe}s'
|
||||||
|
),
|
||||||
|
disabled=False,
|
||||||
|
delayed=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
aids_set: set[int] = ctxs[ipc_key][1]
|
||||||
|
|
||||||
|
# build unified gap_specs for GapAnnotations class
|
||||||
|
from piker.ui._annotate import GapAnnotations
|
||||||
|
|
||||||
|
gap_specs: list[dict] = []
|
||||||
|
n_gaps: int = max(
|
||||||
|
len(rect_specs),
|
||||||
|
len(arrow_specs),
|
||||||
|
)
|
||||||
|
profiler('setup batch annot creation')
|
||||||
|
|
||||||
|
# collect all unique timestamps for vectorized lookup
|
||||||
|
timestamps: list[float] = []
|
||||||
|
for rect_spec in rect_specs:
|
||||||
|
if start_time := rect_spec.get('start_time'):
|
||||||
|
timestamps.append(start_time)
|
||||||
|
if end_time := rect_spec.get('end_time'):
|
||||||
|
timestamps.append(end_time)
|
||||||
|
for arrow_spec in arrow_specs:
|
||||||
|
if time_val := arrow_spec.get('time'):
|
||||||
|
timestamps.append(time_val)
|
||||||
|
|
||||||
|
profiler('collect `timestamps: list` complet!')
|
||||||
|
|
||||||
|
# build timestamp -> row mapping using binary search
|
||||||
|
# O(m log n) instead of O(n*m) with np.isin
|
||||||
|
time_to_row: dict[float, dict] = {}
|
||||||
|
if timestamps:
|
||||||
|
import numpy as np
|
||||||
|
time_arr = arr['time']
|
||||||
|
ts_array = np.array(timestamps)
|
||||||
|
|
||||||
|
# binary search for each timestamp in sorted time array
|
||||||
|
search_indices = np.searchsorted(
|
||||||
|
time_arr,
|
||||||
|
ts_array,
|
||||||
|
)
|
||||||
|
|
||||||
|
profiler('`np.searchsorted()` complete!')
|
||||||
|
|
||||||
|
# vectorized bounds check and exact match verification
|
||||||
|
valid_mask = (
|
||||||
|
(search_indices < len(arr))
|
||||||
|
& (time_arr[search_indices] == ts_array)
|
||||||
|
)
|
||||||
|
|
||||||
|
# get all valid indices and timestamps
|
||||||
|
valid_indices = search_indices[valid_mask]
|
||||||
|
valid_timestamps = ts_array[valid_mask]
|
||||||
|
|
||||||
|
# use fancy indexing to get all rows at once
|
||||||
|
matched_rows = arr[valid_indices]
|
||||||
|
|
||||||
|
# extract fields to plain arrays BEFORE dict building
|
||||||
|
indices_arr = matched_rows['index'].astype(float)
|
||||||
|
opens_arr = matched_rows['open'].astype(float)
|
||||||
|
closes_arr = matched_rows['close'].astype(float)
|
||||||
|
|
||||||
|
profiler('extracted field arrays')
|
||||||
|
|
||||||
|
# build dict from plain arrays (much faster)
|
||||||
|
time_to_row: dict[float, dict] = {
|
||||||
|
float(ts): {
|
||||||
|
'index': idx,
|
||||||
|
'open': opn,
|
||||||
|
'close': cls,
|
||||||
|
}
|
||||||
|
for (
|
||||||
|
ts,
|
||||||
|
idx,
|
||||||
|
opn,
|
||||||
|
cls,
|
||||||
|
) in zip(
|
||||||
|
valid_timestamps,
|
||||||
|
indices_arr,
|
||||||
|
opens_arr,
|
||||||
|
closes_arr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
profiler('`time_to_row` creation complete!')
|
||||||
|
|
||||||
|
profiler(f'built timestamp lookup for {len(timestamps)} times')
|
||||||
|
|
||||||
|
# build gap_specs from rect+arrow specs
|
||||||
|
for i in range(n_gaps):
|
||||||
|
gap_spec: dict = {}
|
||||||
|
|
||||||
|
# get rect spec for this gap
|
||||||
|
if i < len(rect_specs):
|
||||||
|
rect_spec: dict = rect_specs[i].copy()
|
||||||
|
start_time = rect_spec.get('start_time')
|
||||||
|
end_time = rect_spec.get('end_time')
|
||||||
|
|
||||||
|
if (
|
||||||
|
start_time is not None
|
||||||
|
and end_time is not None
|
||||||
|
):
|
||||||
|
# lookup from pre-built mapping
|
||||||
|
start_row = time_to_row.get(start_time)
|
||||||
|
end_row = time_to_row.get(end_time)
|
||||||
|
|
||||||
|
if (
|
||||||
|
start_row is None
|
||||||
|
or end_row is None
|
||||||
|
):
|
||||||
|
log.warning(
|
||||||
|
f'Timestamp lookup failed for '
|
||||||
|
f'gap[{i}], skipping'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_idx = start_row['index']
|
||||||
|
end_idx = end_row['index']
|
||||||
|
start_close = start_row['close']
|
||||||
|
end_open = end_row['open']
|
||||||
|
|
||||||
|
from_idx: float = 0.16 - 0.06
|
||||||
|
gap_spec['start_pos'] = (
|
||||||
|
start_idx + 1 - from_idx,
|
||||||
|
start_close,
|
||||||
|
)
|
||||||
|
gap_spec['end_pos'] = (
|
||||||
|
end_idx + from_idx,
|
||||||
|
end_open,
|
||||||
|
)
|
||||||
|
gap_spec['start_time'] = start_time
|
||||||
|
gap_spec['end_time'] = end_time
|
||||||
|
gap_spec['color'] = rect_spec.get(
|
||||||
|
'color',
|
||||||
|
'dad_blue',
|
||||||
|
)
|
||||||
|
|
||||||
|
# get arrow spec for this gap
|
||||||
|
if i < len(arrow_specs):
|
||||||
|
arrow_spec: dict = arrow_specs[i].copy()
|
||||||
|
x: float = float(arrow_spec.get('x', 0))
|
||||||
|
y: float = float(arrow_spec.get('y', 0))
|
||||||
|
time_val: float|None = arrow_spec.get('time')
|
||||||
|
|
||||||
|
# timestamp-based index lookup (only for x, NOT y!)
|
||||||
|
# y is already set to the PREVIOUS bar's close
|
||||||
|
if time_val is not None:
|
||||||
|
arrow_row = time_to_row.get(time_val)
|
||||||
|
if arrow_row is not None:
|
||||||
|
x = arrow_row['index']
|
||||||
|
# NOTE: do NOT update y! it's the
|
||||||
|
# previous bar's close, not current
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
f'Arrow timestamp {time_val} not '
|
||||||
|
f'found for gap[{i}], using x={x}'
|
||||||
|
)
|
||||||
|
|
||||||
|
gap_spec['arrow_x'] = x
|
||||||
|
gap_spec['arrow_y'] = y
|
||||||
|
gap_spec['time'] = time_val
|
||||||
|
gap_spec['pointing'] = arrow_spec.get(
|
||||||
|
'pointing',
|
||||||
|
'down',
|
||||||
|
)
|
||||||
|
gap_spec['alpha'] = arrow_spec.get('alpha', 169)
|
||||||
|
|
||||||
|
gap_specs.append(gap_spec)
|
||||||
|
|
||||||
|
profiler(f'built {len(gap_specs)} gap_specs')
|
||||||
|
|
||||||
|
# create single GapAnnotations item for all gaps
|
||||||
|
if gap_specs:
|
||||||
|
gaps_item = GapAnnotations(
|
||||||
|
gap_specs=gap_specs,
|
||||||
|
array=arr,
|
||||||
|
color=gap_specs[0].get('color', 'dad_blue'),
|
||||||
|
alpha=gap_specs[0].get('alpha', 169),
|
||||||
|
arrow_size=10.0,
|
||||||
|
fqme=fqme,
|
||||||
|
timeframe=timeframe,
|
||||||
|
)
|
||||||
|
chart.plotItem.addItem(gaps_item)
|
||||||
|
|
||||||
|
# register single item for repositioning
|
||||||
|
aid: int = id(gaps_item)
|
||||||
|
annots[aid] = gaps_item
|
||||||
|
aids_set.add(aid)
|
||||||
|
result['rects'].append(aid)
|
||||||
|
profiler(
|
||||||
|
f'created GapAnnotations item for {len(gap_specs)} '
|
||||||
|
f'gaps'
|
||||||
|
)
|
||||||
|
|
||||||
|
# A/B comparison: optionally create individual arrows
|
||||||
|
# alongside batch for visual comparison
|
||||||
|
if show_individual_arrows:
|
||||||
|
godw = chart.linked.godwidget
|
||||||
|
arrows: ArrowEditor = ArrowEditor(godw=godw)
|
||||||
|
for i, spec in enumerate(gap_specs):
|
||||||
|
if 'arrow_x' not in spec:
|
||||||
|
continue
|
||||||
|
|
||||||
|
aid_str: str = str(uuid4())
|
||||||
|
arrow: pg.ArrowItem = arrows.add(
|
||||||
|
plot=chart.plotItem,
|
||||||
|
uid=aid_str,
|
||||||
|
x=spec['arrow_x'],
|
||||||
|
y=spec['arrow_y'],
|
||||||
|
pointing=spec['pointing'],
|
||||||
|
color='bracket', # different color
|
||||||
|
alpha=spec.get('alpha', 169),
|
||||||
|
headLen=10.0,
|
||||||
|
headWidth=2.222,
|
||||||
|
pxMode=True,
|
||||||
|
)
|
||||||
|
arrow._abs_x = spec['arrow_x']
|
||||||
|
arrow._abs_y = spec['arrow_y']
|
||||||
|
|
||||||
|
annots[aid_str] = arrow
|
||||||
|
_editors[aid_str] = arrows
|
||||||
|
aids_set.add(aid_str)
|
||||||
|
result['arrows'].append(aid_str)
|
||||||
|
|
||||||
|
profiler(
|
||||||
|
f'created {len(gap_specs)} individual arrows '
|
||||||
|
f'for comparison'
|
||||||
|
)
|
||||||
|
|
||||||
|
# handle text items separately (less common, keep
|
||||||
|
# individual items)
|
||||||
|
n_texts: int = 0
|
||||||
|
for text_spec in text_specs:
|
||||||
|
kwargs: dict = text_spec.copy()
|
||||||
|
text: str = kwargs.pop('text')
|
||||||
|
x: float = float(kwargs.pop('x'))
|
||||||
|
y: float = float(kwargs.pop('y'))
|
||||||
|
time_val: float|None = kwargs.pop('time', None)
|
||||||
|
|
||||||
|
# timestamp-based index lookup
|
||||||
|
if time_val is not None:
|
||||||
|
matches = arr[arr['time'] == time_val]
|
||||||
|
if len(matches) > 0:
|
||||||
|
x = float(matches[0]['index'])
|
||||||
|
y = float(matches[0]['close'])
|
||||||
|
|
||||||
|
color = kwargs.pop('color', 'dad_blue')
|
||||||
|
anchor = kwargs.pop('anchor', (0, 1))
|
||||||
|
font_size = kwargs.pop('font_size', None)
|
||||||
|
|
||||||
|
text_item: pg.TextItem = pg.TextItem(
|
||||||
|
text,
|
||||||
|
color=hcolor(color),
|
||||||
|
anchor=anchor,
|
||||||
|
)
|
||||||
|
|
||||||
|
if font_size is None:
|
||||||
|
from ._style import get_fonts
|
||||||
|
font, font_small = get_fonts()
|
||||||
|
font_size = font_small.px_size - 1
|
||||||
|
|
||||||
|
qfont: QFont = text_item.textItem.font()
|
||||||
|
qfont.setPixelSize(font_size)
|
||||||
|
text_item.setFont(qfont)
|
||||||
|
|
||||||
|
text_item.setPos(float(x), float(y))
|
||||||
|
chart.plotItem.addItem(text_item)
|
||||||
|
|
||||||
|
text_item._abs_x = float(x)
|
||||||
|
text_item._abs_y = float(y)
|
||||||
|
|
||||||
|
aid: str = str(uuid4())
|
||||||
|
annots[aid] = text_item
|
||||||
|
aids_set.add(aid)
|
||||||
|
result['texts'].append(aid)
|
||||||
|
n_texts += 1
|
||||||
|
|
||||||
|
profiler(
|
||||||
|
f'created text annotations: {n_texts} texts'
|
||||||
|
)
|
||||||
|
profiler.finish()
|
||||||
|
|
||||||
|
await annot_req_stream.send(result)
|
||||||
|
|
||||||
case {
|
case {
|
||||||
'cmd': 'remove',
|
'cmd': 'remove',
|
||||||
'aid': int(aid)|str(aid),
|
'aid': int(aid)|str(aid),
|
||||||
|
|
@ -471,10 +828,26 @@ async def serve_rc_annots(
|
||||||
# XXX: reposition all annotations to ensure they
|
# XXX: reposition all annotations to ensure they
|
||||||
# stay aligned with viz data after reset (eg during
|
# stay aligned with viz data after reset (eg during
|
||||||
# backfill when abs-index range changes)
|
# backfill when abs-index range changes)
|
||||||
|
chart: ChartPlotWidget = {
|
||||||
|
60: ds.hist_chart,
|
||||||
|
1: ds.chart,
|
||||||
|
}[timeframe]
|
||||||
|
viz: Viz = chart.get_viz(fqme)
|
||||||
|
arr = viz.shm.array
|
||||||
|
|
||||||
n_repositioned: int = 0
|
n_repositioned: int = 0
|
||||||
for aid, annot in annots.items():
|
for aid, annot in annots.items():
|
||||||
|
# GapAnnotations batch items have .reposition()
|
||||||
|
if hasattr(annot, 'reposition'):
|
||||||
|
annot.reposition(
|
||||||
|
array=arr,
|
||||||
|
fqme=fqme,
|
||||||
|
timeframe=timeframe,
|
||||||
|
)
|
||||||
|
n_repositioned += 1
|
||||||
|
|
||||||
# arrows and text items use abs x,y coords
|
# arrows and text items use abs x,y coords
|
||||||
if (
|
elif (
|
||||||
hasattr(annot, '_abs_x')
|
hasattr(annot, '_abs_x')
|
||||||
and
|
and
|
||||||
hasattr(annot, '_abs_y')
|
hasattr(annot, '_abs_y')
|
||||||
|
|
@ -539,12 +912,21 @@ async def remote_annotate(
|
||||||
finally:
|
finally:
|
||||||
# ensure all annots for this connection are deleted
|
# ensure all annots for this connection are deleted
|
||||||
# on any final teardown
|
# on any final teardown
|
||||||
|
profiler = Profiler(
|
||||||
|
msg=f'Annotation teardown for ctx {ctx.cid}',
|
||||||
|
disabled=False,
|
||||||
|
ms_threshold=0.0,
|
||||||
|
)
|
||||||
(_ctx, aids) = _ctxs[ctx.cid]
|
(_ctx, aids) = _ctxs[ctx.cid]
|
||||||
assert _ctx is ctx
|
assert _ctx is ctx
|
||||||
|
profiler(f'got {len(aids)} aids to remove')
|
||||||
|
|
||||||
for aid in aids:
|
for aid in aids:
|
||||||
annot: QGraphicsItem = _annots[aid]
|
annot: QGraphicsItem = _annots[aid]
|
||||||
assert rm_annot(annot)
|
assert rm_annot(annot)
|
||||||
|
|
||||||
|
profiler(f'removed all {len(aids)} annotations')
|
||||||
|
|
||||||
|
|
||||||
class AnnotCtl(Struct):
|
class AnnotCtl(Struct):
|
||||||
'''
|
'''
|
||||||
|
|
@ -746,6 +1128,64 @@ class AnnotCtl(Struct):
|
||||||
)
|
)
|
||||||
return aid
|
return aid
|
||||||
|
|
||||||
|
async def add_batch(
|
||||||
|
self,
|
||||||
|
fqme: str,
|
||||||
|
timeframe: float,
|
||||||
|
rects: list[dict]|None = None,
|
||||||
|
arrows: list[dict]|None = None,
|
||||||
|
texts: list[dict]|None = None,
|
||||||
|
show_individual_arrows: bool = False,
|
||||||
|
|
||||||
|
from_acm: bool = False,
|
||||||
|
|
||||||
|
) -> dict[str, list[int]]:
|
||||||
|
'''
|
||||||
|
Batch submit multiple annotations in single IPC msg for
|
||||||
|
much faster remote annotation vs. per-annot round-trips.
|
||||||
|
|
||||||
|
Returns dict of annotation IDs:
|
||||||
|
{
|
||||||
|
'rects': [aid1, aid2, ...],
|
||||||
|
'arrows': [aid3, aid4, ...],
|
||||||
|
'texts': [aid5, aid6, ...],
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
ipc: MsgStream = self._get_ipc(fqme)
|
||||||
|
with trio.fail_after(10):
|
||||||
|
await ipc.send({
|
||||||
|
'fqme': fqme,
|
||||||
|
'cmd': 'batch',
|
||||||
|
'timeframe': timeframe,
|
||||||
|
'rects': rects or [],
|
||||||
|
'arrows': arrows or [],
|
||||||
|
'texts': texts or [],
|
||||||
|
'show_individual_arrows': show_individual_arrows,
|
||||||
|
})
|
||||||
|
result: dict = await ipc.receive()
|
||||||
|
match result:
|
||||||
|
case {'error': str(msg)}:
|
||||||
|
log.error(msg)
|
||||||
|
return {
|
||||||
|
'rects': [],
|
||||||
|
'arrows': [],
|
||||||
|
'texts': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# register all AIDs with their IPC streams
|
||||||
|
for aid_list in result.values():
|
||||||
|
for aid in aid_list:
|
||||||
|
self._ipcs[aid] = ipc
|
||||||
|
if not from_acm:
|
||||||
|
self._annot_stack.push_async_callback(
|
||||||
|
partial(
|
||||||
|
self.remove,
|
||||||
|
aid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
async def add_text(
|
async def add_text(
|
||||||
self,
|
self,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
@ -881,3 +1321,14 @@ async def open_annot_ctl(
|
||||||
_annot_stack=annots_stack,
|
_annot_stack=annots_stack,
|
||||||
)
|
)
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
# client exited, measure teardown time
|
||||||
|
teardown_profiler = Profiler(
|
||||||
|
msg='Client AnnotCtl teardown',
|
||||||
|
disabled=False,
|
||||||
|
ms_threshold=0.0,
|
||||||
|
)
|
||||||
|
teardown_profiler('exiting annots_stack')
|
||||||
|
|
||||||
|
teardown_profiler('annots_stack exited')
|
||||||
|
teardown_profiler('exiting gather_contexts')
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,10 @@ class GodWidget(QWidget):
|
||||||
getattr(widget, 'on_resize')
|
getattr(widget, 'on_resize')
|
||||||
self._widgets[widget.mode_name] = widget
|
self._widgets[widget.mode_name] = widget
|
||||||
|
|
||||||
def on_win_resize(self, event: QtCore.QEvent) -> None:
|
def on_win_resize(
|
||||||
|
self,
|
||||||
|
event: QtCore.QEvent,
|
||||||
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Top level god widget handler from window (the real yaweh) resize
|
Top level god widget handler from window (the real yaweh) resize
|
||||||
events such that any registered widgets which wish to be
|
events such that any registered widgets which wish to be
|
||||||
|
|
@ -315,7 +318,10 @@ class GodWidget(QWidget):
|
||||||
|
|
||||||
self._resizing = True
|
self._resizing = True
|
||||||
|
|
||||||
log.info('God widget resize')
|
log.debug(
|
||||||
|
f'God widget resize\n'
|
||||||
|
f'{event}\n'
|
||||||
|
)
|
||||||
for name, widget in self._widgets.items():
|
for name, widget in self._widgets.items():
|
||||||
widget.on_resize()
|
widget.on_resize()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -255,8 +255,16 @@ class MainWindow(QMainWindow):
|
||||||
current: QWidget,
|
current: QWidget,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
'''
|
||||||
|
Focus handler.
|
||||||
|
|
||||||
log.info(f'widget focus changed from {last} -> {current}')
|
For now updates the "current mode" name.
|
||||||
|
|
||||||
|
'''
|
||||||
|
log.debug(
|
||||||
|
f'widget focus changed from,\n'
|
||||||
|
f'{last} -> {current}'
|
||||||
|
)
|
||||||
|
|
||||||
if current is not None:
|
if current is not None:
|
||||||
# cursor left window?
|
# cursor left window?
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ def chart(
|
||||||
return
|
return
|
||||||
|
|
||||||
# global opts
|
# global opts
|
||||||
brokernames = config['brokers']
|
# brokernames: list[str] = config['brokers']
|
||||||
brokermods = config['brokermods']
|
brokermods = config['brokermods']
|
||||||
assert brokermods
|
assert brokermods
|
||||||
tractorloglevel = config['tractorloglevel']
|
tractorloglevel = config['tractorloglevel']
|
||||||
|
|
@ -216,6 +216,7 @@ def chart(
|
||||||
layers['tcp']['port'],
|
layers['tcp']['port'],
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# breakpoint()
|
||||||
from tractor.devx import maybe_open_crash_handler
|
from tractor.devx import maybe_open_crash_handler
|
||||||
pdb: bool = config['pdb']
|
pdb: bool = config['pdb']
|
||||||
with maybe_open_crash_handler(pdb=pdb):
|
with maybe_open_crash_handler(pdb=pdb):
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ from ._style import _font
|
||||||
from ._forms import open_form_input_handling
|
from ._forms import open_form_input_handling
|
||||||
from ._notify import notify_from_ems_status_msg
|
from ._notify import notify_from_ems_status_msg
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._chart import (
|
from ._chart import (
|
||||||
ChartPlotWidget,
|
ChartPlotWidget,
|
||||||
|
|
@ -436,7 +435,7 @@ class OrderMode:
|
||||||
lines=lines,
|
lines=lines,
|
||||||
last_status_close=self.multistatus.open_status(
|
last_status_close=self.multistatus.open_status(
|
||||||
f'submitting {order.exec_mode}-{order.action}',
|
f'submitting {order.exec_mode}-{order.action}',
|
||||||
final_msg=f'submitted {order.exec_mode}-{order.action}',
|
# final_msg=f'submitted {order.exec_mode}-{order.action}',
|
||||||
clear_on_next=True,
|
clear_on_next=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -514,13 +513,14 @@ class OrderMode:
|
||||||
def on_submit(
|
def on_submit(
|
||||||
self,
|
self,
|
||||||
uuid: str,
|
uuid: str,
|
||||||
order: Order | None = None,
|
order: Order|None = None,
|
||||||
|
|
||||||
) -> Dialog | None:
|
) -> Dialog|None:
|
||||||
'''
|
'''
|
||||||
Order submitted status event handler.
|
Order submitted status event handler.
|
||||||
|
|
||||||
Commit the order line and registered order uuid, store ack time stamp.
|
Commit the order line and registered order uuid, store ack
|
||||||
|
time stamp.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
lines = self.lines.commit_line(uuid)
|
lines = self.lines.commit_line(uuid)
|
||||||
|
|
@ -528,7 +528,7 @@ class OrderMode:
|
||||||
# a submission is the start of a new order dialog
|
# a submission is the start of a new order dialog
|
||||||
dialog = self.dialogs[uuid]
|
dialog = self.dialogs[uuid]
|
||||||
dialog.lines = lines
|
dialog.lines = lines
|
||||||
cls: Callable | None = dialog.last_status_close
|
cls: Callable|None = dialog.last_status_close
|
||||||
if cls:
|
if cls:
|
||||||
cls()
|
cls()
|
||||||
|
|
||||||
|
|
@ -658,7 +658,7 @@ class OrderMode:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def cancel_orders_under_cursor(self) -> list[str]:
|
def cancel_orders_under_cursor(self) -> list[Dialog]:
|
||||||
return self.cancel_orders(
|
return self.cancel_orders(
|
||||||
self.oids_from_lines(
|
self.oids_from_lines(
|
||||||
self.lines.lines_under_cursor()
|
self.lines.lines_under_cursor()
|
||||||
|
|
@ -687,24 +687,28 @@ class OrderMode:
|
||||||
self,
|
self,
|
||||||
oids: list[str],
|
oids: list[str],
|
||||||
|
|
||||||
) -> None:
|
) -> list[Dialog]:
|
||||||
'''
|
'''
|
||||||
Cancel all orders from a list of order ids: `oids`.
|
Cancel all orders from a list of order ids: `oids`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
key = self.multistatus.open_status(
|
# key = self.multistatus.open_status(
|
||||||
f'cancelling {len(oids)} orders',
|
# f'cancelling {len(oids)} orders',
|
||||||
final_msg=f'cancelled orders:\n{oids}',
|
# final_msg=f'cancelled orders:\n{oids}',
|
||||||
group_key=True
|
# group_key=True
|
||||||
)
|
# )
|
||||||
|
dialogs: list[Dialog] = []
|
||||||
for oid in oids:
|
for oid in oids:
|
||||||
if dialog := self.dialogs.get(oid):
|
if dialog := self.dialogs.get(oid):
|
||||||
self.client.cancel_nowait(uuid=oid)
|
self.client.cancel_nowait(uuid=oid)
|
||||||
cancel_status_close = self.multistatus.open_status(
|
# cancel_status_close = self.multistatus.open_status(
|
||||||
f'cancelling order {oid}',
|
# f'cancelling order {oid}',
|
||||||
group_key=key,
|
# group_key=key,
|
||||||
)
|
# )
|
||||||
dialog.last_status_close = cancel_status_close
|
# dialog.last_status_close = cancel_status_close
|
||||||
|
dialogs.append(dialog)
|
||||||
|
|
||||||
|
return dialogs
|
||||||
|
|
||||||
def cancel_all_orders(self) -> None:
|
def cancel_all_orders(self) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -776,7 +780,6 @@ class OrderMode:
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def open_order_mode(
|
async def open_order_mode(
|
||||||
|
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
godw: GodWidget,
|
godw: GodWidget,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ dependencies = [
|
||||||
"trio-typing>=0.10.0",
|
"trio-typing>=0.10.0",
|
||||||
"numba>=0.61.0",
|
"numba>=0.61.0",
|
||||||
"pyvnc",
|
"pyvnc",
|
||||||
|
"exchange-calendars>=4.13.1",
|
||||||
]
|
]
|
||||||
# ------ dependencies ------
|
# ------ dependencies ------
|
||||||
# NOTE, by default we ship only a "headless" deps set bc
|
# NOTE, by default we ship only a "headless" deps set bc
|
||||||
|
|
@ -103,7 +104,7 @@ default-groups = [
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
uis = [
|
uis = [
|
||||||
"pyqtgraph",
|
"pyqtgraph >= 0.14.0",
|
||||||
"qdarkstyle >=3.0.2, <4.0.0",
|
"qdarkstyle >=3.0.2, <4.0.0",
|
||||||
"pyqt6 >=6.7.0, <7.0.0",
|
"pyqt6 >=6.7.0, <7.0.0",
|
||||||
|
|
||||||
|
|
@ -190,9 +191,12 @@ include = ["piker"]
|
||||||
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" }
|
|
||||||
tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" }
|
tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" }
|
||||||
pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
||||||
|
# pyqtgraph = { git = "https://github.com/pyqtgraph/pyqtgraph.git", branch = 'master' }
|
||||||
|
# pyqtgraph = { path = '../pyqtgraph', editable = true }
|
||||||
|
# ?TODO, resync our fork?
|
||||||
|
# pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" }
|
||||||
|
|
||||||
# to get fancy next-cmd/suggestion feats prior to 0.22.2 B)
|
# to get fancy next-cmd/suggestion feats prior to 0.22.2 B)
|
||||||
# https://github.com/xonsh/xonsh/pull/6037
|
# https://github.com/xonsh/xonsh/pull/6037
|
||||||
|
|
|
||||||
132
uv.lock
132
uv.lock
|
|
@ -2,8 +2,12 @@ version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.14'",
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||||
"python_full_version < '3.14'",
|
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
"python_full_version < '3.14' and sys_platform == 'win32'",
|
||||||
|
"python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -416,6 +420,23 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exchange-calendars"
|
||||||
|
version = "4.13.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "korean-lunar-calendar" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "pandas" },
|
||||||
|
{ name = "pyluach" },
|
||||||
|
{ name = "toolz" },
|
||||||
|
{ name = "tzdata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/fd/1bda66b3c2fefbf54b8cf765c9d8001b12654b5a897a21b0c6c9f55de5e3/exchange_calendars-4.13.1.tar.gz", hash = "sha256:42a4c7296da1f71b9625c668c9b3359cf5de4a2ffca28842b230e062bb4961ba", size = 4119843, upload-time = "2026-02-05T00:15:03.947Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/b7/fffe7d5a6da6be10b43be96640f31d4191e746de66b046cc1a6ea5fc4f26/exchange_calendars-4.13.1-py3-none-any.whl", hash = "sha256:cf39d2128a4da3ac253283f91ab63d79930a68196a3aac811091a4e38b6cbe49", size = 211538, upload-time = "2026-02-05T00:15:05.694Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "frozenlist"
|
name = "frozenlist"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
|
@ -659,6 +680,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/d3/c3db0b92a0ff39c3e08f168cd382c24bf021d4a96fc89b47a3e55294f883/keysymdef-1.2.0-py2.py3-none-any.whl", hash = "sha256:19a5c2263a861f3ff884a1f58e2b4f7efa319ffc9d11f9ba8e20129babc31a9e", size = 20146, upload-time = "2023-02-25T00:22:36.318Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/d3/c3db0b92a0ff39c3e08f168cd382c24bf021d4a96fc89b47a3e55294f883/keysymdef-1.2.0-py2.py3-none-any.whl", hash = "sha256:19a5c2263a861f3ff884a1f58e2b4f7efa319ffc9d11f9ba8e20129babc31a9e", size = 20146, upload-time = "2023-02-25T00:22:36.318Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "korean-lunar-calendar"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/93/a0bd2bd53ab19330e83ecc5652b7774ae86fd2fee19bc05ad220cf9db08b/korean_lunar_calendar-0.3.1.tar.gz", hash = "sha256:eb2c485124a061016926bdea6d89efdf9b9fdbf16db55895b6cf1e5bec17b857", size = 9877, upload-time = "2022-09-16T10:53:25.713Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/96/30f3fe51b336bb6da4714f4fdad7bbdce8f13af79af2eb75e22908f3f9f4/korean_lunar_calendar-0.3.1-py3-none-any.whl", hash = "sha256:392757135c492c4f42a604e6038042953c35c6f449dda5f27e3f86a7f9c943e5", size = 9033, upload-time = "2022-09-16T10:53:23.771Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "llvmlite"
|
name = "llvmlite"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
|
|
@ -953,6 +983,58 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdbp"
|
name = "pdbp"
|
||||||
version = "1.8.2"
|
version = "1.8.2"
|
||||||
|
|
@ -1023,6 +1105,7 @@ dependencies = [
|
||||||
{ name = "colorama" },
|
{ name = "colorama" },
|
||||||
{ name = "colorlog" },
|
{ name = "colorlog" },
|
||||||
{ name = "cryptofeed" },
|
{ name = "cryptofeed" },
|
||||||
|
{ name = "exchange-calendars" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "ib-insync" },
|
{ name = "ib-insync" },
|
||||||
{ name = "msgspec" },
|
{ name = "msgspec" },
|
||||||
|
|
@ -1098,6 +1181,7 @@ requires-dist = [
|
||||||
{ name = "colorama", specifier = ">=0.4.6,<0.5.0" },
|
{ name = "colorama", specifier = ">=0.4.6,<0.5.0" },
|
||||||
{ name = "colorlog", specifier = ">=6.7.0,<7.0.0" },
|
{ name = "colorlog", specifier = ">=6.7.0,<7.0.0" },
|
||||||
{ name = "cryptofeed", specifier = ">=2.4.0,<3.0.0" },
|
{ 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 = "httpx", specifier = ">=0.27.0,<0.28.0" },
|
||||||
{ name = "ib-insync", specifier = ">=0.9.86,<0.10.0" },
|
{ name = "ib-insync", specifier = ">=0.9.86,<0.10.0" },
|
||||||
{ name = "msgspec", specifier = ">=0.19.0,<0.20" },
|
{ name = "msgspec", specifier = ">=0.19.0,<0.20" },
|
||||||
|
|
@ -1134,7 +1218,7 @@ dev = [
|
||||||
{ name = "prompt-toolkit", specifier = "==3.0.40" },
|
{ name = "prompt-toolkit", specifier = "==3.0.40" },
|
||||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||||
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
||||||
{ name = "pyqtgraph", git = "https://github.com/pikers/pyqtgraph.git" },
|
{ name = "pyqtgraph", specifier = ">=0.14.0" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
||||||
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
||||||
|
|
@ -1152,18 +1236,18 @@ repl = [
|
||||||
testing = [{ name = "pytest" }]
|
testing = [{ name = "pytest" }]
|
||||||
uis = [
|
uis = [
|
||||||
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
||||||
{ name = "pyqtgraph", git = "https://github.com/pikers/pyqtgraph.git" },
|
{ name = "pyqtgraph", specifier = ">=0.14.0" },
|
||||||
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
||||||
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.5.1"
|
version = "4.6.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/474d0a8508029286b905622e6929470fb84337cfa08f9d09fbb624515249/platformdirs-4.6.0.tar.gz", hash = "sha256:4a13c2db1071e5846c3b3e04e5b095c0de36b2a24be9a3bc0145ca66fce4e328", size = 23433, upload-time = "2026-02-12T14:36:21.288Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/10/1b0dcf51427326f70e50d98df21b18c228117a743a1fc515a42f8dc7d342/platformdirs-4.6.0-py3-none-any.whl", hash = "sha256:dd7f808d828e1764a22ebff09e60f175ee3c41876606a6132a688d809c7c9c73", size = 19549, upload-time = "2026-02-12T14:36:19.743Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1446,6 +1530,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyluach"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/11/11/42568c1568a75f8803c59f26d29af01a0890352b7a8e03d41ecda8bfb5dd/pyluach-2.3.0.tar.gz", hash = "sha256:ec6e30669d1df50c9ca160486da44a8195bb4c7a5d3d533990d0c5b03accd281", size = 26910, upload-time = "2025-09-09T20:24:39.651Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/c8/f96208ade3ca4c23b372497d0788bcf0f2e0ff4310e5ee693366bc33fdf0/pyluach-2.3.0-py3-none-any.whl", hash = "sha256:4497b731aef59508b079dbf5f00bc5bf4329ac45090a6cd37b5a83756f0e69ab", size = 25914, upload-time = "2025-09-09T20:24:37.831Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyperclip"
|
name = "pyperclip"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
|
@ -1510,11 +1603,15 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyqtgraph"
|
name = "pyqtgraph"
|
||||||
version = "0.12.3"
|
version = "0.14.0"
|
||||||
source = { git = "https://github.com/pikers/pyqtgraph.git#373f9561ea8ec4fef9b4e8bdcdd4bbf372dd6512" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "colorama" },
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
]
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyreadline3"
|
name = "pyreadline3"
|
||||||
|
|
@ -1865,6 +1962,15 @@ name = "tomlkit"
|
||||||
version = "0.11.8"
|
version = "0.11.8"
|
||||||
source = { git = "https://github.com/pikers/tomlkit.git?branch=piker_pin#8e0239a766e96739da700cd87cc00b48dbe7451f" }
|
source = { git = "https://github.com/pikers/tomlkit.git?branch=piker_pin#8e0239a766e96739da700cd87cc00b48dbe7451f" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toolz"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tractor"
|
name = "tractor"
|
||||||
version = "0.1.0a6.dev0"
|
version = "0.1.0a6.dev0"
|
||||||
|
|
@ -1904,7 +2010,7 @@ dev = [
|
||||||
{ name = "pytest", specifier = ">=8.3.5" },
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||||
{ name = "xonsh", specifier = ">=0.19.2" },
|
{ name = "xonsh", specifier = ">=0.22.2" },
|
||||||
]
|
]
|
||||||
devx = [
|
devx = [
|
||||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||||
|
|
@ -1916,7 +2022,7 @@ repl = [
|
||||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||||
{ name = "psutil", specifier = ">=7.0.0" },
|
{ name = "psutil", specifier = ">=7.0.0" },
|
||||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||||
{ name = "xonsh", specifier = ">=0.19.2" },
|
{ name = "xonsh", specifier = ">=0.22.2" },
|
||||||
]
|
]
|
||||||
testing = [
|
testing = [
|
||||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||||
|
|
@ -2162,8 +2268,8 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xonsh"
|
name = "xonsh"
|
||||||
version = "0.22.1"
|
version = "0.22.3"
|
||||||
source = { git = "https://github.com/xonsh/xonsh.git?branch=main#336658ff0919f8d7bb96d581136d37d470a8fe99" }
|
source = { git = "https://github.com/xonsh/xonsh.git?branch=main#b446946fd94c3913e002318db1d1b41ee4fa1f9a" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yapic-json"
|
name = "yapic-json"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue