Compare commits

...

24 Commits

Author SHA1 Message Date
Gud Boi 458467bd5c Better doc-strings n styling in `piker.cli` eps
Add comprehensive docstrings to the top-level CLI endpoints and helpers,
explaining the purpose and structure of each (sub)command.

Deats,
- add detailed docstring to `pikerd()` explaining its role as the
  root service-actor/daemon supervisor.
- add docstring to `cli()` noting it's the root endpoint generally
  requiring a sub-cmd input.
- add extensive docstring to `services()` explaining the daemon naming
  conventions and listing a few current/common service actors.
- add docstring to `_load_clis()` explaining dynamic CLI loading.

Stylin,
- add multiline style to `and not maddrs` conditional in
  `load_trans_eps()`.
- drop commented-out `--tsdb` and `--es` click options from
  `pikerd()`, they're more or less obsolete given `nativedb`.
- add type annots where obviously handy.
- add TODO comment about UDS support in `services()`.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:29:40 -05:00
Gud Boi 56308e1393 Bump `platformdirs` version in lock file 2026-02-22 18:18:06 -05:00
Gud Boi 01dcdded64 Enable console logging in `.accounting` on import
Enable `get_console_log()` at `.accounting.__init__` import-time
to ensure console output is available whenever the subsystem is
used by `.clearing` or other code.

Deats,
- uncomment and complete `get_console_log()` call in
  `.accounting.__init__` with default `level='warning'` and
  `name=__name__`.
- update comment explaining rationale: better to enable on import
  since namely used by `.clearing` subsystem.

Also,
- change `piker.calc` import to relative `.calc` in
  `.accounting.__init__`.
- drop unused `get_console_log` import from `.accounting._pos`.
- add `log = get_logger(name=__name__)` to `.accounting.cli`.
- change `get_logger(loglevel)` -> `get_console_log()` in
  `.accounting.cli.sync()` with proper kwargs.
- add `get_console_log` import to `.accounting.cli`.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi d485b4778a Use `name=__name__` for logs throughout `.service`
Change all `.service` sub-modules to use `get_logger(name=__name__)`
for per-submod instances vs a shared `._util.log`.

Deats,
- import `get_logger()` and `get_console_log()` from top-level
  `piker.log` instead of `._util` for all.
- drop `log` and `get_console_log()` partial from `._util`.
- add `name=subsys` kwarg to `get_console_log()` call in
  `_actor_runtime.maybe_open_pikerd()`.
- add `name='piker.service'` to `get_console_log()` in
  `_ahab.open_ahabd()`.
- change default `loglevel` from `None` to `'cancel'` in
  `_ahab.open_ahabd()`.
- add sanity check: `assert log.name == 'piker.service'` in
  `_daemon.maybe_spawn_daemon()`.
- change `print()` -> `log.info()` in `_registry.find_service()`.
- drop stray `from piker.service._util import log` import in
  `brokers._daemon.spawn_brokerd()`.

Styling/cleanups,
- drop blank lines from various fn sigs.
- do more sin-ws union type annots.
- add more multiline style to `or` expressions in `_actor_runtime` and
  `_registry`.
- update `._util` docstring with TODO about `import`-time console
  log setup.
- add TODO comments in `_registry` about UDS registry support.
- use `.aid.uid` from actor in `_registry.open_registry()`.
- add intermediate var `reg_addrs` in `_registry.open_registry()` (bc
  i was tracing rtvs value issues in `tractor`).
- add `pformat` import to `.elastic` (code path is currently
  not used but figured might as well appease the linter..)

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 2b93d858aa Pass `loglevel` down through `.ui` graphics tasks
Add `loglevel` propagation to UI graphics tasks and sampler stream
opens to enable proper console logging in chart update loops. This
ensures the graphics and FSP subsystems receive the same loglevel
as their parent and/or sibling UI-actor tasks.

Deats,
- add `loglevel` param to `graphics_update_loop()` and
  `increment_history_view()` with default `'warning'`.
- pass `loglevel` to `open_sample_stream()` calls in both fns.
- use `partial()` to pass `loglevel` through to `nurse.start_soon()`
  calls in `display_symbol_data()` and `graphics_update_loop()`.

Also logging, doc-strs, and code-style tweaks,
- change `print()` -> `log.debug()` for hidden-chart and
  interaction-pause msgs in graphics loop.
- change `log.info()` -> `log.debug()` for resize events in
  `GodWidget` and `MainWindow`.
- add multiline style to resize log msg in `GodWidget`.
- add docstring to `MainWindow.on_focus_change()`.
- moar union type annot adjustments.
- switch to explicit kwarg `period_s=` for `open_sample_stream()`
  in `increment_history_view()`.
- multiline style for `names` list in `open_fsp_actor_cluster()`.
- change `count=2` -> `count=len(names)` in
  `open_fsp_actor_cluster()`.
- add TODO about using `.experimental` for cluster import (once that
  get's patched into upstream `tractor`).
- multiline style for `or` in `FspAdmin.start_engine_task()`.
- comment-out unused `brokernames` in `ui.cli.chart()`.
- add commented breakpoint in `ui.cli.chart()`.
- fix docstring style in `OrderMode.on_submit()`.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 8c7f3635b3 Pass `loglevel` to `cascade()` feed/sampler opens
Add `loglevel` param to both `maybe_open_feed()` and
`open_sample_stream()` calls in FSP engine's `cascade()` task to
ensure proper console log setup in downstream sampling tasks.

Deats,
- pass `loglevel=loglevel` to `maybe_open_feed()` call.
- pass `loglevel=loglevel` to `open_sample_stream()` call.

Also,
- switch to explicit kwargs: `fqmes=[fqme]` and `period_s=` for
  clarity and consistency with other callsites.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 760570c01b Pass `loglevel` down through `.data` callstack
Add `loglevel` param propagation across the data feed and sampling
subsystems to enable proper console log setup in downstream (distibuted)
subactor tasks. This ensures sampler and history-mgmt tasks receive the
same loglevel as their parent `.data.feed` tasks.

Deats,
- add `loglevel: str|None` param to `register_with_sampler()`,
  `maybe_open_samplerd()`, and `open_sample_stream()`.
- pass `loglevel` through to `get_console_log()` in
  `register_with_sampler()` with fallback to actor `loglevel`.
- use `partial()` in `allocate_persistent_feed()` to pass
  `loglevel` to `manage_history()` at task-start.
- add `loglevel` param to `manage_history()` with default
  `'warning'` and pass through to `open_sample_stream()` from there.
- capture `loglevel` var in `brokers.cli.search()` and pass to
  `symbol_search()` call.

Also,
- drop blank lines in fn sigs for consistency with piker style.
- add debug bp in `open_feed()` when `loglevel != 'info'`.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 36e6489a38 Enable console via `.clearing._ems.open_brokerd_dialog()`
Enable console logs for both `.clearing` and `.accounting` in
`open_brokerd_dialog()` and pass `loglevel` to all broker-backend
trade-dialog endpoints. This ensures all `open_trade_dialog()` will
receive the same level passed to the EMS, including the paper engine.

Deats,
- add `loglevel` param to `mk_paper_ep()` closure.
- pass `loglevel=loglevel` to all trade endpoint `open_context()`
  calls and `mk_paper_ep()` invocations.
- change default `loglevel` in `open_ems()` from `'error'` to
  `'warning'`.
- add `get_console_log()` calls for `'clearing'` and
  `'piker.accounting'` at top of `open_brokerd_dialog()` to ensure those
  dependent subsystems are console enabled given they're namely used by
  the `brokerd` trade-dialog ep tasks.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 6fc1484d23 Use `__name__` for loggers across most sub-mods
Change most sub-modules to use `get_logger(name=__name__)` for
per-leaf-module `log` instances vs previous subpkg-level/shared refs.

Primary changes,
- import `get_[console_]logger()` from top-level `piker.log` across leaf
  mods.
- change any `<subsys>._util.log` logger-instances as well (though this
  approach should no longer be used since it masks the endpoint module's
  emissions.

Also,
- add a defaulted `loglevel: str` param to all `open_trade_dialog()`
  endpoints, anticipating it being passed in by `.clearing`-engine.
- call `get_console_log(level=loglevel, name=__name__)` in each trade
  dialog ep to enable per-`brokerd`-backend console writing.
- drop `get_logger` from `.brokers.__all__` exports
- fix type annotations: `str|None` vs `str | None`
- add TODOs for,
  * comments in `._util` about multi-subsys logging
  * `.accounting.__init__` about console log setup

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 8cca4ae1ce Use `__name__` for loggers across `.ib` sub-mods
Change all `.ib` sub-modules to use `get_logger(name=__name__)`
for per-module logger instances vs shared `._util.log`.

Deats,
- change `._util` to use `__name__` vs literal string.
- change `.broker`, `.feed`, `.ledger`, `.symbols` to import
  `get_logger()` from top-level `.log` and call with `__name__`.
- drop `log` imports from `._util` in all affected mods.

Also,
- drop trailing comma in `.cli.services()` conditional for `loglevel`
  passthrough -> fixes an actual kwargs bug!!

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 1b50485b12 .data.feed: move `Flume` import to avoid cycle
Move `Flume` to `TYPE_CHECKING` and add runtime imports in
`allocate_persistent_feed()` + `open_feed()` to avoid cycle
with `.flows` mod.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 6c81d0b241 .fsp._engine: enable console logging in `cascade()`
Add console log setup with module name + multiline style for
desync warning msg.

Also,
- fix import: `Flume` from `.data.flows` vs `.data.feed`
- move `Feed` to `TYPE_CHECKING` block
- add TODO comment about `tractor._state` dict issue

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 00627e5bb5 Add order-cancel debugging and multiline kbd logs
Add verbose logging + error handling for order cancellation
hotkey path and multiline style for view-mode kb msgs.

Deats,
- add `Cursor.is_hovered()` to check hover state
- log warnings when no orders cancelled via <c> hotkey
- add try-except around `.cancel_orders_under_cursor()`
- log `cur._hovered` state in `.ui._lines` hover handlers
- change `Dialog.cancel_orders()` to return `list[Dialog]`
- fix import: `Flume` from `.data.flows` vs `.data.feed`
- comment-out multi-status msgs in order submit/cancel

Also,
- convert all multiline kbd `if` conditionals to use `and`
  on separate lines for consistency
- move `import tractor` to top of `._interaction`
- change `print()` to `log.debug()` in `LevelLine`
- fix type annotation spacing: `Callable|None` vs `Callable | None`

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 2a5de5e6ac .ui._app: enable console logging in `_async_main()`
Now we're actualy emitting colored-logs (again?), not sure how this got
borked but maybe it's due to `tractor.log`'s new changes?

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi fa0a3374d6 Move `GodWidget` to new `._widget` mod
Extract root-most widget to resolve (various) `.ui` import cycles
when the type is declared on `Struct`s..

Deats,
- flip to `from ._widget import GodWidget`.
- move `Feed` + `Flume` imports to TYPE_CHECKING in `._chart`
- drop unused `trio` import from `._chart`
- fix docstring typo: "datums```" -> "`datums``"
- change `print()` to `log.warning()` for global step msg

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 9dfedfc2f7 Point `.types.Struct` to `tractor.msg.pretty_struct`
Drop the local (and original) `Struct` impl from `piker.types` in favour
of `tractor`'s version now that it's been upstreamed.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 827bcde850 .brokers._daemon: enable `tractor` log in `brokerd`
Also,
- capture `Actor.loglevel` in `tll` var for reuse (when `loglevel` is
  null) and pass `bool`-ed as new `with_tractor_log`-flag.
- add `with_tractor_log=bool(tll)` to `.get_console_log()`
- add assertion check for logger name.
- comment-out `tractor.trionics.collapse_eg()` context for now, pretty
  sure we don't need it and it just ends up adding extra logging
  overhead for no good reason (warnings on various `trio` internal
  cancelled-maskings, etc).
- change type annotation: `str|None` vs `str | None`.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 0e4e5a5615 Auto-enable `tractor` logging when runtime active
Check for active `tractor` runtime via `.current_actor()` and use its
`.loglevel` to auto-enable `tractor`'s internal console logging when
`with_tractor_log` is not explicitly set.

Deats,
- add `tll` (tractor log level) var to capture level
- check `with_tractor_log is not False` first
- fallback to `maybe_actor.loglevel` if runtime exists
- only call `tractor.log.get_console_log()` if `tll` set
- add TODO comment about "log-spec" style config support

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-22 18:18:06 -05:00
Gud Boi 4f27789d6f Bump to latest `xonsh` release 2026-02-22 18:18:06 -05:00
Gud Boi 8dd1e24c39 Adjust `tractor.log` API compat
Update logging helpers to use new `tractor.log` API with `pkg_name=`
kwarg and add optional `tractor` "root logger" enabling.

Deats,
- change `piker.log.get_logger()` to use `pkg_name=` vs `_root_name=`.
- add `**tractor_log_kwargs` passthrough to both wrapper fns.
- add `with_tractor_log: bool` toggle to `.get_console_log()`.
- strip `'piker.'` prefix from logger names when present to avoid
  newly added `tractor.get_logger()` warnings.

Surroundingly,
- add `subsys` import to `.clearing._ems` for log name
- update all `get_console_log()` calls to use `level=` kwarg
- add assertion checks for logger names in `_setup_persistent_emsd()`

Additionally,,
- fix all type annotations: `str|None` vs `str | None`.
- add multiline style to conditional in `.cli.services()`.
- drop unused `Optional` import from `._actor_runtime`.
- drop a few "blank lines" in various function sigs.

Warning: this patch will require an equivalent dev-commit at the time of
writing in `tractor` itself, for now the `piker_pin` branch should be
sufficient to avoid breakage 🙏!

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code

k
2026-02-22 18:18:06 -05:00
Gud Boi e937b60ed6 Flip `.tsp._history` logger to explicit mod-name (again) 2026-02-22 18:16:58 -05:00
Gud Boi ed9c211b96 Adjust binance stale-bar detection to 2x tolerance
Change the stale-bar check in `.binance.feed` from `timeframe` to
`timeframe * 2` tolerance to avoid false-positive pauses when bars
are slightly delayed but still within acceptable bounds.

Styling,
- add walrus operator to capture `_time_step` for debugger
  inspection.
- add comment explaining the debug purpose of this check.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-20 16:36:44 -05:00
Gud Boi f1b27e9696 Replace assert with warn for no-gaps in `.storage.cli`
Change `assert aids` to a warning log when no history gaps are found
during `ldshm` gap detection; it is the **ideal case** OBVI. This avoids
crashing the CLI when gap detection finds no issues, which is actually
good news!

Bp

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-20 16:33:53 -05:00
Gud Boi 959d04024b .tsp._history: add gap detection in backfill loop
Add frame-gap detection when `frame_last_dt < end_dt_param` to
warn about potential venue closures or missing data during the
backfill loop in `start_backfill()`.

Deats,
- add `frame_last_dt < end_dt_param` check after frame recv
- log warnings with EST-converted timestamps for clarity
- add `await tractor.pause()` for REPL-investigation on gaps
- add TODO comment about venue closure hour checking
- capture `_until_was_none` walrus var for null-check clarity
- add `last_time` assertion for `time[-1] == next_end_dt`
- rename `_daterr` to `nodata` with `_nodata` capture

Also,
- import `pendulum.timezone` and create `est` tz instance
- change `get_logger()` import from `.data._util` to `.log`
- add parens around `(next_prepend_index - ln) < 0` check

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-20 16:32:34 -05:00
56 changed files with 1157 additions and 835 deletions

View File

@ -19,8 +19,10 @@
for tendiez.
'''
from ..log import get_logger
from piker.log import (
get_console_log,
get_logger,
)
from .calc import (
iter_by_dt,
)
@ -51,7 +53,17 @@ from ._allocate import (
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__ = [
'Account',
'Allocator',

View File

@ -60,12 +60,16 @@ from ..clearing._messages import (
BrokerdPosition,
)
from piker.types import Struct
from piker.log import get_logger
from piker.log import (
get_logger,
)
if TYPE_CHECKING:
from piker.data._symcache import SymbologyCache
log = get_logger(__name__)
log = get_logger(
name=__name__,
)
class Position(Struct):

View File

@ -21,7 +21,6 @@ CLI front end for trades ledger and position tracking management.
from __future__ import annotations
from pprint import pformat
from rich.console import Console
from rich.markdown import Markdown
import polars as pl
@ -29,7 +28,10 @@ import tractor
import trio
import typer
from ..log import get_logger
from piker.log import (
get_console_log,
get_logger,
)
from ..service import (
open_piker_runtime,
)
@ -45,6 +47,7 @@ from .calc import (
open_ledger_dfs,
)
log = get_logger(name=__name__)
ledger = typer.Typer()
@ -79,7 +82,10 @@ def sync(
"-l",
),
):
log = get_logger(loglevel)
log = get_console_log(
level=loglevel,
name=__name__,
)
console = Console()
pair: tuple[str, str]

View File

@ -25,15 +25,16 @@ from types import ModuleType
from tractor.trionics import maybe_open_context
from piker.log import (
get_logger,
)
from ._util import (
log,
BrokerError,
SymbolNotFound,
NoData,
DataUnavailable,
DataThrottle,
resproc,
get_logger,
)
__all__: list[str] = [
@ -43,7 +44,6 @@ __all__: list[str] = [
'DataUnavailable',
'DataThrottle',
'resproc',
'get_logger',
]
__brokers__: list[str] = [
@ -65,6 +65,10 @@ __brokers__: list[str] = [
# bitso
]
log = get_logger(
name=__name__,
)
def get_brokermod(brokername: str) -> ModuleType:
'''

View File

@ -33,12 +33,18 @@ import exceptiongroup as eg
import tractor
import trio
from piker.log import (
get_logger,
get_console_log,
)
from . import _util
from . import get_brokermod
if TYPE_CHECKING:
from ..data import _FeedsBus
log = get_logger(name=__name__)
# `brokerd` enabled modules
# TODO: move this def to the `.data` subpkg..
# 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(
ctx: tractor.Context,
brokername: str,
loglevel: str | None = None,
loglevel: str|None = None,
) -> None:
'''
@ -72,13 +78,14 @@ async def _setup_persistent_brokerd(
# since all hosted daemon tasks will reference this same
# log instance's (actor local) state and thus don't require
# any further (level) configuration on their own B)
log = _util.get_console_log(
loglevel or tractor.current_actor().loglevel,
actor: tractor.Actor = tractor.current_actor()
tll: str = actor.loglevel
log = get_console_log(
level=loglevel or tll,
name=f'{_util.subsys}.{brokername}',
with_tractor_log=bool(tll),
)
# set global for this actor to this new process-wide instance B)
_util.log = log
assert log.name == _util.subsys
# further, set the log level on any broker broker specific
# logger instance.
@ -97,7 +104,7 @@ async def _setup_persistent_brokerd(
# NOTE: see ep invocation details inside `.data.feed`.
try:
async with (
tractor.trionics.collapse_eg(),
# tractor.trionics.collapse_eg(),
trio.open_nursery() as service_nursery
):
bus: _FeedsBus = feed.get_feed_bus(
@ -193,7 +200,6 @@ def broker_init(
async def spawn_brokerd(
brokername: str,
loglevel: str | None = None,
@ -201,8 +207,10 @@ async def spawn_brokerd(
) -> bool:
from piker.service._util import log # use service mngr log
log.info(f'Spawning {brokername} broker daemon')
log.info(
f'Spawning broker-daemon,\n'
f'backend: {brokername!r}'
)
(
brokermode,
@ -249,7 +257,7 @@ async def spawn_brokerd(
async def maybe_spawn_brokerd(
brokername: str,
loglevel: str | None = None,
loglevel: str|None = None,
**pikerd_kwargs,
@ -264,6 +272,11 @@ async def maybe_spawn_brokerd(
'''
from piker.service import maybe_spawn_daemon
# if (
# loglevel != 'info'
# ):
# await tractor.pause()
async with maybe_spawn_daemon(
f'brokerd.{brokername}',

View File

@ -19,15 +19,13 @@ Handy cross-broker utils.
"""
from __future__ import annotations
from functools import partial
# from functools import partial
import json
import httpx
import logging
from ..log import (
get_logger,
get_console_log,
from piker.log import (
colorize_json,
)
subsys: str = 'piker.brokers'
@ -35,12 +33,22 @@ subsys: str = 'piker.brokers'
# NOTE: level should be reset by any actor that is spawned
# as well as given a (more) explicit name/key such
# as `piker.brokers.binance` matching the subpkg.
log = get_logger(subsys)
# log = get_logger(subsys)
get_console_log = partial(
get_console_log,
name=subsys,
)
# ?TODO?? we could use this approach, but we need to be able
# to pass multiple `name=` values so for example we can include the
# 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):

View File

@ -37,8 +37,9 @@ import trio
from piker.accounting import (
Asset,
)
from piker.brokers._util import (
from piker.log import (
get_logger,
get_console_log,
)
from piker.data._web_bs import (
open_autorecon_ws,
@ -69,7 +70,9 @@ from .venues import (
)
from .api import Client
log = get_logger('piker.brokers.binance')
log = get_logger(
name=__name__,
)
# Fee schedule template, mostly for paper engine fees modelling.
@ -245,9 +248,16 @@ async def handle_order_requests(
@tractor.context
async def open_trade_dialog(
ctx: tractor.Context,
loglevel: str = 'warning',
) -> 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
# positions are loaded from the correct venue on the user
# stream at startup? (that is in an attempt to support both

View File

@ -64,9 +64,9 @@ from piker.data._web_bs import (
open_autorecon_ws,
NoBsWs,
)
from piker.log import get_logger
from piker.brokers._util import (
DataUnavailable,
get_logger,
)
from .api import (
@ -78,7 +78,7 @@ from .venues import (
get_api_eps,
)
log = get_logger('piker.brokers.binance')
log = get_logger(name=__name__)
class L1(Struct):
@ -275,9 +275,15 @@ async def open_history_client(
f'{times}'
)
# XXX, debug any case where the latest 1m bar we get is
# already another "sample's-step-old"..
if end_dt is None:
inow: int = round(time.time())
if (inow - times[-1]) > 60:
if (
_time_step := (inow - times[-1])
>
timeframe * 2
):
await tractor.pause()
start_dt = from_timestamp(times[0])

View File

@ -27,14 +27,12 @@ import click
import trio
import tractor
from ..cli import cli
from .. import watchlists as wl
from ..log import (
from piker.cli import cli
from piker import watchlists as wl
from piker.log import (
colorize_json,
)
from ._util import (
log,
get_console_log,
get_logger,
)
from ..service import (
maybe_spawn_brokerd,
@ -45,12 +43,15 @@ from ..brokers import (
get_brokermod,
data,
)
DEFAULT_BROKER = 'binance'
log = get_logger(
name=__name__,
)
DEFAULT_BROKER = 'binance'
_config_dir = click.get_app_dir('piker')
_watchlists_data_path = os.path.join(_config_dir, 'watchlists.json')
OK = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
@ -345,7 +346,10 @@ def contracts(ctx, loglevel, broker, symbol, ids):
'''
brokermod = get_brokermod(broker)
get_console_log(loglevel)
get_console_log(
level=loglevel,
name=__name__,
)
contracts = trio.run(partial(core.contracts, brokermod, symbol))
if not ids:
@ -477,11 +481,12 @@ def search(
# the `piker --pdb` XD ..
# -[ ] pull from the parent click ctx's values..dumdum
# assert pdb
loglevel: str = config['loglevel']
# define tractor entrypoint
async def main(func):
async with maybe_open_pikerd(
loglevel=config['loglevel'],
loglevel=loglevel,
debug_mode=pdb,
):
return await func()
@ -494,6 +499,7 @@ def search(
core.symbol_search,
brokermods,
pattern,
loglevel=loglevel,
),
)

View File

@ -28,12 +28,14 @@ from typing import (
import trio
from ._util import log
from piker.log import get_logger
from . import get_brokermod
from ..service import maybe_spawn_brokerd
from . import open_cached_client
from ..accounting import MktPair
log = get_logger(name=__name__)
async def api(brokername: str, methname: str, **kwargs) -> dict:
'''
@ -147,6 +149,7 @@ async def search_w_brokerd(
async def symbol_search(
brokermods: list[ModuleType],
pattern: str,
loglevel: str = 'warning',
**kwargs,
) -> dict[str, dict[str, dict[str, Any]]]:
@ -176,6 +179,7 @@ async def symbol_search(
'_infect_asyncio',
False,
),
loglevel=loglevel
) as portal:
results.append((

View File

@ -41,12 +41,15 @@ import tractor
from tractor.experimental import msgpub
from async_generator import asynccontextmanager
from ._util import (
log,
from piker.log import(
get_logger,
get_console_log,
)
from . import get_brokermod
log = get_logger(
name='piker.brokers.binance',
)
async def wait_for_network(
net_func: Callable,
@ -243,7 +246,10 @@ async def start_quote_stream(
'''
# 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
symbols = list(symbols)

View File

@ -34,13 +34,13 @@ import subprocess
import tractor
from piker.brokers._util import get_logger
from piker.log import get_logger
if TYPE_CHECKING:
from .api import Client
import i3ipc
log = get_logger('piker.brokers.ib')
log = get_logger(name=__name__)
_reset_tech: Literal[
'vnc',

View File

@ -50,6 +50,10 @@ from ib_insync.objects import (
)
from piker import config
from piker.log import (
get_logger,
get_console_log,
)
from piker.types import Struct
from piker.accounting import (
Position,
@ -77,7 +81,6 @@ from piker.clearing._messages import (
BrokerdFill,
BrokerdError,
)
from ._util import log
from .api import (
_accounts2clients,
get_config,
@ -95,6 +98,10 @@ from .ledger import (
update_ledger_from_api_trades,
)
log = get_logger(
name=__name__,
)
def pack_position(
pos: IbPosition,
@ -536,9 +543,15 @@ class IbAcnt(Struct):
@tractor.context
async def open_trade_dialog(
ctx: tractor.Context,
loglevel: str = 'warning',
) -> AsyncIterator[dict[str, Any]]:
get_console_log(
level=loglevel,
name=__name__,
)
# task local msg dialog tracking
flows = OrderDialogs()
accounts_def = config.load_accounts(['ib'])

View File

@ -56,11 +56,11 @@ from piker.brokers._util import (
NoData,
DataUnavailable,
)
from piker.log import get_logger
from .api import (
# _adhoc_futes_set,
Client,
con2fqme,
log,
load_aio_clients,
MethodProxy,
open_client_proxies,
@ -78,6 +78,9 @@ from .symbols import get_mkt_info
if TYPE_CHECKING:
from trio._core._run import Task
log = get_logger(
name=__name__,
)
# XXX NOTE: See available types table docs:
# https://interactivebrokers.github.io/tws-api/tick_types.html

View File

@ -44,6 +44,7 @@ from ib_insync import (
CommissionReport,
)
from piker.log import get_logger
from piker.types import Struct
from piker.data import (
SymbologyCache,
@ -57,7 +58,6 @@ from piker.accounting import (
iter_by_dt,
)
from ._flex_reports import parse_flex_dt
from ._util import log
if TYPE_CHECKING:
from .api import (
@ -65,6 +65,9 @@ if TYPE_CHECKING:
MethodProxy,
)
log = get_logger(
name=__name__,
)
tx_sort: Callable = partial(
iter_by_dt,

View File

@ -42,10 +42,7 @@ from piker.accounting import (
from piker._cacheables import (
async_lifo_cache,
)
from ._util import (
log,
)
from piker.log import get_logger
if TYPE_CHECKING:
from .api import (
@ -53,6 +50,10 @@ if TYPE_CHECKING:
Client,
)
log = get_logger(
name=__name__,
)
_futes_venues = (
'GLOBEX',
'NYMEX',

View File

@ -62,9 +62,12 @@ from piker.clearing._messages import (
from piker.brokers import (
open_cached_client,
)
from piker.log import (
get_console_log,
get_logger,
)
from piker.data import open_symcache
from .api import (
log,
Client,
BrokerError,
)
@ -78,6 +81,8 @@ from .ledger import (
verify_balances,
)
log = get_logger(name=__name__)
MsgUnion = Union[
BrokerdCancel,
BrokerdError,
@ -431,9 +436,15 @@ def trades2pps(
@tractor.context
async def open_trade_dialog(
ctx: tractor.Context,
loglevel: str = 'warning',
) -> AsyncIterator[dict[str, Any]]:
get_console_log(
level=loglevel,
name=__name__,
)
async with (
# TODO: maybe bind these together and deliver
# a tuple from `.open_cached_client()`?

View File

@ -50,13 +50,19 @@ from . import open_cached_client
from piker._cacheables import async_lifo_cache
from .. import config
from ._util import resproc, BrokerError, SymbolNotFound
from ..log import (
from piker.log import (
colorize_json,
)
from ._util import (
log,
get_console_log,
)
from piker.log import (
get_logger,
)
log = get_logger(
name=__name__,
)
_use_practice_account = False
_refresh_token_ep = 'https://{}login.questrade.com/oauth2/'
@ -1205,7 +1211,10 @@ async def stream_quotes(
# feed_type: str = 'stock',
) -> AsyncGenerator[str, Dict[str, Any]]:
# 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:
if feed_type == 'stock':

View File

@ -30,9 +30,16 @@ import asks
from ._util import (
resproc,
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'

View File

@ -215,7 +215,7 @@ async def relay_orders_from_sync_code(
async def open_ems(
fqme: str,
mode: str = 'live',
loglevel: str = 'error',
loglevel: str = 'warning',
) -> tuple[
OrderClient, # client

View File

@ -47,6 +47,7 @@ from tractor import trionics
from ._util import (
log, # sub-sys logger
get_console_log,
subsys,
)
from ..accounting._mktinfo import (
unpack_fqme,
@ -136,7 +137,7 @@ class DarkBook(Struct):
tuple[
Callable[[float], bool], # predicate
tuple[str, ...], # tickfilter
dict | Order, # cmd / msg type
dict|Order, # cmd / msg type
# live submission constraint parameters
float, # percent_away max price diff
@ -278,7 +279,7 @@ async def clear_dark_triggers(
# remove exec-condition from set
log.info(f'Removing trigger for {oid}')
trigger: tuple | None = execs.pop(oid, None)
trigger: tuple|None = execs.pop(oid, None)
if not trigger:
log.warning(
f'trigger for {oid} was already removed!?'
@ -336,8 +337,8 @@ async def open_brokerd_dialog(
brokermod: ModuleType,
portal: tractor.Portal,
exec_mode: str,
fqme: str | None = None,
loglevel: str | None = None,
fqme: str|None = None,
loglevel: str|None = None,
) -> tuple[
tractor.MsgStream,
@ -351,9 +352,21 @@ async def open_brokerd_dialog(
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
def mk_paper_ep():
def mk_paper_ep(
loglevel: str,
):
from . import _paper_engine as paper_mod
nonlocal brokermod, exec_mode
@ -405,17 +418,21 @@ async def open_brokerd_dialog(
if (
trades_endpoint is not None
or exec_mode != 'paper'
or
exec_mode != 'paper'
):
# open live brokerd trades endpoint
open_trades_endpoint = portal.open_context(
trades_endpoint,
loglevel=loglevel,
)
@acm
async def maybe_open_paper_ep():
if exec_mode == 'paper':
async with mk_paper_ep() as msg:
async with mk_paper_ep(
loglevel=loglevel,
) as msg:
yield msg
return
@ -426,7 +443,9 @@ async def open_brokerd_dialog(
# runtime indication that the backend can't support live
# order ctrl yet, so boot the paperboi B0
if first == 'paper':
async with mk_paper_ep() as msg:
async with mk_paper_ep(
loglevel=loglevel,
) as msg:
yield msg
return
else:
@ -760,12 +779,16 @@ _router: Router = None
@tractor.context
async def _setup_persistent_emsd(
ctx: tractor.Context,
loglevel: str | None = None,
loglevel: str|None = None,
) -> None:
if loglevel:
get_console_log(loglevel)
_log = get_console_log(
level=loglevel,
name=subsys,
)
assert _log.name == 'piker.clearing'
global _router
@ -821,7 +844,7 @@ async def translate_and_relay_brokerd_events(
f'Rx brokerd trade msg:\n'
f'{fmsg}'
)
status_msg: Status | None = None
status_msg: Status|None = None
match brokerd_msg:
# BrokerdPosition
@ -1282,7 +1305,7 @@ async def process_client_order_cmds(
and status.resp == 'dark_open'
):
# 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:
(
pred,

View File

@ -59,9 +59,9 @@ from piker.data import (
open_symcache,
)
from piker.types import Struct
from ._util import (
log, # sub-sys logger
from piker.log import (
get_console_log,
get_logger,
)
from ._messages import (
BrokerdCancel,
@ -73,6 +73,8 @@ from ._messages import (
BrokerdError,
)
log = get_logger(name=__name__)
class PaperBoi(Struct):
'''
@ -550,16 +552,18 @@ _sells: defaultdict[
@tractor.context
async def open_trade_dialog(
ctx: tractor.Context,
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',
) -> None:
# enable piker.clearing console log for *this* subactor
get_console_log(loglevel)
# enable piker.clearing console log for *this* `brokerd` subactor
get_console_log(
level=loglevel,
name=__name__,
)
symcache: SymbologyCache
async with open_symcache(get_brokermod(broker)) as symcache:

View File

@ -28,12 +28,14 @@ from ..log import (
from piker.types import Struct
subsys: str = 'piker.clearing'
log = get_logger(subsys)
log = get_logger(
name='piker.clearing',
)
# TODO, oof doesn't this ignore the `loglevel` then???
get_console_log = partial(
get_console_log,
name=subsys,
name='clearing',
)

View File

@ -61,7 +61,8 @@ def load_trans_eps(
if (
network
and not maddrs
and
not maddrs
):
# load network section and (attempt to) connect all endpoints
# which are reachable B)
@ -112,31 +113,27 @@ def load_trans_eps(
default=None,
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(
maddr: list[str] | None,
loglevel: str,
tl: 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
# 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:
log.warning((
@ -237,6 +234,14 @@ def cli(
regaddr: str,
) -> 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:
assert os.path.isdir(configdir), f"`{configdir}` is not a valid path"
config._override_config_dir(configdir)
@ -295,17 +300,50 @@ def cli(
@click.option('--tl', is_flag=True, help='Enable tractor logging')
@click.argument('ports', nargs=-1, required=False)
@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,
_default_registry_port,
_default_registry_host,
)
host = _default_registry_host
# !TODO, mk this to work with UDS!
host: str = _default_registry_host
if not ports:
ports = [_default_registry_port]
ports: list[int] = [_default_registry_port]
addr = tractor._addr.wrap_address(
addr=(host, ports[0])
@ -316,7 +354,11 @@ def services(config, tl, ports):
async with (
open_piker_runtime(
name='service_query',
loglevel=config['loglevel'] if tl else None,
loglevel=(
config['loglevel']
if tl
else None
),
),
tractor.get_registry(
addr=addr,
@ -336,7 +378,15 @@ def services(config, tl, ports):
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 ..ui import cli # noqa
from ..watchlists import cli # noqa
@ -346,5 +396,5 @@ def _load_clis() -> None:
from ..accounting import cli # noqa
# load downstream cli modules
# load all subsytem cli eps
_load_clis()

View File

@ -334,10 +334,18 @@ async def register_with_sampler(
open_index_stream: bool = True, # open a 2way stream for sample step msgs?
sub_for_broadcasts: bool = True, # sampler side to send step updates?
loglevel: str|None = None,
) -> 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
try:
@ -474,6 +482,7 @@ async def spawn_samplerd(
register_with_sampler,
period_s=1,
sub_for_broadcasts=False,
loglevel=loglevel,
)
return True
@ -482,7 +491,6 @@ async def spawn_samplerd(
@acm
async def maybe_open_samplerd(
loglevel: str|None = None,
**pikerd_kwargs,
@ -511,10 +519,10 @@ async def open_sample_stream(
shms_by_period: dict[float, dict]|None = None,
open_index_stream: bool = True,
sub_for_broadcasts: bool = True,
loglevel: str|None = None,
cache_key: str|None = None,
allow_new_sampler: bool = True,
# cache_key: str|None = None,
# allow_new_sampler: bool = True,
ensure_is_active: bool = False,
) -> AsyncIterator[dict[str, float]]:
@ -549,7 +557,9 @@ async def open_sample_stream(
# XXX: this should be singleton on a host,
# a lone broker-daemon per provider should be
# created for all practical purposes
maybe_open_samplerd() as portal,
maybe_open_samplerd(
loglevel=loglevel,
) as portal,
portal.open_context(
register_with_sampler,
@ -558,6 +568,7 @@ async def open_sample_stream(
'shms_by_period': shms_by_period,
'open_index_stream': open_index_stream,
'sub_for_broadcasts': sub_for_broadcasts,
'loglevel': loglevel,
},
) as (ctx, shm_periods)
):

View File

@ -26,7 +26,9 @@ from ..log import (
)
subsys: str = 'piker.data'
log = get_logger(subsys)
log = get_logger(
name=subsys,
)
get_console_log = partial(
get_console_log,

View File

@ -62,7 +62,6 @@ from ._util import (
log,
get_console_log,
)
from .flows import Flume
from .validate import (
FeedInit,
validate_backend,
@ -77,6 +76,7 @@ from ._sampling import (
)
if TYPE_CHECKING:
from .flows import Flume
from tractor._addr import Address
from tractor.msg.types import Aid
@ -239,7 +239,6 @@ async def allocate_persistent_feed(
brokername: str,
symstr: str,
loglevel: str,
start_stream: bool = True,
init_timeout: float = 616,
@ -278,7 +277,7 @@ async def allocate_persistent_feed(
# ``stream_quotes()``, a required broker backend endpoint.
init_msgs: (
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
@ -348,11 +347,14 @@ async def allocate_persistent_feed(
izero_rt,
rt_shm,
) = await bus.nursery.start(
manage_history,
mod,
mkt,
some_data_ready,
feed_is_live,
partial(
manage_history,
mod=mod,
mkt=mkt,
some_data_ready=some_data_ready,
feed_is_live=feed_is_live,
loglevel=loglevel,
)
)
# yield back control to starting nursery once we receive either
@ -362,6 +364,8 @@ async def allocate_persistent_feed(
)
await some_data_ready.wait()
# XXX, avoid cycle; it imports this mod.
from .flows import Flume
flume = Flume(
# TODO: we have to use this for now since currently the
@ -458,7 +462,6 @@ async def allocate_persistent_feed(
@tractor.context
async def open_feed_bus(
ctx: tractor.Context,
brokername: str,
symbols: list[str], # normally expected to the broker-specific fqme
@ -479,13 +482,16 @@ async def open_feed_bus(
'''
if loglevel is None:
loglevel = tractor.current_actor().loglevel
loglevel: str = tractor.current_actor().loglevel
# XXX: required to propagate ``tractor`` loglevel to piker
# logging
get_console_log(
loglevel
or tractor.current_actor().loglevel
level=(loglevel
or
tractor.current_actor().loglevel
),
name=__name__,
)
# local state sanity checks
@ -500,7 +506,6 @@ async def open_feed_bus(
sub_registered = trio.Event()
flumes: dict[str, Flume] = {}
for symbol in symbols:
# if no cached feed for this symbol has been created for this
@ -684,6 +689,7 @@ class Feed(Struct):
'''
mods: dict[str, ModuleType] = {}
portals: dict[ModuleType, tractor.Portal] = {}
flumes: dict[
str, # FQME
Flume,
@ -797,7 +803,7 @@ async def install_brokerd_search(
@acm
async def maybe_open_feed(
fqmes: list[str],
loglevel: str | None = None,
loglevel: str|None = None,
**kwargs,
@ -813,6 +819,11 @@ async def maybe_open_feed(
'''
fqme = fqmes[0]
# if (
# loglevel != 'info'
# ):
# await tractor.pause()
async with trionics.maybe_open_context(
acm_func=open_feed,
kwargs={
@ -879,9 +890,12 @@ async def open_feed(
providers.setdefault(mod, []).append(bs_fqme)
feed.mods[mod.name] = mod
if (
loglevel != 'info'
):
await tractor.pause()
# one actor per brokerd for now
brokerd_ctxs = []
for brokermod, bfqmes in providers.items():
# 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)
# XXX, avoid cycle; it imports this mod.
from .flows import Flume
async with (
trionics.gather_contexts(bus_ctxs) as ctxs,
):

View File

@ -24,6 +24,7 @@ from functools import partial
from typing import (
AsyncIterator,
Callable,
TYPE_CHECKING,
)
import numpy as np
@ -33,12 +34,12 @@ import tractor
from tractor.msg import NamespacePath
from piker.types import Struct
from ..log import get_logger, get_console_log
from .. import data
from ..data.feed import (
Flume,
Feed,
from ..log import (
get_logger,
get_console_log,
)
from .. import data
from ..data.flows import Flume
from ..data._sharedmem import ShmArray
from ..data._sampling import (
_default_delay_s,
@ -52,6 +53,9 @@ from ._api import (
)
from ..toolz import Profiler
if TYPE_CHECKING:
from ..data.feed import Feed
log = get_logger(__name__)
@ -169,8 +173,10 @@ class Cascade(Struct):
if not synced:
fsp: Fsp = self.fsp
log.warning(
'***DESYNCED FSP***\n'
f'{fsp.ns_path}@{src_shm.token}\n'
f'***DESYNCED fsp***\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'len_diff: {len_diff}\n'
)
@ -398,7 +404,6 @@ async def connect_streams(
@tractor.context
async def cascade(
ctx: tractor.Context,
# data feed key
@ -412,7 +417,7 @@ async def cascade(
shm_registry: dict[str, _Token],
zero_on_step: bool = False,
loglevel: str | None = None,
loglevel: str|None = None,
) -> None:
'''
@ -426,7 +431,17 @@ async def cascade(
)
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)
dst: Flume = Flume.from_msg(
@ -469,7 +484,8 @@ async def cascade(
# open a data feed stream with requested broker
feed: Feed
async with data.feed.maybe_open_feed(
[fqme],
fqmes=[fqme],
loglevel=loglevel,
# TODO throttle tick outputs from *this* daemon since
# 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`
# service.
async with open_sample_stream(
float(delay_s)
period_s=float(delay_s),
loglevel=loglevel,
) as istream:
profiler(f'{func_name}: sample stream up')

View File

@ -37,35 +37,84 @@ _proj_name: str = 'piker'
def get_logger(
name: str = None,
name: str|None = None,
**tractor_log_kwargs,
) -> 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(
name=name,
_root_name=_proj_name,
pkg_name=pkg_name,
**tractor_log_kwargs,
)
def get_console_log(
level: str | None = None,
name: str | None = None,
level: 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:
'''
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(
level,
level=level,
name=name,
_root_name=_proj_name,
) # our root logger
pkg_name=pkg_name,
**tractor_log_kwargs,
)
def colorize_json(

View File

@ -21,7 +21,6 @@
from __future__ import annotations
import os
from typing import (
Optional,
Any,
ClassVar,
)
@ -32,9 +31,12 @@ from contextlib import (
import tractor
import trio
from ._util import (
from piker.log import (
get_console_log,
)
from ._util import (
subsys,
)
from ._mngr import (
Services,
)
@ -59,7 +61,7 @@ async def open_piker_runtime(
registry_addrs: list[tuple[str, int]] = [],
enable_modules: list[str] = [],
loglevel: Optional[str] = None,
loglevel: str|None = None,
# XXX NOTE XXX: you should pretty much never want debug mode
# for data daemons when running in production.
@ -69,7 +71,7 @@ async def open_piker_runtime(
# and spawn the service tree distributed per that.
start_method: str = 'trio',
tractor_runtime_overrides: dict | None = None,
tractor_runtime_overrides: dict|None = None,
**tractor_kwargs,
) -> tuple[
@ -97,7 +99,8 @@ async def open_piker_runtime(
# setting it as the root actor on localhost.
registry_addrs = (
registry_addrs
or [_default_reg_addr]
or
[_default_reg_addr]
)
if ems := tractor_kwargs.pop('enable_modules', None):
@ -163,8 +166,7 @@ _root_modules: list[str] = [
@acm
async def open_pikerd(
registry_addrs: list[tuple[str, int]],
loglevel: str | None = None,
loglevel: str|None = None,
# XXX: you should pretty much never want debug mode
# for data daemons when running in production.
@ -192,7 +194,6 @@ async def open_pikerd(
async with (
open_piker_runtime(
name=_root_dname,
loglevel=loglevel,
debug_mode=debug_mode,
@ -273,7 +274,10 @@ async def maybe_open_pikerd(
'''
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
query_name = kwargs.pop(

View File

@ -49,13 +49,15 @@ from requests.exceptions import (
ReadTimeout,
)
from ._mngr import Services
from ._util import (
log, # sub-sys logger
from piker.log import (
get_console_log,
get_logger,
)
from ._mngr import Services
from .. import config
log = get_logger(name=__name__)
class DockerNotStarted(Exception):
'Prolly you dint start da daemon bruh'
@ -336,13 +338,16 @@ class Container:
async def open_ahabd(
ctx: tractor.Context,
endpoint: str, # ns-pointer str-msg-type
loglevel: str | None = None,
loglevel: str = 'cancel',
**ep_kwargs,
) -> None:
log = get_console_log(loglevel or 'cancel')
log = get_console_log(
level=loglevel,
name='piker.service',
)
async with open_docker() as client:

View File

@ -30,8 +30,9 @@ from contextlib import (
import tractor
from trio.lowlevel import current_task
from ._util import (
log, # sub-sys logger
from piker.log import (
get_console_log,
get_logger,
)
from ._mngr import (
Services,
@ -39,16 +40,17 @@ from ._mngr import (
from ._actor_runtime import maybe_open_pikerd
from ._registry import find_service
log = get_logger(name=__name__)
@acm
async def maybe_spawn_daemon(
service_name: str,
service_task_target: Callable,
spawn_args: dict[str, Any],
loglevel: str | None = None,
loglevel: str|None = None,
singleton: bool = False,
**pikerd_kwargs,
@ -66,6 +68,12 @@ async def maybe_spawn_daemon(
clients.
'''
log = get_console_log(
level=loglevel,
name=__name__,
)
assert log.name == 'piker.service'
# serialize access to this section to avoid
# 2 or more tasks racing to create a daemon
lock = Services.locks[service_name]
@ -152,8 +160,7 @@ async def maybe_spawn_daemon(
async def spawn_emsd(
loglevel: str | None = None,
loglevel: str|None = None,
**extra_tractor_kwargs
) -> bool:
@ -190,9 +197,8 @@ async def spawn_emsd(
@acm
async def maybe_open_emsd(
brokername: str,
loglevel: str | None = None,
loglevel: str|None = None,
**pikerd_kwargs,

View File

@ -34,9 +34,9 @@ from tractor import (
Portal,
)
from ._util import (
log, # sub-sys logger
)
from piker.log import get_logger
log = get_logger(name=__name__)
# TODO: we need remote wrapping and a general soln:

View File

@ -27,15 +27,29 @@ from typing import (
)
import tractor
from tractor import Portal
from ._util import (
log, # sub-sys logger
from tractor import (
msg,
Actor,
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_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_port,
)
@ -75,16 +89,22 @@ async def open_registry(
'''
global _tractor_kwargs
actor = tractor.current_actor()
uid = actor.uid
preset_reg_addrs: list[tuple[str, int]] = Registry.addrs
actor: Actor = tractor.current_actor()
aid: msg.Aid = actor.aid
uid: tuple[str, str] = aid.uid
preset_reg_addrs: list[
tuple[str, int]
] = Registry.addrs
if (
preset_reg_addrs
and addrs
and
addrs
):
if preset_reg_addrs != 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:
log.warning(
f'`{uid}` requested only subset of registrars: {addrs}\n'
@ -98,7 +118,6 @@ async def open_registry(
)
was_set: bool = False
if (
not tractor.is_root_process()
and
@ -115,16 +134,23 @@ async def open_registry(
f"`{uid}` registry should already exist but doesn't?"
)
if (
not Registry.addrs
):
if not Registry.addrs:
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
# `.ui._exec` which is the (eventual qtloops) bootstrapping
# 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:
yield Registry.addrs
@ -149,7 +175,7 @@ async def find_service(
| None
):
# try:
reg_addrs: list[tuple[str, int]]
reg_addrs: list[tuple[str, int|str]]
async with open_registry(
addrs=(
registry_addrs
@ -172,15 +198,13 @@ async def find_service(
only_first=first_only, # if set only returns single ref
) as maybe_portals:
if not maybe_portals:
# log.info(
print(
log.info(
f'Could NOT find service {service_name!r} -> {maybe_portals!r}'
)
yield None
return
# log.info(
print(
log.info(
f'Found service {service_name!r} -> {maybe_portals}'
)
yield maybe_portals
@ -195,8 +219,7 @@ async def find_service(
async def check_for_service(
service_name: str,
) -> None | tuple[str, int]:
) -> None|tuple[str, int]:
'''
Service daemon "liveness" predicate.

View File

@ -14,20 +14,12 @@
# 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/>.
"""
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'
log = get_logger(subsys)
get_console_log = partial(
get_console_log,
name=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
# `level=` value? seems too early in the runtime to be injected
# right?

View File

@ -16,6 +16,7 @@
from __future__ import annotations
from contextlib import asynccontextmanager as acm
from pprint import pformat
from typing import (
Any,
TYPE_CHECKING,
@ -26,12 +27,17 @@ import asks
if TYPE_CHECKING:
import docker
from ._ahab import DockerContainer
from . import (
Services,
)
from ._util import log # sub-sys logger
from ._util import (
from piker.log import (
get_console_log,
get_logger,
)
log = get_logger(name=__name__)
# container level config
_config = {
@ -67,7 +73,10 @@ def start_elasticsearch(
elastic
'''
get_console_log('info', name=__name__)
get_console_log(
level='info',
name=__name__,
)
dcntr: DockerContainer = client.containers.run(
'piker:elastic',

View File

@ -52,17 +52,18 @@ import pendulum
# TODO: import this for specific error set expected by mkts client
# import purerpc
from ..data.feed import maybe_open_feed
from piker.data.feed import maybe_open_feed
from . import Services
from ._util import (
log, # sub-sys logger
from piker.log import (
get_console_log,
get_logger,
)
if TYPE_CHECKING:
import docker
from ._ahab import DockerContainer
log = get_logger(name=__name__)
# ahabd-supervisor and container level config

View File

@ -447,7 +447,13 @@ def ldshm(
)
# last chance manual overwrites in REPL
# await tractor.pause()
assert aids
if not aids:
log.warning(
f'No gaps were found !?\n'
f'fqme: {fqme!r}\n'
f'timeframe: {period_s!r}\n'
f"WELL THAT'S GOOD NOOZ!\n"
)
tf2aids[period_s] = aids
else:

View File

@ -54,10 +54,10 @@ from ..log import (
# for "time series processing"
subsys: str = 'piker.tsp'
log = get_logger(subsys)
log = get_logger(name=__name__)
get_console_log = partial(
get_console_log,
name=subsys,
name=subsys, # activate for subsys-pkg "downward"
)
# NOTE: union type-defs to handle generic `numpy` and `polars` types

View File

@ -49,6 +49,7 @@ from pendulum import (
Duration,
duration as mk_duration,
from_timestamp,
timezone,
)
import numpy as np
import polars as pl
@ -57,15 +58,15 @@ from piker.brokers import NoData
from piker.accounting import (
MktPair,
)
from piker.data._util import (
log,
)
from piker.log import get_logger
from ..data._sharedmem import (
maybe_open_shm_array,
ShmArray,
)
from ..data._source import def_iohlcv_fields
from ..data._sampling import (
from piker.data._source import (
def_iohlcv_fields,
)
from piker.data._sampling import (
open_sample_stream,
)
@ -97,6 +98,11 @@ if TYPE_CHECKING:
# from .feed import _FeedsBus
log = get_logger(
name=__name__,
)
# `ShmArray` buffer sizing configuration:
_mins_in_day = int(60 * 24)
# how much is probably dependent on lifestyle
@ -401,7 +407,9 @@ async def start_backfill(
# based on the sample step size, maybe load a certain amount history
update_start_on_prepend: bool = False
if backfill_until_dt is None:
if (
_until_was_none := (backfill_until_dt is None)
):
# TODO: per-provider default history-durations?
# -[ ] inside the `open_history_client()` config allow
@ -435,6 +443,8 @@ async def start_backfill(
last_start_dt: datetime = backfill_from_dt
next_prepend_index: int = backfill_from_shm_index
est = timezone('EST')
while last_start_dt > backfill_until_dt:
log.info(
f'Requesting {timeframe}s frame:\n'
@ -448,9 +458,10 @@ async def start_backfill(
next_end_dt,
) = await get_hist(
timeframe,
end_dt=last_start_dt,
end_dt=(end_dt_param := last_start_dt),
)
except NoData as _daterr:
except NoData as nodata:
_nodata = nodata
orig_last_start_dt: datetime = last_start_dt
gap_report: str = (
f'EMPTY FRAME for `end_dt: {last_start_dt}`?\n'
@ -518,8 +529,32 @@ async def start_backfill(
==
next_start_dt.timestamp()
)
assert (
(last_time := time[-1])
==
next_end_dt.timestamp()
)
assert time[-1] == next_end_dt.timestamp()
frame_last_dt = from_timestamp(last_time)
if (
frame_last_dt.add(seconds=timeframe)
<
end_dt_param
):
est_frame_last_dt = est.convert(frame_last_dt)
est_end_dt_param = est.convert(end_dt_param)
log.warning(
f'Provider frame ending BEFORE requested end_dt={end_dt_param} ??\n'
f'frame_last_dt (EST): {est_frame_last_dt!r}\n'
f'end_dt_param (EST): {est_end_dt_param!r}\n'
f'\n'
f'Likely contains,\n'
f'- a venue closure.\n'
f'- (maybe?) missing data ?\n'
)
# ?TODO, check against venue closure hours
# if/when provided by backend?
# await tractor.pause()
expected_dur: Interval = (
last_start_dt.subtract(
@ -581,10 +616,11 @@ async def start_backfill(
'0 BARS TO PUSH after diff!?\n'
f'{next_start_dt} -> {last_start_dt}'
)
await tractor.pause()
# Check if we're about to exceed buffer capacity BEFORE
# attempting the push
if next_prepend_index - ln < 0:
if (next_prepend_index - ln) < 0:
log.warning(
f'Backfill would exceed buffer capacity!\n'
f'next_prepend_index: {next_prepend_index}\n'
@ -655,7 +691,7 @@ async def start_backfill(
},
})
# can't push the entire frame? so
# XXX, can't push the entire frame? so
# push only the amount that can fit..
break
@ -715,8 +751,8 @@ async def start_backfill(
) = dedupe(df)
if diff:
log.warning(
f'Found {diff} duplicates in tsdb, '
f'overwriting with deduped data\n'
f'Found {diff!r} duplicates in tsdb! '
f'=> Overwriting with `deduped` data !! <=\n'
)
await storage.write_ohlcv(
col_sym_key,
@ -1288,6 +1324,7 @@ async def manage_history(
mkt: MktPair,
some_data_ready: trio.Event,
feed_is_live: trio.Event,
loglevel: str = 'warning',
timeframe: float = 60, # in seconds
wait_for_live_timeout: float = 0.5,
@ -1465,6 +1502,7 @@ async def manage_history(
# data feed layer that needs to consume it).
open_index_stream=True,
sub_for_broadcasts=False,
loglevel=loglevel,
) as sample_stream:
# register 1s and 1m buffers with the global

View File

@ -21,230 +21,6 @@ Extensions to built-in or (heavily used but 3rd party) friend-lib
types.
'''
from __future__ import annotations
from collections import UserList
from pprint import (
saferepr,
from tractor.msg.pretty_struct import (
Struct as Struct,
)
from typing import Any
from msgspec import (
msgpack,
Struct as _Struct,
structs,
)
class DiffDump(UserList):
'''
Very simple list delegator that repr() dumps (presumed) tuple
elements of the form `tuple[str, Any, Any]` in a nice
multi-line readable form for analyzing `Struct` diffs.
'''
def __repr__(self) -> str:
if not len(self):
return super().__repr__()
# format by displaying item pair's ``repr()`` on multiple,
# indented lines such that they are more easily visually
# comparable when printed to console when printed to
# console.
repstr: str = '[\n'
for k, left, right in self:
repstr += (
f'({k},\n'
f'\t{repr(left)},\n'
f'\t{repr(right)},\n'
')\n'
)
repstr += ']\n'
return repstr
class Struct(
_Struct,
# https://jcristharif.com/msgspec/structs.html#tagged-unions
# tag='pikerstruct',
# tag=True,
):
'''
A "human friendlier" (aka repl buddy) struct subtype.
'''
def _sin_props(self) -> Iterator[
tuple[
structs.FieldIinfo,
str,
Any,
]
]:
'''
Iterate over all non-@property fields of this struct.
'''
fi: structs.FieldInfo
for fi in structs.fields(self):
key: str = fi.name
val: Any = getattr(self, key)
yield fi, key, val
def to_dict(
self,
include_non_members: bool = True,
) -> dict:
'''
Like it sounds.. direct delegation to:
https://jcristharif.com/msgspec/api.html#msgspec.structs.asdict
BUT, by default we pop all non-member (aka not defined as
struct fields) fields by default.
'''
asdict: dict = structs.asdict(self)
if include_non_members:
return asdict
# only return a dict of the struct members
# which were provided as input, NOT anything
# added as type-defined `@property` methods!
sin_props: dict = {}
fi: structs.FieldInfo
for fi, k, v in self._sin_props():
sin_props[k] = asdict[k]
return sin_props
def pformat(
self,
field_indent: int = 2,
indent: int = 0,
) -> str:
'''
Recursion-safe `pprint.pformat()` style formatting of
a `msgspec.Struct` for sane reading by a human using a REPL.
'''
# global whitespace indent
ws: str = ' '*indent
# field whitespace indent
field_ws: str = ' '*(field_indent + indent)
# qtn: str = ws + self.__class__.__qualname__
qtn: str = self.__class__.__qualname__
obj_str: str = '' # accumulator
fi: structs.FieldInfo
k: str
v: Any
for fi, k, v in self._sin_props():
# TODO: how can we prefer `Literal['option1', 'option2,
# ..]` over .__name__ == `Literal` but still get only the
# latter for simple types like `str | int | None` etc..?
ft: type = fi.type
typ_name: str = getattr(ft, '__name__', str(ft))
# recurse to get sub-struct's `.pformat()` output Bo
if isinstance(v, Struct):
val_str: str = v.pformat(
indent=field_indent + indent,
field_indent=indent + field_indent,
)
else: # the `pprint` recursion-safe format:
# https://docs.python.org/3.11/library/pprint.html#pprint.saferepr
val_str: str = saferepr(v)
obj_str += (field_ws + f'{k}: {typ_name} = {val_str},\n')
return (
f'{qtn}(\n'
f'{obj_str}'
f'{ws})'
)
# TODO: use a pprint.PrettyPrinter instance around ONLY rendering
# inside a known tty?
# def __repr__(self) -> str:
# ...
# __str__ = __repr__ = pformat
__repr__ = pformat
def copy(
self,
update: dict | None = None,
) -> Struct:
'''
Validate-typecast all self defined fields, return a copy of
us with all such fields.
NOTE: This is kinda like the default behaviour in
`pydantic.BaseModel` except a copy of the object is
returned making it compat with `frozen=True`.
'''
if update:
for k, v in update.items():
setattr(self, k, v)
# NOTE: roundtrip serialize to validate
# - enode to msgpack binary format,
# - decode that back to a struct.
return msgpack.Decoder(type=type(self)).decode(
msgpack.Encoder().encode(self)
)
def typecast(
self,
# TODO: allow only casting a named subset?
# fields: set[str] | None = None,
) -> None:
'''
Cast all fields using their declared type annotations
(kinda like what `pydantic` does by default).
NOTE: this of course won't work on frozen types, use
``.copy()`` above in such cases.
'''
# https://jcristharif.com/msgspec/api.html#msgspec.structs.fields
fi: structs.FieldInfo
for fi in structs.fields(self):
setattr(
self,
fi.name,
fi.type(getattr(self, fi.name)),
)
def __sub__(
self,
other: Struct,
) -> DiffDump[tuple[str, Any, Any]]:
'''
Compare fields/items key-wise and return a ``DiffDump``
for easy visual REPL comparison B)
'''
diffs: DiffDump[tuple[str, Any, Any]] = DiffDump()
for fi in structs.fields(self):
attr_name: str = fi.name
ours: Any = getattr(self, attr_name)
theirs: Any = getattr(other, attr_name)
if ours != theirs:
diffs.append((
attr_name,
ours,
theirs,
))
return diffs

View File

@ -33,7 +33,10 @@ from . import _search
from ..accounting import unpack_fqme
from ..data._symcache import open_symcache
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 ._exec import run_qtractor
@ -87,6 +90,13 @@ async def _async_main(
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
_chart._godw = main_widget

View File

@ -29,7 +29,6 @@ from typing import (
)
import pyqtgraph as pg
import trio
from piker.ui.qt import (
QtCore,
@ -41,6 +40,7 @@ from piker.ui.qt import (
QVBoxLayout,
QSplitter,
)
from ._widget import GodWidget
from ._axes import (
DynamicDateAxis,
PriceAxis,
@ -61,10 +61,6 @@ from ._style import (
_xaxis_at,
# _min_points_to_show,
)
from ..data.feed import (
Feed,
Flume,
)
from ..accounting import (
MktPair,
)
@ -78,305 +74,12 @@ from . import _pg_overrides as pgo
if TYPE_CHECKING:
from ._display import DisplayState
from ..data.flows import Flume
from ..data.feed import Feed
log = get_logger(__name__)
_godw: GodWidget|None = None
def get_godw() -> GodWidget:
'''
Get the top level "god widget", the root/central-most Qt
widget-object set as `QMainWindow.setCentralWidget(_godw)`.
See `piker.ui._exec` for the runtime init details and all the
machinery for running `trio` on the Qt event loop in guest mode.
'''
if _godw is None:
raise RuntimeError(
'No god-widget initialized ??\n'
'Have you called `run_qtractor()` yet?\n'
)
return _godw
class GodWidget(QWidget):
'''
"Our lord and savior, the holy child of window-shua, there is no
widget above thee." - 6|6
The highest level composed widget which contains layouts for
organizing charts as well as other sub-widgets used to control or
modify them.
'''
search: SearchWidget
mode_name: str = 'god'
def __init__(
self,
parent=None,
) -> None:
super().__init__(parent)
self.search: SearchWidget|None = None
self.hbox = QHBoxLayout(self)
self.hbox.setContentsMargins(0, 0, 0, 0)
self.hbox.setSpacing(6)
self.hbox.setAlignment(Qt.AlignTop)
self.vbox = QVBoxLayout()
self.vbox.setContentsMargins(0, 0, 0, 0)
self.vbox.setSpacing(2)
self.vbox.setAlignment(Qt.AlignTop)
self.hbox.addLayout(self.vbox)
self._chart_cache: dict[
str,
tuple[LinkedSplits, LinkedSplits],
] = {}
self.hist_linked: LinkedSplits|None = None
self.rt_linked: LinkedSplits|None = None
self._active_cursor: Cursor|None = None
# assigned in the startup func `_async_main()`
self._root_n: trio.Nursery = None
self._widgets: dict[str, QWidget] = {}
self._resizing: bool = False
# TODO: do we need this, when would god get resized
# and the window does not? Never right?!
# self.reg_for_resize(self)
# TODO: strat loader/saver that we don't need yet.
# def init_strategy_ui(self):
# self.toolbar_layout = QHBoxLayout()
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
# self.vbox.addLayout(self.toolbar_layout)
# self.strategy_box = StrategyBoxWidget(self)
# self.toolbar_layout.addWidget(self.strategy_box)
@property
def linkedsplits(self) -> LinkedSplits:
return self.rt_linked
def set_chart_symbols(
self,
group_key: tuple[str], # of form <fqme>.<providername>
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
) -> None:
# re-sort org cache symbol list in LIFO order
cache = self._chart_cache
cache.pop(group_key, None)
cache[group_key] = all_linked
def get_chart_symbols(
self,
symbol_key: str,
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
return self._chart_cache.get(symbol_key)
async def load_symbols(
self,
fqmes: list[str],
loglevel: str,
reset: bool = False,
) -> trio.Event:
'''
Load a new contract into the charting app.
Expects a ``numpy`` structured array containing all the ohlcv fields.
'''
# NOTE: for now we use the first symbol in the set as the "key"
# for the overlay of feeds on the chart.
group_key: tuple[str] = tuple(fqmes)
all_linked = self.get_chart_symbols(group_key)
order_mode_started = trio.Event()
if not self.vbox.isEmpty():
# XXX: seems to make switching slower?
# qframe = self.hist_linked.chart.qframe
# if qframe.sidepane is self.search:
# qframe.hbox.removeWidget(self.search)
for linked in [self.rt_linked, self.hist_linked]:
# XXX: this is CRITICAL especially with pixel buffer caching
linked.hide()
linked.unfocus()
# XXX: pretty sure we don't need this
# remove any existing plots?
# XXX: ahh we might want to support cache unloading..
# self.vbox.removeWidget(linked)
# switching to a new viewable chart
if all_linked is None or reset:
from ._display import display_symbol_data
# we must load a fresh linked charts set
self.rt_linked = rt_charts = LinkedSplits(self)
self.hist_linked = hist_charts = LinkedSplits(self)
# spawn new task to start up and update new sub-chart instances
self._root_n.start_soon(
display_symbol_data,
self,
fqmes,
loglevel,
order_mode_started,
)
# self.vbox.addWidget(hist_charts)
self.vbox.addWidget(rt_charts)
self.set_chart_symbols(
group_key,
(hist_charts, rt_charts),
)
for linked in [hist_charts, rt_charts]:
linked.show()
linked.focus()
await trio.sleep(0)
else:
# symbol is already loaded and ems ready
order_mode_started.set()
self.hist_linked, self.rt_linked = all_linked
for linked in all_linked:
# TODO:
# - we'll probably want per-instrument/provider state here?
# change the order config form over to the new chart
# chart is already in memory so just focus it
linked.show()
linked.focus()
linked.graphics_cycle()
await trio.sleep(0)
# resume feeds *after* rendering chart view asap
chart = linked.chart
if chart:
chart.resume_all_feeds()
# TODO: we need a check to see if the chart
# last had the xlast in view, if so then shift so it's
# still in view, if the user was viewing history then
# do nothing yah?
self.rt_linked.chart.main_viz.default_view(
do_min_bars=True,
)
# if a history chart instance is already up then
# set the search widget as its sidepane.
hist_chart = self.hist_linked.chart
if hist_chart:
hist_chart.qframe.set_sidepane(self.search)
# NOTE: this is really stupid/hard to follow.
# we have to reposition the active position nav
# **AFTER** applying the search bar as a sidepane
# to the newly switched to symbol.
await trio.sleep(0)
# TODO: probably stick this in some kinda `LooknFeel` API?
for tracker in self.rt_linked.mode.trackers.values():
pp_nav = tracker.nav
if tracker.live_pp.cumsize:
pp_nav.show()
pp_nav.hide_info()
else:
pp_nav.hide()
# set window titlebar info
symbol = self.rt_linked.mkt
if symbol is not None:
self.window.setWindowTitle(
f'{symbol.fqme} '
f'tick:{symbol.size_tick}'
)
return order_mode_started
def focus(self) -> None:
'''
Focus the top level widget which in turn focusses the chart
ala "view mode".
'''
# go back to view-mode focus (aka chart focus)
self.clearFocus()
chart = self.rt_linked.chart
if chart:
chart.setFocus()
def reg_for_resize(
self,
widget: QWidget,
) -> None:
getattr(widget, 'on_resize')
self._widgets[widget.mode_name] = widget
def on_win_resize(self, event: QtCore.QEvent) -> None:
'''
Top level god widget handler from window (the real yaweh) resize
events such that any registered widgets which wish to be
notified are invoked using our pythonic `.on_resize()` method
api.
Where we do UX magic to make things not suck B)
'''
if self._resizing:
return
self._resizing = True
log.info('God widget resize')
for name, widget in self._widgets.items():
widget.on_resize()
self._resizing = False
# on_resize = on_win_resize
def get_cursor(self) -> Cursor:
return self._active_cursor
def iter_linked(self) -> Iterator[LinkedSplits]:
for linked in [self.hist_linked, self.rt_linked]:
yield linked
def resize_all(self) -> None:
'''
Dynamic resize sequence: adjusts all sub-widgets/charts to
sensible default ratios of what space is detected as available
on the display / window.
'''
rt_linked = self.rt_linked
rt_linked.set_split_sizes()
self.rt_linked.resize_sidepanes()
self.hist_linked.resize_sidepanes(from_linked=rt_linked)
self.search.on_resize()
class ChartnPane(QFrame):
'''
One-off ``QFrame`` composite which pairs a chart
@ -438,7 +141,6 @@ class LinkedSplits(QWidget):
'''
def __init__(
self,
godwidget: GodWidget,
@ -1050,7 +752,7 @@ class ChartPlotWidget(pg.PlotWidget):
) -> None:
'''
Increment the data view ``datums``` steps toward y-axis thus
Increment the data view `datums`` steps toward y-axis thus
"following" the current time slot/step/bar.
'''
@ -1060,7 +762,7 @@ class ChartPlotWidget(pg.PlotWidget):
x_shift = viz.index_step() * datums
if datums >= 300:
print("FUCKING FIX THE GLOBAL STEP BULLSHIT")
log.warning('FUCKING FIX THE GLOBAL STEP BULLSHIT')
# breakpoint()
return

View File

@ -413,9 +413,18 @@ class Cursor(pg.GraphicsObject):
self,
item: pg.GraphicsObject,
) -> 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)
def is_hovered(
self,
item: pg.GraphicsObject,
) -> bool:
return item in self._hovered
def add_plot(
self,
plot: ChartPlotWidget, # noqa

View File

@ -45,7 +45,7 @@ from piker.ui.qt import QLineF
from ..data._sharedmem import (
ShmArray,
)
from ..data.feed import Flume
from ..data.flows import Flume
from ..data._formatters import (
IncrementalFormatter,
OHLCBarsFmtr, # Plain OHLC renderer

View File

@ -21,6 +21,7 @@ this module ties together quote and computational (fsp) streams with
graphics update methods via our custom ``pyqtgraph`` charting api.
'''
from functools import partial
import itertools
from math import floor
import time
@ -208,6 +209,7 @@ class DisplayState(Struct):
async def increment_history_view(
# min_istream: tractor.MsgStream,
ds: DisplayState,
loglevel: str = 'warning',
):
hist_chart: ChartPlotWidget = ds.hist_chart
hist_viz: Viz = ds.hist_viz
@ -229,7 +231,10 @@ async def increment_history_view(
hist_viz.reset_graphics()
# 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:
profiler = Profiler(
@ -310,7 +315,6 @@ async def increment_history_view(
async def graphics_update_loop(
dss: dict[str, DisplayState],
nurse: trio.Nursery,
godwidget: GodWidget,
@ -319,6 +323,7 @@ async def graphics_update_loop(
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {},
vlm_charts: dict[str, ChartPlotWidget] = {},
loglevel: str = 'warning',
) -> None:
'''
@ -462,9 +467,12 @@ async def graphics_update_loop(
# })
nurse.start_soon(
increment_history_view,
# min_istream,
ds,
partial(
increment_history_view,
# min_istream,
ds=ds,
loglevel=loglevel,
),
)
await trio.sleep(0)
@ -511,14 +519,19 @@ async def graphics_update_loop(
fast_chart.linked.isHidden()
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()
continue
ic = fast_chart.view._in_interact
if ic:
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()
fast_chart.resume_all_feeds()
@ -1591,15 +1604,18 @@ async def display_symbol_data(
# start update loop task
dss: dict[str, DisplayState] = {}
ln.start_soon(
graphics_update_loop,
dss,
ln,
godwidget,
feed,
# min_istream,
partial(
graphics_update_loop,
dss=dss,
nurse=ln,
godwidget=godwidget,
feed=feed,
# min_istream,
pis,
vlm_charts,
pis=pis,
vlm_charts=vlm_charts,
loglevel=loglevel,
)
)
# boot order-mode

View File

@ -55,6 +55,11 @@ from ._style import (
from ._lines import LevelLine
from ..log import get_logger
# TODO, rm the cycle here!
from ._widget import (
GodWidget,
)
if TYPE_CHECKING:
from ._chart import (
GodWidget,

View File

@ -56,7 +56,7 @@ from . import _style
if TYPE_CHECKING:
from ._chart import GodWidget
from ._widget import GodWidget
log = get_logger(__name__)

View File

@ -183,13 +183,17 @@ async def open_fsp_sidepane(
@acm
async def open_fsp_actor_cluster(
names: list[str] = ['fsp_0', 'fsp_1'],
names: list[str] = [
'fsp_0',
'fsp_1',
],
) -> AsyncGenerator[
int,
dict[str, tractor.Portal]
]:
# TODO! change to .experimental!
from tractor._clustering import open_actor_cluster
# profiler = Profiler(
@ -197,7 +201,7 @@ async def open_fsp_actor_cluster(
# disabled=False
# )
async with open_actor_cluster(
count=2,
count=len(names),
names=names,
modules=['piker.fsp._engine'],
@ -497,7 +501,8 @@ class FspAdmin:
portal: tractor.Portal = (
self.cluster.get(worker_name)
or self.rr_next_portal()
or
self.rr_next_portal()
)
# TODO: this should probably be turned into a

View File

@ -43,6 +43,7 @@ from pyqtgraph import (
functions as fn,
)
import numpy as np
import tractor
import trio
from piker.ui.qt import (
@ -72,7 +73,10 @@ if TYPE_CHECKING:
GodWidget,
)
from ._dataviz import Viz
from .order_mode import OrderMode
from .order_mode import (
OrderMode,
Dialog,
)
from ._display import DisplayState
@ -130,7 +134,12 @@ async def handle_viewmode_kb_inputs(
async for kbmsg in recv_chan:
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()
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
# met
if (
not fast_key_seq or
period <= min_tap and fast_key_seq
not fast_key_seq
or (
period <= min_tap
and
fast_key_seq
)
):
fast_key_seq.append(text)
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")
if (
ctrl
and key in {
and
key in {
Qt.Key_P,
}
):
@ -184,7 +198,6 @@ async def handle_viewmode_kb_inputs(
vlm_chart = chart.linked.subplots['volume'] # noqa
vlm_viz = vlm_chart.main_viz # noqa
dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa
import tractor
await tractor.pause()
view.interact_graphics_cycle()
@ -192,7 +205,8 @@ async def handle_viewmode_kb_inputs(
# shown data `Viz`s for the current chart app.
if (
ctrl
and key in {
and
key in {
Qt.Key_R,
}
):
@ -231,7 +245,8 @@ async def handle_viewmode_kb_inputs(
key == Qt.Key_Escape
or (
ctrl
and key == Qt.Key_C
and
key == Qt.Key_C
)
):
# ctrl-c as cancel
@ -242,17 +257,35 @@ async def handle_viewmode_kb_inputs(
# cancel order or clear graphics
if (
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
if (
ctrl
and (
key == Qt.Key_Equal
or key == Qt.Key_I
or
key == Qt.Key_I
)
):
view.wheelEvent(
@ -264,7 +297,8 @@ async def handle_viewmode_kb_inputs(
ctrl
and (
key == Qt.Key_Minus
or key == Qt.Key_O
or
key == Qt.Key_O
)
):
view.wheelEvent(
@ -275,7 +309,8 @@ async def handle_viewmode_kb_inputs(
elif (
not ctrl
and key == Qt.Key_R
and
key == Qt.Key_R
):
# NOTE: seems that if we don't yield a Qt render
# cycle then the m4 downsampled curves will show here
@ -477,7 +512,8 @@ async def handle_viewmode_mouse(
# view.raiseContextMenu(event)
if (
view.order_mode.active and
view.order_mode.active
and
button == QtCore.Qt.LeftButton
):
# when in order mode, submit execution
@ -781,7 +817,8 @@ class ChartView(ViewBox):
# Scale or translate based on mouse button
if btn & (
QtCore.Qt.LeftButton | QtCore.Qt.MidButton
QtCore.Qt.LeftButton
| QtCore.Qt.MidButton
):
# zoom y-axis ONLY when click-n-drag on it
# if axis == 1:

View File

@ -52,10 +52,13 @@ from ._anchors import (
from ..calc import humanize
from ._label import Label
from ._style import hcolor, _font
from ..log import get_logger
if TYPE_CHECKING:
from ._cursor import Cursor
log = get_logger(__name__)
# TODO: probably worth investigating if we can
# make .boundingRect() faster:
@ -347,7 +350,7 @@ class LevelLine(pg.InfiniteLine):
) -> None:
# TODO: enter labels edit mode
print(f'double click {ev}')
log.debug(f'double click {ev}')
def paint(
self,
@ -461,10 +464,19 @@ class LevelLine(pg.InfiniteLine):
# hovered
if (
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 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
if self.only_show_markers_on_hover:
@ -481,6 +493,7 @@ class LevelLine(pg.InfiniteLine):
cur._y_label_update = False
# add us to cursor state
log.debug(f'Adding line {self!r}\n')
cur.add_hovered(self)
if self._hide_xhair_on_hover:
@ -508,6 +521,7 @@ class LevelLine(pg.InfiniteLine):
self.currentPen = self.pen
log.debug(f'Removing line {self!r}\n')
cur._hovered.remove(self)
if self.only_show_markers_on_hover:

352
piker/ui/_widget.py 100644
View File

@ -0,0 +1,352 @@
# 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/>.
'''
Root-most (what they call a "central widget") of every Qt-UI-app's
window.
'''
from __future__ import annotations
from typing import (
Iterator,
TYPE_CHECKING,
)
import trio
from piker.ui.qt import (
QtCore,
Qt,
QWidget,
QHBoxLayout,
QVBoxLayout,
)
from ..log import get_logger
if TYPE_CHECKING:
from ._search import SearchWidget
from ._chart import (
LinkedSplits,
)
from ._cursor import (
Cursor,
)
log = get_logger(__name__)
_godw: GodWidget|None = None
def get_godw() -> GodWidget:
'''
Get the top level "god widget", the root/central-most Qt
widget-object set as `QMainWindow.setCentralWidget(_godw)`.
See `piker.ui._exec` for the runtime init details and all the
machinery for running `trio` on the Qt event loop in guest mode.
'''
if _godw is None:
raise RuntimeError(
'No god-widget initialized ??\n'
'Have you called `run_qtractor()` yet?\n'
)
return _godw
class GodWidget(QWidget):
'''
"Our lord and savior, the holy child of window-shua, there is no
widget above thee." - 6|6
The highest level composed widget which contains layouts for
organizing charts as well as other sub-widgets used to control or
modify them.
'''
search: SearchWidget
mode_name: str = 'god'
def __init__(
self,
parent=None,
) -> None:
super().__init__(parent)
self.search: SearchWidget|None = None
self.hbox = QHBoxLayout(self)
self.hbox.setContentsMargins(0, 0, 0, 0)
self.hbox.setSpacing(6)
self.hbox.setAlignment(Qt.AlignTop)
self.vbox = QVBoxLayout()
self.vbox.setContentsMargins(0, 0, 0, 0)
self.vbox.setSpacing(2)
self.vbox.setAlignment(Qt.AlignTop)
self.hbox.addLayout(self.vbox)
self._chart_cache: dict[
str,
tuple[LinkedSplits, LinkedSplits],
] = {}
self.hist_linked: LinkedSplits|None = None
self.rt_linked: LinkedSplits|None = None
self._active_cursor: Cursor|None = None
# assigned in the startup func `_async_main()`
self._root_n: trio.Nursery = None
self._widgets: dict[str, QWidget] = {}
self._resizing: bool = False
# TODO: do we need this, when would god get resized
# and the window does not? Never right?!
# self.reg_for_resize(self)
# TODO: strat loader/saver that we don't need yet.
# def init_strategy_ui(self):
# self.toolbar_layout = QHBoxLayout()
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
# self.vbox.addLayout(self.toolbar_layout)
# self.strategy_box = StrategyBoxWidget(self)
# self.toolbar_layout.addWidget(self.strategy_box)
@property
def linkedsplits(self) -> LinkedSplits:
return self.rt_linked
def set_chart_symbols(
self,
group_key: tuple[str], # of form <fqme>.<providername>
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
) -> None:
# re-sort org cache symbol list in LIFO order
cache = self._chart_cache
cache.pop(group_key, None)
cache[group_key] = all_linked
def get_chart_symbols(
self,
symbol_key: str,
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
return self._chart_cache.get(symbol_key)
async def load_symbols(
self,
fqmes: list[str],
loglevel: str,
reset: bool = False,
) -> trio.Event:
'''
Load a new contract into the charting app.
Expects a ``numpy`` structured array containing all the ohlcv fields.
'''
# NOTE: for now we use the first symbol in the set as the "key"
# for the overlay of feeds on the chart.
group_key: tuple[str] = tuple(fqmes)
all_linked = self.get_chart_symbols(group_key)
order_mode_started = trio.Event()
if not self.vbox.isEmpty():
# XXX: seems to make switching slower?
# qframe = self.hist_linked.chart.qframe
# if qframe.sidepane is self.search:
# qframe.hbox.removeWidget(self.search)
for linked in [self.rt_linked, self.hist_linked]:
# XXX: this is CRITICAL especially with pixel buffer caching
linked.hide()
linked.unfocus()
# XXX: pretty sure we don't need this
# remove any existing plots?
# XXX: ahh we might want to support cache unloading..
# self.vbox.removeWidget(linked)
# switching to a new viewable chart
if all_linked is None or reset:
from ._display import display_symbol_data
# we must load a fresh linked charts set
from ._chart import LinkedSplits
self.rt_linked = rt_charts = LinkedSplits(self)
self.hist_linked = hist_charts = LinkedSplits(self)
# spawn new task to start up and update new sub-chart instances
self._root_n.start_soon(
display_symbol_data,
self,
fqmes,
loglevel,
order_mode_started,
)
# self.vbox.addWidget(hist_charts)
self.vbox.addWidget(rt_charts)
self.set_chart_symbols(
group_key,
(hist_charts, rt_charts),
)
for linked in [hist_charts, rt_charts]:
linked.show()
linked.focus()
await trio.sleep(0)
else:
# symbol is already loaded and ems ready
order_mode_started.set()
self.hist_linked, self.rt_linked = all_linked
for linked in all_linked:
# TODO:
# - we'll probably want per-instrument/provider state here?
# change the order config form over to the new chart
# chart is already in memory so just focus it
linked.show()
linked.focus()
linked.graphics_cycle()
await trio.sleep(0)
# resume feeds *after* rendering chart view asap
chart = linked.chart
if chart:
chart.resume_all_feeds()
# TODO: we need a check to see if the chart
# last had the xlast in view, if so then shift so it's
# still in view, if the user was viewing history then
# do nothing yah?
self.rt_linked.chart.main_viz.default_view(
do_min_bars=True,
)
# if a history chart instance is already up then
# set the search widget as its sidepane.
hist_chart = self.hist_linked.chart
if hist_chart:
hist_chart.qframe.set_sidepane(self.search)
# NOTE: this is really stupid/hard to follow.
# we have to reposition the active position nav
# **AFTER** applying the search bar as a sidepane
# to the newly switched to symbol.
await trio.sleep(0)
# TODO: probably stick this in some kinda `LooknFeel` API?
for tracker in self.rt_linked.mode.trackers.values():
pp_nav = tracker.nav
if tracker.live_pp.cumsize:
pp_nav.show()
pp_nav.hide_info()
else:
pp_nav.hide()
# set window titlebar info
symbol = self.rt_linked.mkt
if symbol is not None:
self.window.setWindowTitle(
f'{symbol.fqme} '
f'tick:{symbol.size_tick}'
)
return order_mode_started
def focus(self) -> None:
'''
Focus the top level widget which in turn focusses the chart
ala "view mode".
'''
# go back to view-mode focus (aka chart focus)
self.clearFocus()
chart = self.rt_linked.chart
if chart:
chart.setFocus()
def reg_for_resize(
self,
widget: QWidget,
) -> None:
getattr(widget, 'on_resize')
self._widgets[widget.mode_name] = widget
def on_win_resize(
self,
event: QtCore.QEvent,
) -> None:
'''
Top level god widget handler from window (the real yaweh) resize
events such that any registered widgets which wish to be
notified are invoked using our pythonic `.on_resize()` method
api.
Where we do UX magic to make things not suck B)
'''
if self._resizing:
return
self._resizing = True
log.debug(
f'God widget resize\n'
f'{event}\n'
)
for name, widget in self._widgets.items():
widget.on_resize()
self._resizing = False
# on_resize = on_win_resize
def get_cursor(self) -> Cursor:
return self._active_cursor
def iter_linked(self) -> Iterator[LinkedSplits]:
for linked in [self.hist_linked, self.rt_linked]:
yield linked
def resize_all(self) -> None:
'''
Dynamic resize sequence: adjusts all sub-widgets/charts to
sensible default ratios of what space is detected as available
on the display / window.
'''
rt_linked = self.rt_linked
rt_linked.set_split_sizes()
self.rt_linked.resize_sidepanes()
self.hist_linked.resize_sidepanes(from_linked=rt_linked)
self.search.on_resize()

View File

@ -40,7 +40,7 @@ from piker.ui.qt import (
)
from ..log import get_logger
from ._style import _font_small, hcolor
from ._chart import GodWidget
from ._widget import GodWidget
log = get_logger(__name__)
@ -255,8 +255,16 @@ class MainWindow(QMainWindow):
current: QWidget,
) -> 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:
# cursor left window?

View File

@ -177,7 +177,7 @@ def chart(
return
# global opts
brokernames = config['brokers']
# brokernames: list[str] = config['brokers']
brokermods = config['brokermods']
assert brokermods
tractorloglevel = config['tractorloglevel']
@ -216,6 +216,7 @@ def chart(
layers['tcp']['port'],
))
# breakpoint()
from tractor.devx import maybe_open_crash_handler
pdb: bool = config['pdb']
with maybe_open_crash_handler(pdb=pdb):

View File

@ -77,7 +77,6 @@ from ._style import _font
from ._forms import open_form_input_handling
from ._notify import notify_from_ems_status_msg
if TYPE_CHECKING:
from ._chart import (
ChartPlotWidget,
@ -436,7 +435,7 @@ class OrderMode:
lines=lines,
last_status_close=self.multistatus.open_status(
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,
)
)
@ -514,13 +513,14 @@ class OrderMode:
def on_submit(
self,
uuid: str,
order: Order | None = None,
order: Order|None = None,
) -> Dialog | None:
) -> Dialog|None:
'''
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)
@ -528,7 +528,7 @@ class OrderMode:
# a submission is the start of a new order dialog
dialog = self.dialogs[uuid]
dialog.lines = lines
cls: Callable | None = dialog.last_status_close
cls: Callable|None = dialog.last_status_close
if cls:
cls()
@ -658,7 +658,7 @@ class OrderMode:
return True
def cancel_orders_under_cursor(self) -> list[str]:
def cancel_orders_under_cursor(self) -> list[Dialog]:
return self.cancel_orders(
self.oids_from_lines(
self.lines.lines_under_cursor()
@ -687,24 +687,28 @@ class OrderMode:
self,
oids: list[str],
) -> None:
) -> list[Dialog]:
'''
Cancel all orders from a list of order ids: `oids`.
'''
key = self.multistatus.open_status(
f'cancelling {len(oids)} orders',
final_msg=f'cancelled orders:\n{oids}',
group_key=True
)
# key = self.multistatus.open_status(
# f'cancelling {len(oids)} orders',
# final_msg=f'cancelled orders:\n{oids}',
# group_key=True
# )
dialogs: list[Dialog] = []
for oid in oids:
if dialog := self.dialogs.get(oid):
self.client.cancel_nowait(uuid=oid)
cancel_status_close = self.multistatus.open_status(
f'cancelling order {oid}',
group_key=key,
)
dialog.last_status_close = cancel_status_close
# cancel_status_close = self.multistatus.open_status(
# f'cancelling order {oid}',
# group_key=key,
# )
# dialog.last_status_close = cancel_status_close
dialogs.append(dialog)
return dialogs
def cancel_all_orders(self) -> None:
'''
@ -776,7 +780,6 @@ class OrderMode:
@asynccontextmanager
async def open_order_mode(
feed: Feed,
godw: GodWidget,
fqme: str,

14
uv.lock
View File

@ -1159,11 +1159,11 @@ uis = [
[[package]]
name = "platformdirs"
version = "4.5.1"
version = "4.6.0"
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 = [
{ 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]]
@ -1904,7 +1904,7 @@ dev = [
{ name = "pytest", specifier = ">=8.3.5" },
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
{ name = "typing-extensions", specifier = ">=4.14.1" },
{ name = "xonsh", specifier = ">=0.19.2" },
{ name = "xonsh", specifier = ">=0.22.2" },
]
devx = [
{ name = "greenback", specifier = ">=1.2.1,<2" },
@ -1916,7 +1916,7 @@ repl = [
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "pyperclip", specifier = ">=1.9.0" },
{ name = "xonsh", specifier = ">=0.19.2" },
{ name = "xonsh", specifier = ">=0.22.2" },
]
testing = [
{ name = "pexpect", specifier = ">=4.9.0,<5" },
@ -2162,8 +2162,8 @@ wheels = [
[[package]]
name = "xonsh"
version = "0.22.1"
source = { git = "https://github.com/xonsh/xonsh.git?branch=main#336658ff0919f8d7bb96d581136d37d470a8fe99" }
version = "0.22.3"
source = { git = "https://github.com/xonsh/xonsh.git?branch=main#b446946fd94c3913e002318db1d1b41ee4fa1f9a" }
[[package]]
name = "yapic-json"