Compare commits

...

37 Commits

Author SHA1 Message Date
Gud Boi 47cd48fea7 Pin `pg` at latest official `0.14.0` release
Keep in masked GH sources lines for easy hackin against upstream
`master` branch when needed as well!
2026-02-24 12:31:52 -05:00
Gud Boi 6b6eb82c21 .ui._editors: log multiline styling and re-leveling 2026-02-24 12:31:52 -05:00
Gud Boi 4d327d4c7a .ui._lines: drop unused graphics-item import 2026-02-24 12:31:52 -05:00
Gud Boi d36de5910b Add batch-submit API for gap annotations
Introduce `AnnotCtl.add_batch()` and `serve_rc_annots()` batch
handler to submit 1000s of gaps in single IPC msg instead of
per-annot round-trips. Server builds `GapAnnotations` from specs
and handles vectorized timestamp-to-index lookups.

Deats,
- add `'cmd': 'batch'` handler in `serve_rc_annots()`
- vectorized timestamp lookup via `np.searchsorted()` + masking
- build `gap_specs: list[dict]` from rect+arrow specs client-side
- create single `GapAnnotations` item for all gaps server-side
- handle `GapAnnotations.reposition()` in redraw handler
- add profiling to batch path for perf measurement
- support optional individual arrows for A/B comparison

Also,
- refactor `markup_gaps()` to collect specs + single batch call
- add `no_qt_updates()` context mgr for batch render ops
- add profiling to annotation teardown path
- add `GapAnnotations` case to `rm_annot()` match block

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-24 12:31:52 -05:00
Gud Boi c299a08a06 Add a `GapAnnotations` path-renderer
For a ~1000x perf gain says ol' claudy, our boi who wrote this entire
patch! Bo

Introduce `GapAnnotations` in `.ui._annotate` for batch-rendering gap
rects/arrows instead of individual `QGraphicsItem` instances. Uses
upstream's `pyqtgraph.Qt.internals.PrimitiveArray` for rects and
a `QPainterPath` for arrows. This API-replicates our prior annotator's
in view shape-graphics but now using (what we're dubbing)
"single-array-multiple-graphics" tech much like our `.ui._curve`
extensions to `pg` B)

Impl deats,
- batch draw ~1000 gaps in single paint call vs 1000 items
- arrows render in scene coords to maintain pixel size on zoom
- add vectorized timestamp-to-index lookup for repositioning
- cache bounding rect, rebuild on `reposition()` calls
- match `SelectRect` + `ArrowItem` visual style/colors
- skip reposition when timeframe doesn't match gap's period

Other,
- fix typo in `LevelMarker` docstring: "graphich" -> "graphic"
- reflow docstring in `qgo_draw_markers()` to 67 char limit

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-24 12:31:52 -05:00
Gud Boi 897db81305 Add info log for shm processing in `ldshm` CLI cmd
Log shm file name and detected period before null segment
processing to aid debugging.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-02-24 12:31:52 -05:00
Gud Boi 56bc3596bb Bump to latest official `pyqtgraph` release 2026-02-24 12:31:52 -05:00
Gud Boi 9a720f8e21 Merge pull request 'ib_venue_closures: gap detection for "legacy mkts"' (#71)
Reviewed-on: https://www.pikers.dev/pikers/piker/pulls/71
2026-02-24 17:31:13 +00:00
Gud Boi d17e6ab5d9 Add holiday-gap detection via `exchange_calendars`
Integrate `exchange_calendars` lib to detect market holidays in
gap-checking logic via new `.ib.venues.has_holiday()` helper!

The `.ib.venues` impl deats,
- add  a new `has_holiday()` using `xcals.get_calendar()` and friends
  for sanity checking a venue's holiday closure-gaps.
  * final holiday detection-check is basically,
   `(cash_gap := (next_open - prev_close)) > period`
- include `time_step_s` param to `is_venue_closure()` for boundary
  tolerance checks.
  * let's us expand closure-time checks to include `+/-time_step_s`
    "off-by-one-`timeframe`-sample" edge case ranges.
- add real docstring to `has_weekend()`.

In `.ib.api` refine usage for ^ changes,
- move `is_venue_open()` call + tz-convert outside gap check
- use a walrus to capture `has_closure_gap` from `is_venue_closure()`
- add a `not has_closure_gap` condition to the
  mismatched-duration/short-frame warning block to avoid needless warns.
- keep duration-based "short-frame" log as `.error()` but toss in a bp
  so (somone can) umask to figure out wtf is going on..
  * we should **never** really hit this path unless there's a valid bug
    or data issue with IB/GFIS!
  * keep recursion path masked-out just leave a `breakpoint()` for now.

Also some logger updates,
- import `get_logger()` from top-level `piker.log` vs `.ib._util` which
  was always kinda wrong..
- change `NonShittyIB._logger` to use `__name__` vs literal.

(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-23 13:41:00 -05:00
Gud Boi cd0c780d04 Add `exchange_calendar` dep for venue-closure gap checkin 2026-02-23 13:41:00 -05:00
Gud Boi 1417c49051 Adjust type annots in binance and IB symbol mods
Namely, again switching `|`-union syntax to rm adjacent white space.

Also, flip to multiline style for threshold comparison in
`.binance.feed` and change gap-check threshold to `timeframe` (vs
a hardcoded `60`s) in the `get_ohlc()` closure.

(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-23 13:41:00 -05:00
Gud Boi 044afb0f6e Use `.ib.venues.is_venue_open()` in `.feed`
In `.ib.feed.stream_quotes()` specifically that is since time-range
checking code was moved to the new sub-mod.

Deats,
- drop import of old `is_current_time_in_range()` from `._util`
- change `get_bars()` sig: `end_dt`/`start_dt` to `datetime|None`
- comment-out `breakpoint()` in `open_history_client()`

Styling,
- add multiline style to conditionals and tuple unpacks
- fix type annotation: `Contract|None` vs `Contract | None`
- fix backticks in comment: `ib_insync` vs `ib_async`

(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-23 13:41:00 -05:00
Gud Boi c96ecdab75 Add venue-closure gap-detection in `.ib.api.Client.bars()`
With all detection logic coming from our new `.ib.venues` helpers
allowing use to verify IB's OHLC bars frames don't contain unexpected
time-gaps.

`Client.bars()` new checking deats,
- add `is_venue_open()`, `has_weekend()`, `sesh_times()`, and
  `is_venue_closure()` checks when `last_dt < end_dt`
- always calc gap-period in local tz via `ContractDetails.timeZoneId`.
- log warnings on invalid non-closure gaps, debug on closures for now.
- change recursion case to just `log.error()` + `breakpoint()`; we might end
  up tossing it since i don't think i could ever get it to be reliable..
  * mask-out recursive `.bars()` call (likely unnecessary).
- flip `start_dt`/`end_dt` param defaults to `None` vs epoch `str`.
- update docstring to clarify no `start_dt` support by IB
- add mod level `_iso8601_epoch_in_est` const to keep track of orig
  param default value.
- add multiline style to return type-annot, type all `pendulum` objects.

Also,
- uppercase `Crypto.symbol` for PAXOS contracts in `.find_contracts()`,
  tho now we're getting a weird new API error i left in a todo-comment..

(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-23 13:41:00 -05:00
Gud Boi e1e59453a9 Mv `parse_trading_hours()` from `._util` to `.venues`
It was an AI-model draft that we can prolly toss but figured might as
well org it appropriately.

(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-23 13:41:00 -05:00
Gud Boi d784af9df9 Add `.ib.venues` for mkt-venue-closure checkers
Introduce set of helper-fns for detecting venue open/close status,
session start/end times, and related time-gap detection using
`pendulum`.

Deats,
- add `iter_sessions()` to yield `pendulum.Interval`s from
  a `ContractDetails` instance.
- add `is_venue_open()` to check if active at a given time.
- add `is_venue_closure()` to detect valid closure gaps.
- add `sesh_times()` to extract weekday-agnostic open/close times.
- add `has_weekend()` to check for Sat/Sun in interval.
- move in lowlevel `is_current_time_in_range()` for checking a
  datetime within a `sesh: pendulum.Interval`.

(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-23 13:41:00 -05:00
Gud Boi cabd3fde92 Merge pull request 'fix_tractor_logging: porting to latest `tractor.log` API(s)' (#76)
Reviewed-on: #76
2026-02-23 18:40:35 +00:00
Gud Boi 2d0005ce48 Drop info-level `.pause()`-es used while devving 2026-02-23 13:26:46 -05:00
Gud Boi d0add050b7 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-23 12:22:48 -05:00
Gud Boi 709bc8a5be Bump `platformdirs` version in lock file 2026-02-23 12:22:48 -05:00
Gud Boi c7979d0100 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-23 12:22:48 -05:00
Gud Boi 9a97c477e2 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-23 12:22:48 -05:00
Gud Boi 2516d97fe4 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-23 12:22:48 -05:00
Gud Boi 5bfc9d46e1 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-23 12:22:48 -05:00
Gud Boi aa403bd390 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-23 12:22:48 -05:00
Gud Boi c1530c7a37 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-23 12:22:48 -05:00
Gud Boi 50ffc1095b 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-23 12:22:48 -05:00
Gud Boi 437d87ab5f 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-23 12:22:48 -05:00
Gud Boi 0087cc8876 .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-23 12:22:48 -05:00
Gud Boi 034fa19372 .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-23 12:22:48 -05:00
Gud Boi 0f0bbd1cda 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-23 12:22:48 -05:00
Gud Boi 3b6484c340 .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-23 12:22:48 -05:00
Gud Boi 6d896eeed2 .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-23 12:22:48 -05:00
Gud Boi bdedb16cdc 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-23 12:22:48 -05:00
Gud Boi d8bfdd775c 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-23 12:22:48 -05:00
Gud Boi 73369fb1ef Merge pull request 'hist_backfill_fixes: working-around (some) conc issues in the tsdb backfiller' (#62)
Reviewed-on: #62
Reviewed-by: momo <dilarayalniz1@gmail.com>
2026-02-23 17:22:24 +00:00
Gud Boi 8dd969e85f Pin to min `xonsh` release for @goodboy needs 2026-02-23 12:17:04 -05:00
Gud Boi 90fce9fcd4 Woops, use `piker_pin` from GH for `tractor`
Also, install the `'repl'` deps-group by default to ensure we get the
extras required by `tractor` for non-`trio` task debug REPLin..
Bump lock file to match.
2026-02-22 23:37:32 -05:00
59 changed files with 2365 additions and 580 deletions

View File

@ -19,8 +19,10 @@
for tendiez. for tendiez.
''' '''
from ..log import get_logger from piker.log import (
get_console_log,
get_logger,
)
from .calc import ( from .calc import (
iter_by_dt, iter_by_dt,
) )
@ -51,7 +53,17 @@ from ._allocate import (
log = get_logger(__name__) log = get_logger(__name__)
# ?TODO, enable console on import
# [ ] necessary? or `open_brokerd_dialog()` doing it is sufficient?
#
# bc might as well enable whenev imported by
# other sub-sys code (namely `.clearing`).
get_console_log(
level='warning',
name=__name__,
)
# TODO, the `as <samename>` style?
__all__ = [ __all__ = [
'Account', 'Account',
'Allocator', 'Allocator',

View File

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

View File

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

View File

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

View File

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

View File

@ -19,15 +19,13 @@ Handy cross-broker utils.
""" """
from __future__ import annotations from __future__ import annotations
from functools import partial # from functools import partial
import json import json
import httpx import httpx
import logging import logging
from ..log import ( from piker.log import (
get_logger,
get_console_log,
colorize_json, colorize_json,
) )
subsys: str = 'piker.brokers' subsys: str = 'piker.brokers'
@ -35,12 +33,22 @@ subsys: str = 'piker.brokers'
# NOTE: level should be reset by any actor that is spawned # NOTE: level should be reset by any actor that is spawned
# as well as given a (more) explicit name/key such # as well as given a (more) explicit name/key such
# as `piker.brokers.binance` matching the subpkg. # as `piker.brokers.binance` matching the subpkg.
log = get_logger(subsys) # log = get_logger(subsys)
get_console_log = partial( # ?TODO?? we could use this approach, but we need to be able
get_console_log, # to pass multiple `name=` values so for example we can include the
name=subsys, # emissions in `.accounting._pos` and others!
) # [ ] maybe we could do the `log = get_logger()` above,
# then cycle through the list of subsys mods we depend on
# and then get all their loggers and pass them to
# `get_console_log(logger=)`??
# [ ] OR just write THIS `get_console_log()` as a hook which does
# that based on who calls it?.. i dunno
#
# get_console_log = partial(
# get_console_log,
# name=subsys,
# )
class BrokerError(Exception): class BrokerError(Exception):

View File

@ -37,8 +37,9 @@ import trio
from piker.accounting import ( from piker.accounting import (
Asset, Asset,
) )
from piker.brokers._util import ( from piker.log import (
get_logger, get_logger,
get_console_log,
) )
from piker.data._web_bs import ( from piker.data._web_bs import (
open_autorecon_ws, open_autorecon_ws,
@ -69,7 +70,9 @@ from .venues import (
) )
from .api import Client from .api import Client
log = get_logger('piker.brokers.binance') log = get_logger(
name=__name__,
)
# Fee schedule template, mostly for paper engine fees modelling. # Fee schedule template, mostly for paper engine fees modelling.
@ -245,9 +248,16 @@ async def handle_order_requests(
@tractor.context @tractor.context
async def open_trade_dialog( async def open_trade_dialog(
ctx: tractor.Context, ctx: tractor.Context,
loglevel: str = 'warning',
) -> AsyncIterator[dict[str, Any]]: ) -> AsyncIterator[dict[str, Any]]:
# enable piker.clearing console log for *this* `brokerd` subactor
get_console_log(
level=loglevel,
name=__name__,
)
# TODO: how do we set this from the EMS such that # TODO: how do we set this from the EMS such that
# positions are loaded from the correct venue on the user # positions are loaded from the correct venue on the user
# stream at startup? (that is in an attempt to support both # stream at startup? (that is in an attempt to support both

View File

@ -64,9 +64,9 @@ from piker.data._web_bs import (
open_autorecon_ws, open_autorecon_ws,
NoBsWs, NoBsWs,
) )
from piker.log import get_logger
from piker.brokers._util import ( from piker.brokers._util import (
DataUnavailable, DataUnavailable,
get_logger,
) )
from .api import ( from .api import (
@ -78,7 +78,7 @@ from .venues import (
get_api_eps, get_api_eps,
) )
log = get_logger('piker.brokers.binance') log = get_logger(name=__name__)
class L1(Struct): class L1(Struct):
@ -237,8 +237,8 @@ async def open_history_client(
async def get_ohlc( async def get_ohlc(
timeframe: float, timeframe: float,
end_dt: datetime | None = None, end_dt: datetime|None = None,
start_dt: datetime | None = None, start_dt: datetime|None = None,
) -> tuple[ ) -> tuple[
np.ndarray, np.ndarray,
@ -297,7 +297,7 @@ async def open_history_client(
async def get_mkt_info( async def get_mkt_info(
fqme: str, fqme: str,
) -> tuple[MktPair, Pair] | None: ) -> tuple[MktPair, Pair]|None:
# uppercase since kraken bs_mktid is always upper # uppercase since kraken bs_mktid is always upper
if 'binance' not in fqme.lower(): if 'binance' not in fqme.lower():
@ -374,7 +374,7 @@ async def get_mkt_info(
if 'futes' in mkt_mode: if 'futes' in mkt_mode:
assert isinstance(pair, FutesPair) assert isinstance(pair, FutesPair)
dst: Asset | None = assets.get(pair.bs_dst_asset) dst: Asset|None = assets.get(pair.bs_dst_asset)
if ( if (
not dst not dst
# TODO: a known asset DNE list? # TODO: a known asset DNE list?
@ -433,7 +433,7 @@ async def subscribe(
# might get ack from ws server, or maybe some # might get ack from ws server, or maybe some
# other msg still in transit.. # other msg still in transit..
res = await ws.recv_msg() res = await ws.recv_msg()
subid: str | None = res.get('id') subid: str|None = res.get('id')
if subid: if subid:
assert res['id'] == subid assert res['id'] == subid

View File

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

View File

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

View File

@ -41,12 +41,15 @@ import tractor
from tractor.experimental import msgpub from tractor.experimental import msgpub
from async_generator import asynccontextmanager from async_generator import asynccontextmanager
from ._util import ( from piker.log import(
log, get_logger,
get_console_log, get_console_log,
) )
from . import get_brokermod from . import get_brokermod
log = get_logger(
name='piker.brokers.binance',
)
async def wait_for_network( async def wait_for_network(
net_func: Callable, net_func: Callable,
@ -243,7 +246,10 @@ async def start_quote_stream(
''' '''
# XXX: why do we need this again? # XXX: why do we need this again?
get_console_log(tractor.current_actor().loglevel) get_console_log(
level=tractor.current_actor().loglevel,
name=__name__,
)
# pull global vars from local actor # pull global vars from local actor
symbols = list(symbols) symbols = list(symbols)

View File

@ -34,13 +34,13 @@ import subprocess
import tractor import tractor
from piker.brokers._util import get_logger from piker.log import get_logger
if TYPE_CHECKING: if TYPE_CHECKING:
from .api import Client from .api import Client
import i3ipc import i3ipc
log = get_logger('piker.brokers.ib') log = get_logger(name=__name__)
_reset_tech: Literal[ _reset_tech: Literal[
'vnc', 'vnc',
@ -326,7 +326,6 @@ def i3ipc_fin_wins_titled(
) )
def i3ipc_xdotool_manual_click_hack() -> None: def i3ipc_xdotool_manual_click_hack() -> None:
''' '''
Do the data reset hack but expecting a local X-window using `xdotool`. Do the data reset hack but expecting a local X-window using `xdotool`.
@ -388,99 +387,3 @@ def i3ipc_xdotool_manual_click_hack() -> None:
]) ])
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
log.exception('xdotool timed out?') log.exception('xdotool timed out?')
def is_current_time_in_range(
start_dt: datetime,
end_dt: datetime,
) -> bool:
'''
Check if current time is within the datetime range.
Use any/the-same timezone as provided by `start_dt.tzinfo` value
in the range.
'''
now: datetime = datetime.now(start_dt.tzinfo)
return start_dt <= now <= end_dt
# TODO, put this into `._util` and call it from here!
#
# NOTE, this was generated by @guille from a gpt5 prompt
# and was originally thot to be needed before learning about
# `ib_insync.contract.ContractDetails._parseSessions()` and
# it's downstream meths..
#
# This is still likely useful to keep for now to parse the
# `.tradingHours: str` value manually if we ever decide
# to move off `ib_async` and implement our own `trio`/`anyio`
# based version Bp
#
# >attempt to parse the retarted ib "time stampy thing" they
# >do for "venue hours" with this.. written by
# >gpt5-"thinking",
#
def parse_trading_hours(
spec: str,
tz: TzInfo|None = None
) -> dict[
date,
tuple[datetime, datetime]
]|None:
'''
Parse venue hours like:
'YYYYMMDD:HHMM-YYYYMMDD:HHMM;YYYYMMDD:CLOSED;...'
Returns `dict[date] = (open_dt, close_dt)` or `None` if
closed.
'''
if (
not isinstance(spec, str)
or
not spec
):
raise ValueError('spec must be a non-empty string')
out: dict[
date,
tuple[datetime, datetime]
]|None = {}
for part in (p.strip() for p in spec.split(';') if p.strip()):
if part.endswith(':CLOSED'):
day_s, _ = part.split(':', 1)
d = datetime.strptime(day_s, '%Y%m%d').date()
out[d] = None
continue
try:
start_s, end_s = part.split('-', 1)
start_dt = datetime.strptime(start_s, '%Y%m%d:%H%M')
end_dt = datetime.strptime(end_s, '%Y%m%d:%H%M')
except ValueError as exc:
raise ValueError(f'invalid segment: {part}') from exc
if tz is not None:
start_dt = start_dt.replace(tzinfo=tz)
end_dt = end_dt.replace(tzinfo=tz)
out[start_dt.date()] = (start_dt, end_dt)
return out
# ORIG desired usage,
#
# TODO, for non-drunk tomorrow,
# - call above fn and check that `output[today] is not None`
# trading_hrs: dict = parse_trading_hours(
# details.tradingHours
# )
# liq_hrs: dict = parse_trading_hours(
# details.liquidHours
# )

View File

@ -50,10 +50,11 @@ import tractor
from tractor import to_asyncio from tractor import to_asyncio
from tractor import trionics from tractor import trionics
from pendulum import ( from pendulum import (
from_timestamp,
DateTime, DateTime,
Duration, Duration,
duration as mk_duration, duration as mk_duration,
from_timestamp,
Interval,
) )
from eventkit import Event from eventkit import Event
from ib_insync import ( from ib_insync import (
@ -91,10 +92,15 @@ from .symbols import (
_exch_skip_list, _exch_skip_list,
_futes_venues, _futes_venues,
) )
from ._util import ( from ...log import get_logger
log, from .venues import (
# only for the ib_sync internal logging is_venue_open,
get_logger, sesh_times,
is_venue_closure,
)
log = get_logger(
name=__name__,
) )
_bar_load_dtype: list[tuple[str, type]] = [ _bar_load_dtype: list[tuple[str, type]] = [
@ -180,7 +186,7 @@ class NonShittyIB(IB):
# override `ib_insync` internal loggers so we can see wtf # override `ib_insync` internal loggers so we can see wtf
# it's doing.. # it's doing..
self._logger = get_logger( self._logger = get_logger(
'ib_insync.ib', name=__name__,
) )
self._createEvents() self._createEvents()
@ -188,7 +194,7 @@ class NonShittyIB(IB):
self.wrapper = NonShittyWrapper(self) self.wrapper = NonShittyWrapper(self)
self.client = ib_client.Client(self.wrapper) self.client = ib_client.Client(self.wrapper)
self.client._logger = get_logger( self.client._logger = get_logger(
'ib_insync.client', name='ib_insync.client',
) )
# self.errorEvent += self._onError # self.errorEvent += self._onError
@ -260,6 +266,16 @@ def remove_handler_on_err(
event.disconnect(handler) event.disconnect(handler)
# (originally?) i thot that,
# > "EST in ISO 8601 format is required.."
#
# XXX, but see `ib_async`'s impl,
# - `ib_async.ib.IB.reqHistoricalDataAsync()`
# - `ib_async.util.formatIBDatetime()`
# below is EPOCH.
_iso8601_epoch_in_est: str = "1970-01-01T00:00:00.000000-05:00"
class Client: class Client:
''' '''
IB wrapped for our broker backend API. IB wrapped for our broker backend API.
@ -333,9 +349,11 @@ class Client:
self, self,
fqme: str, fqme: str,
# EST in ISO 8601 format is required... below is EPOCH # EST in ISO 8601 format is required..
start_dt: datetime|str = "1970-01-01T00:00:00.000000-05:00", # XXX, see `ib_async.ib.IB.reqHistoricalDataAsync()`
end_dt: datetime|str = "", # below is EPOCH.
start_dt: datetime|None = None, # _iso8601_epoch_in_est,
end_dt: datetime|None = None,
# ohlc sample period in seconds # ohlc sample period in seconds
sample_period_s: int = 1, sample_period_s: int = 1,
@ -346,9 +364,17 @@ class Client:
**kwargs, **kwargs,
) -> tuple[BarDataList, np.ndarray, Duration]: ) -> tuple[
BarDataList,
np.ndarray,
Duration,
]:
''' '''
Retreive OHLCV bars for a fqme over a range to the present. Retreive the `fqme`'s OHLCV-bars for the time-range "until `end_dt`".
Notes:
- IB's api doesn't support a `start_dt` (which is why default
is null) so we only use it for bar-frame duration checking.
''' '''
# See API docs here: # See API docs here:
@ -363,13 +389,19 @@ class Client:
dt_duration: Duration = ( dt_duration: Duration = (
duration duration
or default_dt_duration or
default_dt_duration
) )
# TODO: maybe remove all this? # TODO: maybe remove all this?
global _enters global _enters
if not end_dt: if end_dt is None:
end_dt = '' end_dt: str = ''
else:
est_end_dt = end_dt.in_tz('EST')
if est_end_dt != end_dt:
breakpoint()
_enters += 1 _enters += 1
@ -438,58 +470,116 @@ class Client:
+ query_info + query_info
) )
# TODO: we could maybe raise ``NoData`` instead if we # TODO: we could maybe raise `NoData` instead if we
# rewrite the method in the first case? # rewrite the method in the first case?
# right now there's no way to detect a timeout.. # right now there's no way to detect a timeout..
return [], np.empty(0), dt_duration return [], np.empty(0), dt_duration
log.info(query_info) log.info(query_info)
# ------ GAP-DETECTION ------
# NOTE XXX: ensure minimum duration in bars? # NOTE XXX: ensure minimum duration in bars?
# => recursively call this method until we get at least as # => recursively call this method until we get at least as
# many bars such that they sum in aggregate to the the # many bars such that they sum in aggregate to the the
# desired total time (duration) at most. # desired total time (duration) at most.
# - if you query over a gap and get no data # - if you query over a gap and get no data
# that may short circuit the history # that may short circuit the history
if ( if end_dt:
# XXX XXX XXX
# => WHY DID WE EVEN NEED THIS ORIGINALLY!? <=
# XXX XXX XXX
False
and end_dt
):
nparr: np.ndarray = bars_to_np(bars) nparr: np.ndarray = bars_to_np(bars)
times: np.ndarray = nparr['time'] times: np.ndarray = nparr['time']
first: float = times[0] first: float = times[0]
tdiff: float = times[-1] - first last: float = times[-1]
# frame_dur: float = times[-1] - first
details: ContractDetails = (
await self.ib.reqContractDetailsAsync(contract)
)[0]
# convert to makt-native tz
tz: str = details.timeZoneId
end_dt = end_dt.in_tz(tz)
first_dt: DateTime = from_timestamp(first).in_tz(tz)
last_dt: DateTime = from_timestamp(last).in_tz(tz)
tdiff: int = (
last_dt
-
first_dt
).in_seconds() + sample_period_s
_open_now: bool = is_venue_open(
con_deats=details,
)
# XXX, do gap detections.
has_closure_gap: bool = False
if (
last_dt.add(seconds=sample_period_s)
<
end_dt
):
open_time, close_time = sesh_times(details)
# XXX, always calc gap in mkt-venue-local timezone
gap: Interval = end_dt - last_dt
if not (
has_closure_gap := is_venue_closure(
gap=gap,
con_deats=details,
time_step_s=sample_period_s,
)):
log.warning(
f'Invalid non-closure gap for {fqme!r} ?!?\n'
f'is-open-now: {_open_now}\n'
f'\n'
f'{gap}\n'
)
log.warning(
f'Detected NON venue-closure GAP ??\n'
f'{gap}\n'
)
breakpoint()
else:
assert has_closure_gap
log.debug(
f'Detected venue closure gap (weekend),\n'
f'{gap}\n'
)
if ( if (
# len(bars) * sample_period_s) < dt_duration.in_seconds() start_dt is None
tdiff < dt_duration.in_seconds() and (
# and False tdiff
<
dt_duration.in_seconds()
)
and
not has_closure_gap
): ):
end_dt: DateTime = from_timestamp(first) log.error(
log.warning(
f'Frame result was shorter then {dt_duration}!?\n' f'Frame result was shorter then {dt_duration}!?\n'
'Recursing for more bars:\n'
f'end_dt: {end_dt}\n' f'end_dt: {end_dt}\n'
f'dt_duration: {dt_duration}\n' f'dt_duration: {dt_duration}\n'
# f'\n'
# f'Recursing for more bars:\n'
) )
( # XXX, debug!
r_bars, breakpoint()
r_arr, # XXX ? TODO? recursively try to re-request?
r_duration, # => i think *NO* right?
) = await self.bars( #
fqme, # (
start_dt=start_dt, # r_bars,
end_dt=end_dt, # r_arr,
sample_period_s=sample_period_s, # r_duration,
# ) = await self.bars(
# fqme,
# start_dt=start_dt,
# end_dt=end_dt,
# sample_period_s=sample_period_s,
# TODO: make a table for Duration to # # TODO: make a table for Duration to
# the ib str values in order to use this? # # the ib str values in order to use this?
# duration=duration, # # duration=duration,
) # )
r_bars.extend(bars) # r_bars.extend(bars)
bars = r_bars # bars = r_bars
nparr: np.ndarray = bars_to_np(bars) nparr: np.ndarray = bars_to_np(bars)
@ -784,9 +874,16 @@ class Client:
# crypto$ # crypto$
elif exch == 'PAXOS': # btc.paxos elif exch == 'PAXOS': # btc.paxos
con = Crypto( con = Crypto(
symbol=symbol, symbol=symbol.upper(),
currency=currency, currency='USD',
exchange='PAXOS',
) )
# XXX, on `ib_insync` when first tried this,
# > Error 10299, reqId 141: Expected what to show is
# > AGGTRADES, please use that instead of TRADES.,
# > contract: Crypto(conId=479624278, symbol='BTC',
# > exchange='PAXOS', currency='USD',
# > localSymbol='BTC.USD', tradingClass='BTC')
# stonks # stonks
else: else:

View File

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

View File

@ -56,11 +56,11 @@ from piker.brokers._util import (
NoData, NoData,
DataUnavailable, DataUnavailable,
) )
from piker.log import get_logger
from .api import ( from .api import (
# _adhoc_futes_set, # _adhoc_futes_set,
Client, Client,
con2fqme, con2fqme,
log,
load_aio_clients, load_aio_clients,
MethodProxy, MethodProxy,
open_client_proxies, open_client_proxies,
@ -69,15 +69,18 @@ from .api import (
Contract, Contract,
RequestError, RequestError,
) )
from .venues import is_venue_open
from ._util import ( from ._util import (
data_reset_hack, data_reset_hack,
is_current_time_in_range,
) )
from .symbols import get_mkt_info from .symbols import get_mkt_info
if TYPE_CHECKING: if TYPE_CHECKING:
from trio._core._run import Task from trio._core._run import Task
log = get_logger(
name=__name__,
)
# XXX NOTE: See available types table docs: # XXX NOTE: See available types table docs:
# https://interactivebrokers.github.io/tws-api/tick_types.html # https://interactivebrokers.github.io/tws-api/tick_types.html
@ -203,7 +206,8 @@ async def open_history_client(
latency = time.time() - query_start latency = time.time() - query_start
if ( if (
not timedout not timedout
# and latency <= max_timeout # and
# latency <= max_timeout
): ):
count += 1 count += 1
mean += latency / count mean += latency / count
@ -219,8 +223,10 @@ async def open_history_client(
) )
if ( if (
end_dt end_dt
and head_dt and
and end_dt <= head_dt head_dt
and
end_dt <= head_dt
): ):
raise DataUnavailable( raise DataUnavailable(
f'First timestamp is {head_dt}\n' f'First timestamp is {head_dt}\n'
@ -278,7 +284,7 @@ async def open_history_client(
start_dt start_dt
): ):
# TODO! rm this once we're more confident it never hits! # TODO! rm this once we're more confident it never hits!
breakpoint() # breakpoint()
raise RuntimeError( raise RuntimeError(
f'OHLC-bars array start is gt `start_dt` limit !!\n' f'OHLC-bars array start is gt `start_dt` limit !!\n'
f'start_dt: {start_dt}\n' f'start_dt: {start_dt}\n'
@ -298,7 +304,7 @@ async def open_history_client(
# TODO: it seems like we can do async queries for ohlc # TODO: it seems like we can do async queries for ohlc
# but getting the order right still isn't working and I'm not # but getting the order right still isn't working and I'm not
# quite sure why.. needs some tinkering and probably # quite sure why.. needs some tinkering and probably
# a lookthrough of the ``ib_insync`` machinery, for eg. maybe # a lookthrough of the `ib_insync` machinery, for eg. maybe
# we have to do the batch queries on the `asyncio` side? # we have to do the batch queries on the `asyncio` side?
yield ( yield (
get_hist, get_hist,
@ -421,14 +427,13 @@ _failed_resets: int = 0
async def get_bars( async def get_bars(
proxy: MethodProxy, proxy: MethodProxy,
fqme: str, fqme: str,
timeframe: int, timeframe: int,
# blank to start which tells ib to look up the latest datum # blank to start which tells ib to look up the latest datum
end_dt: str = '', end_dt: datetime|None = None,
start_dt: str|None = '', start_dt: datetime|None = None,
# TODO: make this more dynamic based on measured frame rx latency? # TODO: make this more dynamic based on measured frame rx latency?
# how long before we trigger a feed reset (seconds) # how long before we trigger a feed reset (seconds)
@ -482,7 +487,8 @@ async def get_bars(
dt_duration, dt_duration,
) = await proxy.bars( ) = await proxy.bars(
fqme=fqme, fqme=fqme,
# XXX TODO! lol we're not using this.. # XXX TODO! LOL we're not using this and IB dun
# support it anyway..
# start_dt=start_dt, # start_dt=start_dt,
end_dt=end_dt, end_dt=end_dt,
sample_period_s=timeframe, sample_period_s=timeframe,
@ -734,7 +740,7 @@ async def _setup_quote_stream(
# '294', # Trade rate / minute # '294', # Trade rate / minute
# '295', # Vlm rate / minute # '295', # Vlm rate / minute
), ),
contract: Contract | None = None, contract: Contract|None = None,
) -> trio.abc.ReceiveChannel: ) -> trio.abc.ReceiveChannel:
''' '''
@ -756,7 +762,12 @@ async def _setup_quote_stream(
# XXX since this is an `asyncio.Task`, we must use # XXX since this is an `asyncio.Task`, we must use
# tractor.pause_from_sync() # tractor.pause_from_sync()
caccount_name, client = get_preferred_data_client(accts2clients) (
_account_name,
client,
) = get_preferred_data_client(
accts2clients,
)
contract = ( contract = (
contract contract
or or
@ -1091,14 +1102,9 @@ async def stream_quotes(
) )
# is venue active rn? # is venue active rn?
venue_is_open: bool = any( venue_is_open: bool = is_venue_open(
is_current_time_in_range( con_deats=details,
start_dt=sesh.start,
end_dt=sesh.end,
)
for sesh in details.tradingSessions()
) )
init_msg = FeedInit(mkt_info=mkt) init_msg = FeedInit(mkt_info=mkt)
# NOTE, tell sampler (via config) to skip vlm summing for dst # NOTE, tell sampler (via config) to skip vlm summing for dst

View File

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

View File

@ -42,10 +42,7 @@ from piker.accounting import (
from piker._cacheables import ( from piker._cacheables import (
async_lifo_cache, async_lifo_cache,
) )
from piker.log import get_logger
from ._util import (
log,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from .api import ( from .api import (
@ -53,6 +50,10 @@ if TYPE_CHECKING:
Client, Client,
) )
log = get_logger(
name=__name__,
)
_futes_venues = ( _futes_venues = (
'GLOBEX', 'GLOBEX',
'NYMEX', 'NYMEX',
@ -134,7 +135,7 @@ _adhoc_fiat_set = set((
# manually discovered tick discrepancies, # manually discovered tick discrepancies,
# onl god knows how or why they'd cuck these up.. # onl god knows how or why they'd cuck these up..
_adhoc_mkt_infos: dict[int | str, dict] = { _adhoc_mkt_infos: dict[int|str, dict] = {
'vtgn.nasdaq': {'price_tick': Decimal('0.01')}, 'vtgn.nasdaq': {'price_tick': Decimal('0.01')},
} }
@ -488,8 +489,7 @@ def con2fqme(
@async_lifo_cache() @async_lifo_cache()
async def get_mkt_info( async def get_mkt_info(
fqme: str, fqme: str,
proxy: MethodProxy|None = None,
proxy: MethodProxy | None = None,
) -> tuple[MktPair, ibis.ContractDetails]: ) -> tuple[MktPair, ibis.ContractDetails]:
@ -550,7 +550,7 @@ async def get_mkt_info(
size_tick: Decimal = Decimal( size_tick: Decimal = Decimal(
str(details.minSize).rstrip('0') str(details.minSize).rstrip('0')
) )
# |-> TODO: there is also the Contract.sizeIncrement, bt wtf is it? # ?TODO, there is also the Contract.sizeIncrement, bt wtf is it?
# NOTE: this is duplicate from the .broker.norm_trade_records() # NOTE: this is duplicate from the .broker.norm_trade_records()
# routine, we should factor all this parsing somewhere.. # routine, we should factor all this parsing somewhere..

View File

@ -0,0 +1,312 @@
# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
(Multi-)venue mgmt helpers.
IB generally supports all "legacy" trading venues, those mostly owned
by ICE and friends.
'''
from __future__ import annotations
from datetime import ( # noqa
datetime,
date,
tzinfo as TzInfo,
)
from typing import (
Iterator,
TYPE_CHECKING,
)
import exchange_calendars as xcals
from pendulum import (
now,
Duration,
Interval,
Time,
)
if TYPE_CHECKING:
from ib_insync import (
TradingSession,
ContractDetails,
)
from exchange_calendars.exchange_calendars import (
ExchangeCalendar,
)
from pandas import (
# DatetimeIndex,
TimeDelta,
Timestamp,
)
def has_weekend(
period: Interval,
) -> bool:
'''
Predicate to for a period being within
days 6->0 (sat->sun).
'''
has_weekend: bool = False
for dt in period:
if dt.day_of_week in [0, 6]: # 0=Sunday, 6=Saturday
has_weekend = True
break
return has_weekend
def has_holiday(
con_deats: ContractDetails,
period: Interval,
) -> bool:
'''
Using the `exchange_calendars` lib detect if a time-gap `period`
is contained in a known "cash hours" closure.
'''
tz: str = con_deats.timeZoneId
exch: str = con_deats.contract.primaryExchange
cal: ExchangeCalendar = xcals.get_calendar(exch)
end: datetime = period.end
# _start: datetime = period.start
# ?TODO, can rm ya?
# => not that useful?
# dti: DatetimeIndex = cal.sessions_in_range(
# _start.date(),
# end.date(),
# )
prev_close: Timestamp = cal.previous_close(
end.date()
).tz_convert(tz)
prev_open: Timestamp = cal.previous_open(
end.date()
).tz_convert(tz)
# now do relative from prev_ values ^
# to get the next open which should match
# "contain" the end of the gap.
next_open: Timestamp = cal.next_open(
prev_open,
).tz_convert(tz)
next_open: Timestamp = cal.next_open(
prev_open,
).tz_convert(tz)
_next_close: Timestamp = cal.next_close(
prev_close
).tz_convert(tz)
cash_gap: TimeDelta = next_open - prev_close
is_holiday_gap = (
cash_gap
>
period
)
# XXX, debug
# breakpoint()
return is_holiday_gap
def is_current_time_in_range(
sesh: Interval,
when: datetime|None = None,
) -> bool:
'''
Check if current time is within the datetime range.
Use any/the-same timezone as provided by `start_dt.tzinfo` value
in the range.
'''
when: datetime = when or now()
return when in sesh
def iter_sessions(
con_deats: ContractDetails,
) -> Iterator[Interval]:
'''
Yield `pendulum.Interval`s for all
`ibas.ContractDetails.tradingSessions() -> TradingSession`s.
'''
sesh: TradingSession
for sesh in con_deats.tradingSessions():
yield Interval(*sesh)
def sesh_times(
con_deats: ContractDetails,
) -> tuple[Time, Time]:
'''
Based on the earliest trading session provided by the IB API,
get the (day-agnostic) times for the start/end.
'''
earliest_sesh: Interval = next(iter_sessions(con_deats))
return (
earliest_sesh.start.time(),
earliest_sesh.end.time(),
)
# ^?TODO, use `.diff()` to get point-in-time-agnostic period?
# https://pendulum.eustace.io/docs/#difference
def is_venue_open(
con_deats: ContractDetails,
when: datetime|Duration|None = None,
) -> bool:
'''
Check if market-venue is open during `when`, which defaults to
"now".
'''
sesh: Interval
for sesh in iter_sessions(con_deats):
if is_current_time_in_range(
sesh=sesh,
when=when,
):
return True
return False
def is_venue_closure(
gap: Interval,
con_deats: ContractDetails,
time_step_s: int,
) -> bool:
'''
Check if a provided time-`gap` is just an (expected) trading
venue closure period.
'''
open: Time
close: Time
open, close = sesh_times(con_deats)
# ensure times are in mkt-native timezone
tz: str = con_deats.timeZoneId
start = gap.start.in_tz(tz)
start_t = start.time()
end = gap.end.in_tz(tz)
end_t = end.time()
if (
(
start_t in (
close,
close.subtract(seconds=time_step_s)
)
and
end_t in (
open,
open.add(seconds=time_step_s),
)
)
or
has_weekend(gap)
or
has_holiday(
con_deats=con_deats,
period=gap,
)
):
return True
# breakpoint()
return False
# TODO, put this into `._util` and call it from here!
#
# NOTE, this was generated by @guille from a gpt5 prompt
# and was originally thot to be needed before learning about
# `ib_insync.contract.ContractDetails._parseSessions()` and
# it's downstream meths..
#
# This is still likely useful to keep for now to parse the
# `.tradingHours: str` value manually if we ever decide
# to move off `ib_async` and implement our own `trio`/`anyio`
# based version Bp
#
# >attempt to parse the retarted ib "time stampy thing" they
# >do for "venue hours" with this.. written by
# >gpt5-"thinking",
#
def parse_trading_hours(
spec: str,
tz: TzInfo|None = None
) -> dict[
date,
tuple[datetime, datetime]
]|None:
'''
Parse venue hours like:
'YYYYMMDD:HHMM-YYYYMMDD:HHMM;YYYYMMDD:CLOSED;...'
Returns `dict[date] = (open_dt, close_dt)` or `None` if
closed.
'''
if (
not isinstance(spec, str)
or
not spec
):
raise ValueError('spec must be a non-empty string')
out: dict[
date,
tuple[datetime, datetime]
]|None = {}
for part in (p.strip() for p in spec.split(';') if p.strip()):
if part.endswith(':CLOSED'):
day_s, _ = part.split(':', 1)
d = datetime.strptime(day_s, '%Y%m%d').date()
out[d] = None
continue
try:
start_s, end_s = part.split('-', 1)
start_dt = datetime.strptime(start_s, '%Y%m%d:%H%M')
end_dt = datetime.strptime(end_s, '%Y%m%d:%H%M')
except ValueError as exc:
raise ValueError(f'invalid segment: {part}') from exc
if tz is not None:
start_dt = start_dt.replace(tzinfo=tz)
end_dt = end_dt.replace(tzinfo=tz)
out[start_dt.date()] = (start_dt, end_dt)
return out
# ORIG desired usage,
#
# TODO, for non-drunk tomorrow,
# - call above fn and check that `output[today] is not None`
# trading_hrs: dict = parse_trading_hours(
# details.tradingHours
# )
# liq_hrs: dict = parse_trading_hours(
# details.liquidHours
# )

View File

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

View File

@ -50,13 +50,19 @@ from . import open_cached_client
from piker._cacheables import async_lifo_cache from piker._cacheables import async_lifo_cache
from .. import config from .. import config
from ._util import resproc, BrokerError, SymbolNotFound from ._util import resproc, BrokerError, SymbolNotFound
from ..log import ( from piker.log import (
colorize_json, colorize_json,
)
from ._util import (
log,
get_console_log, get_console_log,
) )
from piker.log import (
get_logger,
)
log = get_logger(
name=__name__,
)
_use_practice_account = False _use_practice_account = False
_refresh_token_ep = 'https://{}login.questrade.com/oauth2/' _refresh_token_ep = 'https://{}login.questrade.com/oauth2/'
@ -1205,7 +1211,10 @@ async def stream_quotes(
# feed_type: str = 'stock', # feed_type: str = 'stock',
) -> AsyncGenerator[str, Dict[str, Any]]: ) -> AsyncGenerator[str, Dict[str, Any]]:
# XXX: required to propagate ``tractor`` loglevel to piker logging # XXX: required to propagate ``tractor`` loglevel to piker logging
get_console_log(loglevel) get_console_log(
level=loglevel,
name=__name__,
)
async with open_cached_client('questrade') as client: async with open_cached_client('questrade') as client:
if feed_type == 'stock': if feed_type == 'stock':

View File

@ -30,9 +30,16 @@ import asks
from ._util import ( from ._util import (
resproc, resproc,
BrokerError, BrokerError,
log,
) )
from ..calc import percent_change from piker.calc import percent_change
from piker.log import (
get_logger,
)
log = get_logger(
name=__name__,
)
_service_ep = 'https://api.robinhood.com' _service_ep = 'https://api.robinhood.com'

View File

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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,8 @@ def load_trans_eps(
if ( if (
network network
and not maddrs and
not maddrs
): ):
# load network section and (attempt to) connect all endpoints # load network section and (attempt to) connect all endpoints
# which are reachable B) # which are reachable B)
@ -112,31 +113,27 @@ def load_trans_eps(
default=None, default=None,
help='Multiaddrs to bind or contact', help='Multiaddrs to bind or contact',
) )
# @click.option(
# '--tsdb',
# is_flag=True,
# help='Enable local ``marketstore`` instance'
# )
# @click.option(
# '--es',
# is_flag=True,
# help='Enable local ``elasticsearch`` instance'
# )
def pikerd( def pikerd(
maddr: list[str] | None, maddr: list[str] | None,
loglevel: str, loglevel: str,
tl: bool, tl: bool,
pdb: bool, pdb: bool,
# tsdb: bool,
# es: bool,
): ):
''' '''
Spawn the piker broker-daemon. Start the "root service actor", `pikerd`, run it until
cancellation.
This "root daemon" operates as the top most service-mngr and
subsys-as-subactor supervisor, think of it as the "init proc" of
any of any `piker` application or daemon-process tree.
''' '''
# from tractor.devx import maybe_open_crash_handler # from tractor.devx import maybe_open_crash_handler
# with maybe_open_crash_handler(pdb=False): # with maybe_open_crash_handler(pdb=False):
log = get_console_log(loglevel, name='cli') log = get_console_log(
level=loglevel,
with_tractor_log=tl,
)
if pdb: if pdb:
log.warning(( log.warning((
@ -237,6 +234,14 @@ def cli(
regaddr: str, regaddr: str,
) -> None: ) -> None:
'''
The "root" `piker`-cmd CLI endpoint.
NOTE, this def generally relies on and requires a sub-cmd to be
provided by the user, OW only a `--help` msg (listing said
subcmds) will be dumped to console.
'''
if configdir is not None: if configdir is not None:
assert os.path.isdir(configdir), f"`{configdir}` is not a valid path" assert os.path.isdir(configdir), f"`{configdir}` is not a valid path"
config._override_config_dir(configdir) config._override_config_dir(configdir)
@ -295,17 +300,50 @@ def cli(
@click.option('--tl', is_flag=True, help='Enable tractor logging') @click.option('--tl', is_flag=True, help='Enable tractor logging')
@click.argument('ports', nargs=-1, required=False) @click.argument('ports', nargs=-1, required=False)
@click.pass_obj @click.pass_obj
def services(config, tl, ports): def services(
config,
tl: bool,
ports: list[int],
):
'''
List all `piker` "service deamons" to the console in
a `json`-table which maps each actor's UID in the form,
from ..service import ( `{service_name}.{subservice_name}.{UUID}`
to its (primary) IPC server address.
(^TODO, should be its multiaddr form once we support it)
Note that by convention actors which operate as "headless"
processes (those without GUIs/graphics, and which generally
parent some noteworthy subsystem) are normally suffixed by
a "d" such as,
- pikerd: the root runtime supervisor
- brokerd: a broker-backend order ctl daemon
- emsd: the internal dark-clearing and order routing daemon
- datad: a data-provider-backend data feed daemon
- samplerd: the real-time data sampling and clock-syncing daemon
"Headed units" are normally just given an obvious app-like name
with subactors indexed by `.` such as,
- chart: the primary modal charting iface, a Qt app
- chart.fsp_0: a financial-sig-proc cascade instance which
delivers graphics to a parent `chart` app.
- polars_boi: some (presumably) `polars` using console app.
'''
from piker.service import (
open_piker_runtime, open_piker_runtime,
_default_registry_port, _default_registry_port,
_default_registry_host, _default_registry_host,
) )
host = _default_registry_host # !TODO, mk this to work with UDS!
host: str = _default_registry_host
if not ports: if not ports:
ports = [_default_registry_port] ports: list[int] = [_default_registry_port]
addr = tractor._addr.wrap_address( addr = tractor._addr.wrap_address(
addr=(host, ports[0]) addr=(host, ports[0])
@ -316,7 +354,11 @@ def services(config, tl, ports):
async with ( async with (
open_piker_runtime( open_piker_runtime(
name='service_query', name='service_query',
loglevel=config['loglevel'] if tl else None, loglevel=(
config['loglevel']
if tl
else None
),
), ),
tractor.get_registry( tractor.get_registry(
addr=addr, addr=addr,
@ -336,7 +378,15 @@ def services(config, tl, ports):
def _load_clis() -> None: def _load_clis() -> None:
# from ..service import elastic # noqa '''
Dynamically load and register all subsys CLI endpoints (at call
time).
NOTE, obviously this is normally expected to be called at
`import` time and implicitly relies on our use of various
`click`/`typer` decorator APIs.
'''
from ..brokers import cli # noqa from ..brokers import cli # noqa
from ..ui import cli # noqa from ..ui import cli # noqa
from ..watchlists import cli # noqa from ..watchlists import cli # noqa
@ -346,5 +396,5 @@ def _load_clis() -> None:
from ..accounting import cli # noqa from ..accounting import cli # noqa
# load downstream cli modules # load all subsytem cli eps
_load_clis() _load_clis()

View File

@ -336,10 +336,18 @@ async def register_with_sampler(
open_index_stream: bool = True, # open a 2way stream for sample step msgs? open_index_stream: bool = True, # open a 2way stream for sample step msgs?
sub_for_broadcasts: bool = True, # sampler side to send step updates? sub_for_broadcasts: bool = True, # sampler side to send step updates?
loglevel: str|None = None,
) -> set[int]: ) -> set[int]:
get_console_log(tractor.current_actor().loglevel) get_console_log(
level=(
loglevel
or
tractor.current_actor().loglevel
),
name=__name__,
)
incr_was_started: bool = False incr_was_started: bool = False
try: try:
@ -476,6 +484,7 @@ async def spawn_samplerd(
register_with_sampler, register_with_sampler,
period_s=1, period_s=1,
sub_for_broadcasts=False, sub_for_broadcasts=False,
loglevel=loglevel,
) )
return True return True
@ -484,7 +493,6 @@ async def spawn_samplerd(
@acm @acm
async def maybe_open_samplerd( async def maybe_open_samplerd(
loglevel: str|None = None, loglevel: str|None = None,
**pikerd_kwargs, **pikerd_kwargs,
@ -513,10 +521,10 @@ async def open_sample_stream(
shms_by_period: dict[float, dict]|None = None, shms_by_period: dict[float, dict]|None = None,
open_index_stream: bool = True, open_index_stream: bool = True,
sub_for_broadcasts: bool = True, sub_for_broadcasts: bool = True,
loglevel: str|None = None,
cache_key: str|None = None, # cache_key: str|None = None,
allow_new_sampler: bool = True, # allow_new_sampler: bool = True,
ensure_is_active: bool = False, ensure_is_active: bool = False,
) -> AsyncIterator[dict[str, float]]: ) -> AsyncIterator[dict[str, float]]:
@ -551,7 +559,9 @@ async def open_sample_stream(
# XXX: this should be singleton on a host, # XXX: this should be singleton on a host,
# a lone broker-daemon per provider should be # a lone broker-daemon per provider should be
# created for all practical purposes # created for all practical purposes
maybe_open_samplerd() as portal, maybe_open_samplerd(
loglevel=loglevel,
) as portal,
portal.open_context( portal.open_context(
register_with_sampler, register_with_sampler,
@ -560,6 +570,7 @@ async def open_sample_stream(
'shms_by_period': shms_by_period, 'shms_by_period': shms_by_period,
'open_index_stream': open_index_stream, 'open_index_stream': open_index_stream,
'sub_for_broadcasts': sub_for_broadcasts, 'sub_for_broadcasts': sub_for_broadcasts,
'loglevel': loglevel,
}, },
) as (ctx, shm_periods) ) as (ctx, shm_periods)
): ):

View File

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

View File

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

View File

@ -24,6 +24,7 @@ from functools import partial
from typing import ( from typing import (
AsyncIterator, AsyncIterator,
Callable, Callable,
TYPE_CHECKING,
) )
import numpy as np import numpy as np
@ -33,12 +34,12 @@ import tractor
from tractor.msg import NamespacePath from tractor.msg import NamespacePath
from piker.types import Struct from piker.types import Struct
from ..log import get_logger, get_console_log from ..log import (
from .. import data get_logger,
from ..data.feed import ( get_console_log,
Flume,
Feed,
) )
from .. import data
from ..data.flows import Flume
from ..data._sharedmem import ShmArray from ..data._sharedmem import ShmArray
from ..data._sampling import ( from ..data._sampling import (
_default_delay_s, _default_delay_s,
@ -52,6 +53,9 @@ from ._api import (
) )
from ..toolz import Profiler from ..toolz import Profiler
if TYPE_CHECKING:
from ..data.feed import Feed
log = get_logger(__name__) log = get_logger(__name__)
@ -169,8 +173,10 @@ class Cascade(Struct):
if not synced: if not synced:
fsp: Fsp = self.fsp fsp: Fsp = self.fsp
log.warning( log.warning(
'***DESYNCED FSP***\n' f'***DESYNCED fsp***\n'
f'{fsp.ns_path}@{src_shm.token}\n' f'------------------\n'
f'ns-path: {fsp.ns_path!r}\n'
f'shm-token: {src_shm.token}\n'
f'step_diff: {step_diff}\n' f'step_diff: {step_diff}\n'
f'len_diff: {len_diff}\n' f'len_diff: {len_diff}\n'
) )
@ -398,7 +404,6 @@ async def connect_streams(
@tractor.context @tractor.context
async def cascade( async def cascade(
ctx: tractor.Context, ctx: tractor.Context,
# data feed key # data feed key
@ -412,7 +417,7 @@ async def cascade(
shm_registry: dict[str, _Token], shm_registry: dict[str, _Token],
zero_on_step: bool = False, zero_on_step: bool = False,
loglevel: str | None = None, loglevel: str|None = None,
) -> None: ) -> None:
''' '''
@ -426,7 +431,17 @@ async def cascade(
) )
if loglevel: if loglevel:
get_console_log(loglevel) log = get_console_log(
loglevel,
name=__name__,
)
# XXX TODO!
# figure out why this writes a dict to,
# `tractor._state._runtime_vars['_root_mailbox']`
# XD .. wtf
# TODO, solve this as reported in,
# https://www.pikers.dev/pikers/piker/issues/70
# await tractor.pause()
src: Flume = Flume.from_msg(src_flume_addr) src: Flume = Flume.from_msg(src_flume_addr)
dst: Flume = Flume.from_msg( dst: Flume = Flume.from_msg(
@ -469,7 +484,8 @@ async def cascade(
# open a data feed stream with requested broker # open a data feed stream with requested broker
feed: Feed feed: Feed
async with data.feed.maybe_open_feed( async with data.feed.maybe_open_feed(
[fqme], fqmes=[fqme],
loglevel=loglevel,
# TODO throttle tick outputs from *this* daemon since # TODO throttle tick outputs from *this* daemon since
# it'll emit tons of ticks due to the throttle only # it'll emit tons of ticks due to the throttle only
@ -567,7 +583,8 @@ async def cascade(
# on every step msg received from the global `samplerd` # on every step msg received from the global `samplerd`
# service. # service.
async with open_sample_stream( async with open_sample_stream(
float(delay_s) period_s=float(delay_s),
loglevel=loglevel,
) as istream: ) as istream:
profiler(f'{func_name}: sample stream up') profiler(f'{func_name}: sample stream up')

View File

@ -37,35 +37,84 @@ _proj_name: str = 'piker'
def get_logger( def get_logger(
name: str = None, name: str|None = None,
**tractor_log_kwargs,
) -> logging.Logger: ) -> logging.Logger:
''' '''
Return the package log or a sub-log for `name` if provided. Return the package log or a sub-logger if a `name=` is provided,
which defaults to the calling module's pkg-namespace path.
See `tractor.log.get_logger()` for details.
''' '''
pkg_name: str = _proj_name
if (
name
and
pkg_name in name
):
name: str = name.lstrip(f'{_proj_name}.')
return tractor.log.get_logger( return tractor.log.get_logger(
name=name, name=name,
_root_name=_proj_name, pkg_name=pkg_name,
**tractor_log_kwargs,
) )
def get_console_log( def get_console_log(
level: str | None = None, level: str|None = None,
name: str | None = None, name: str|None = None,
pkg_name: str|None = None,
with_tractor_log: bool = False,
# ?TODO, support a "log-spec" style `str|dict[str, str]` which
# dictates both the sublogger-key and a level?
# -> see similar idea in `modden`'s usage.
**tractor_log_kwargs,
) -> logging.Logger: ) -> logging.Logger:
''' '''
Get the package logger and enable a handler which writes to stderr. Get the package logger and enable a handler which writes to
stderr.
Yeah yeah, i know we can use ``DictConfig``. You do it... Yeah yeah, i know we can use `DictConfig`.
You do it.. Bp
''' '''
pkg_name: str = _proj_name
if (
name
and
pkg_name in name
):
name: str = name.lstrip(f'{_proj_name}.')
tll: str|None = None
if (
with_tractor_log is not False
):
tll = level
elif maybe_actor := tractor.current_actor(
err_on_no_runtime=False,
):
tll = maybe_actor.loglevel
if tll:
t_log = tractor.log.get_console_log(
level=tll,
name='tractor', # <- XXX, force root tractor log!
**tractor_log_kwargs,
)
# TODO/ allow only enabling certain tractor sub-logs?
assert t_log.name == 'tractor'
return tractor.log.get_console_log( return tractor.log.get_console_log(
level, level=level,
name=name, name=name,
_root_name=_proj_name, pkg_name=pkg_name,
) # our root logger **tractor_log_kwargs,
)
def colorize_json( def colorize_json(

View File

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

View File

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

View File

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

View File

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

View File

@ -27,15 +27,29 @@ from typing import (
) )
import tractor import tractor
from tractor import Portal from tractor import (
msg,
from ._util import ( Actor,
log, # sub-sys logger Portal,
) )
from piker.log import get_logger
log = get_logger(name=__name__)
# TODO? default path-space for UDS registry?
# [ ] needs to be Xplatform tho!
# _default_registry_path: Path = (
# Path(os.environ['XDG_RUNTIME_DIR'])
# /'piker'
# )
_default_registry_host: str = '127.0.0.1' _default_registry_host: str = '127.0.0.1'
_default_registry_port: int = 6116 _default_registry_port: int = 6116
_default_reg_addr: tuple[str, int] = ( _default_reg_addr: tuple[
str,
int, # |str TODO, once we support UDS, see above.
] = (
_default_registry_host, _default_registry_host,
_default_registry_port, _default_registry_port,
) )
@ -75,16 +89,22 @@ async def open_registry(
''' '''
global _tractor_kwargs global _tractor_kwargs
actor = tractor.current_actor() actor: Actor = tractor.current_actor()
uid = actor.uid aid: msg.Aid = actor.aid
preset_reg_addrs: list[tuple[str, int]] = Registry.addrs uid: tuple[str, str] = aid.uid
preset_reg_addrs: list[
tuple[str, int]
] = Registry.addrs
if ( if (
preset_reg_addrs preset_reg_addrs
and addrs and
addrs
): ):
if preset_reg_addrs != addrs: if preset_reg_addrs != addrs:
# if any(addr in preset_reg_addrs for addr in addrs): # if any(addr in preset_reg_addrs for addr in addrs):
diff: set[tuple[str, int]] = set(preset_reg_addrs) - set(addrs) diff: set[
tuple[str, int]
] = set(preset_reg_addrs) - set(addrs)
if diff: if diff:
log.warning( log.warning(
f'`{uid}` requested only subset of registrars: {addrs}\n' f'`{uid}` requested only subset of registrars: {addrs}\n'
@ -98,7 +118,6 @@ async def open_registry(
) )
was_set: bool = False was_set: bool = False
if ( if (
not tractor.is_root_process() not tractor.is_root_process()
and and
@ -115,16 +134,23 @@ async def open_registry(
f"`{uid}` registry should already exist but doesn't?" f"`{uid}` registry should already exist but doesn't?"
) )
if ( if not Registry.addrs:
not Registry.addrs
):
was_set = True was_set = True
Registry.addrs = addrs or [_default_reg_addr] Registry.addrs = (
addrs
or
[_default_reg_addr]
)
# NOTE: only spot this seems currently used is inside # NOTE: only spot this seems currently used is inside
# `.ui._exec` which is the (eventual qtloops) bootstrapping # `.ui._exec` which is the (eventual qtloops) bootstrapping
# with guest mode. # with guest mode.
_tractor_kwargs['registry_addrs'] = Registry.addrs reg_addrs: list[tuple[str, str|int]] = Registry.addrs
# !TODO, a struct-API to stringently allow this only in special
# cases?
# -> better would be to have some way to (atomically) rewrite
# and entire `RuntimeVars`?? ideas welcome obvi..
_tractor_kwargs['registry_addrs'] = reg_addrs
try: try:
yield Registry.addrs yield Registry.addrs
@ -149,7 +175,7 @@ async def find_service(
| None | None
): ):
# try: # try:
reg_addrs: list[tuple[str, int]] reg_addrs: list[tuple[str, int|str]]
async with open_registry( async with open_registry(
addrs=( addrs=(
registry_addrs registry_addrs
@ -172,15 +198,13 @@ async def find_service(
only_first=first_only, # if set only returns single ref only_first=first_only, # if set only returns single ref
) as maybe_portals: ) as maybe_portals:
if not maybe_portals: if not maybe_portals:
# log.info( log.info(
print(
f'Could NOT find service {service_name!r} -> {maybe_portals!r}' f'Could NOT find service {service_name!r} -> {maybe_portals!r}'
) )
yield None yield None
return return
# log.info( log.info(
print(
f'Found service {service_name!r} -> {maybe_portals}' f'Found service {service_name!r} -> {maybe_portals}'
) )
yield maybe_portals yield maybe_portals
@ -195,8 +219,7 @@ async def find_service(
async def check_for_service( async def check_for_service(
service_name: str, service_name: str,
) -> None|tuple[str, int]:
) -> None | tuple[str, int]:
''' '''
Service daemon "liveness" predicate. Service daemon "liveness" predicate.

View File

@ -14,20 +14,12 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
Sub-sys module commons. Sub-sys module commons (if any ?? Bp).
""" """
from functools import partial
from ..log import (
get_logger,
get_console_log,
)
subsys: str = 'piker.service' subsys: str = 'piker.service'
log = get_logger(subsys) # ?TODO, if we were going to keep a `get_console_log()` in here to be
# invoked at `import`-time, how do we dynamically hand in the
get_console_log = partial( # `level=` value? seems too early in the runtime to be injected
get_console_log, # right?
name=subsys,
)

View File

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

View File

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

View File

@ -294,6 +294,11 @@ def ldshm(
f'Something is wrong with time period for {shm}:\n{times}' f'Something is wrong with time period for {shm}:\n{times}'
) )
period_s: float = float(max(d1, d2, med)) period_s: float = float(max(d1, d2, med))
log.info(
f'Processing shm buffer:\n'
f' file: {shmfile.name}\n'
f' period: {period_s}s\n'
)
null_segs: tuple = tsp.get_null_segs( null_segs: tuple = tsp.get_null_segs(
frame=shm.array, frame=shm.array,

View File

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

View File

@ -30,6 +30,11 @@ import tractor
from piker.data._formatters import BGM from piker.data._formatters import BGM
from piker.storage import log from piker.storage import log
from piker.toolz.profile import (
Profiler,
pg_profile_enabled,
ms_slower_then,
)
from piker.ui._style import get_fonts from piker.ui._style import get_fonts
if TYPE_CHECKING: if TYPE_CHECKING:
@ -92,12 +97,22 @@ async def markup_gaps(
# gap's duration. # gap's duration.
show_txt: bool = False, show_txt: bool = False,
# A/B comparison: render individual arrows alongside batch
# for visual comparison
show_individual_arrows: bool = False,
) -> dict[int, dict]: ) -> dict[int, dict]:
''' '''
Remote annotate time-gaps in a dt-fielded ts (normally OHLC) Remote annotate time-gaps in a dt-fielded ts (normally OHLC)
with rectangles. with rectangles.
''' '''
profiler = Profiler(
msg=f'markup_gaps() for {gaps.height} gaps',
disabled=False,
ms_threshold=0.0,
)
# XXX: force chart redraw FIRST to ensure PlotItem coordinate # XXX: force chart redraw FIRST to ensure PlotItem coordinate
# system is properly initialized before we position annotations! # system is properly initialized before we position annotations!
# Without this, annotations may be misaligned on first creation # Without this, annotations may be misaligned on first creation
@ -106,6 +121,19 @@ async def markup_gaps(
fqme=fqme, fqme=fqme,
timeframe=timeframe, timeframe=timeframe,
) )
profiler('first `.redraw()` before annot creation')
log.info(
f'markup_gaps() called:\n'
f' fqme: {fqme}\n'
f' timeframe: {timeframe}s\n'
f' gaps.height: {gaps.height}\n'
)
# collect all annotation specs for batch submission
rect_specs: list[dict] = []
arrow_specs: list[dict] = []
text_specs: list[dict] = []
aids: dict[int] = {} aids: dict[int] = {}
for i in range(gaps.height): for i in range(gaps.height):
@ -217,56 +245,38 @@ async def markup_gaps(
# 1: 'wine', # down-gap # 1: 'wine', # down-gap
# }[sgn] # }[sgn]
rect_kwargs: dict[str, Any] = dict( # collect rect spec (no fqme/timeframe, added by batch
fqme=fqme, # API)
timeframe=timeframe, rect_spec: dict[str, Any] = dict(
meth='set_view_pos',
start_pos=lc, start_pos=lc,
end_pos=ro, end_pos=ro,
color=color, color=color,
update_label=False,
start_time=start_time, start_time=start_time,
end_time=end_time, end_time=end_time,
) )
rect_specs.append(rect_spec)
# add up/down rects
aid: int|None = await actl.add_rect(**rect_kwargs)
if aid is None:
log.error(
f'Failed to add rect for,\n'
f'{rect_kwargs!r}\n'
f'\n'
f'Skipping to next gap!\n'
)
continue
assert aid
aids[aid] = rect_kwargs
direction: str = ( direction: str = (
'down' if down_gap 'down' if down_gap
else 'up' else 'up'
) )
# TODO! mk this a `msgspec.Struct` which we deserialize
# on the server side! # collect arrow spec
# XXX: send timestamp for server-side index lookup
# to ensure alignment with current shm state
gap_time: float = row['time'][0] gap_time: float = row['time'][0]
arrow_kwargs: dict[str, Any] = dict( arrow_spec: dict[str, Any] = dict(
fqme=fqme,
timeframe=timeframe,
x=iend, # fallback if timestamp lookup fails x=iend, # fallback if timestamp lookup fails
y=cls, y=cls,
time=gap_time, # for server-side index lookup time=gap_time, # for server-side index lookup
color=color, color=color,
alpha=169, alpha=169,
pointing=direction, pointing=direction,
# TODO: expose these as params to markup_gaps()?
headLen=10, headLen=10,
headWidth=2.222, headWidth=2.222,
pxMode=True, pxMode=True,
) )
arrow_specs.append(arrow_spec)
aid: int = await actl.add_arrow(
**arrow_kwargs
)
# add duration label to RHS of arrow # add duration label to RHS of arrow
if up_gap: if up_gap:
@ -278,15 +288,12 @@ async def markup_gaps(
assert flat assert flat
anchor = (0, 0) # up from bottom anchor = (0, 0) # up from bottom
# use a slightly smaller font for gap label txt. # collect text spec if enabled
font, small_font = get_fonts()
font_size: int = small_font.px_size - 1
assert isinstance(font_size, int)
if show_txt: if show_txt:
text_aid: int = await actl.add_text( font, small_font = get_fonts()
fqme=fqme, font_size: int = small_font.px_size - 1
timeframe=timeframe,
text_spec: dict[str, Any] = dict(
text=gap_label, text=gap_label,
x=iend + 1, # fallback if timestamp lookup fails x=iend + 1, # fallback if timestamp lookup fails
y=cls, y=cls,
@ -295,12 +302,46 @@ async def markup_gaps(
anchor=anchor, anchor=anchor,
font_size=font_size, font_size=font_size,
) )
aids[text_aid] = {'text': gap_label} text_specs.append(text_spec)
# tell chart to redraw all its # submit all annotations in single batch IPC msg
# graphics view layers Bo log.info(
f'Submitting batch annotations:\n'
f' rects: {len(rect_specs)}\n'
f' arrows: {len(arrow_specs)}\n'
f' texts: {len(text_specs)}\n'
)
profiler('built all annotation specs')
result: dict[str, list[int]] = await actl.add_batch(
fqme=fqme,
timeframe=timeframe,
rects=rect_specs,
arrows=arrow_specs,
texts=text_specs,
show_individual_arrows=show_individual_arrows,
)
profiler('batch `.add_batch()` IPC call complete')
# build aids dict from batch results
for aid in result['rects']:
aids[aid] = {'type': 'rect'}
for aid in result['arrows']:
aids[aid] = {'type': 'arrow'}
for aid in result['texts']:
aids[aid] = {'type': 'text'}
log.info(
f'Batch submission complete: {len(aids)} annotation(s) '
f'created'
)
profiler('built aids result dict')
# tell chart to redraw all its graphics view layers
await actl.redraw( await actl.redraw(
fqme=fqme, fqme=fqme,
timeframe=timeframe, timeframe=timeframe,
) )
profiler('final `.redraw()` after annot creation')
return aids return aids

View File

@ -63,8 +63,10 @@ from ..data._sharedmem import (
maybe_open_shm_array, maybe_open_shm_array,
ShmArray, ShmArray,
) )
from ..data._source import def_iohlcv_fields from piker.data._source import (
from ..data._sampling import ( def_iohlcv_fields,
)
from piker.data._sampling import (
open_sample_stream, open_sample_stream,
) )
@ -96,7 +98,9 @@ if TYPE_CHECKING:
# from .feed import _FeedsBus # from .feed import _FeedsBus
log = get_logger(__name__) log = get_logger(
name=__name__,
)
# `ShmArray` buffer sizing configuration: # `ShmArray` buffer sizing configuration:
@ -550,7 +554,7 @@ async def start_backfill(
) )
# ?TODO, check against venue closure hours # ?TODO, check against venue closure hours
# if/when provided by backend? # if/when provided by backend?
await tractor.pause() # await tractor.pause()
expected_dur: Interval = ( expected_dur: Interval = (
last_start_dt.subtract( last_start_dt.subtract(
@ -1320,6 +1324,7 @@ async def manage_history(
mkt: MktPair, mkt: MktPair,
some_data_ready: trio.Event, some_data_ready: trio.Event,
feed_is_live: trio.Event, feed_is_live: trio.Event,
loglevel: str = 'warning',
timeframe: float = 60, # in seconds timeframe: float = 60, # in seconds
wait_for_live_timeout: float = 0.5, wait_for_live_timeout: float = 0.5,
@ -1497,6 +1502,7 @@ async def manage_history(
# data feed layer that needs to consume it). # data feed layer that needs to consume it).
open_index_stream=True, open_index_stream=True,
sub_for_broadcasts=False, sub_for_broadcasts=False,
loglevel=loglevel,
) as sample_stream: ) as sample_stream:
# register 1s and 1m buffers with the global # register 1s and 1m buffers with the global

View File

@ -24,8 +24,11 @@ from pyqtgraph import (
Point, Point,
functions as fn, functions as fn,
Color, Color,
GraphicsObject,
) )
from pyqtgraph.Qt import internals
import numpy as np import numpy as np
import pyqtgraph as pg
from piker.ui.qt import ( from piker.ui.qt import (
QtCore, QtCore,
@ -35,6 +38,10 @@ from piker.ui.qt import (
QRectF, QRectF,
QGraphicsPathItem, QGraphicsPathItem,
) )
from piker.ui._style import hcolor
from piker.log import get_logger
log = get_logger(__name__)
def mk_marker_path( def mk_marker_path(
@ -104,7 +111,7 @@ def mk_marker_path(
class LevelMarker(QGraphicsPathItem): class LevelMarker(QGraphicsPathItem):
''' '''
An arrow marker path graphich which redraws itself An arrow marker path graphic which redraws itself
to the specified view coordinate level on each paint cycle. to the specified view coordinate level on each paint cycle.
''' '''
@ -251,9 +258,9 @@ def qgo_draw_markers(
) -> float: ) -> float:
''' '''
Paint markers in ``pg.GraphicsItem`` style by first Paint markers in ``pg.GraphicsItem`` style by first removing the
removing the view transform for the painter, drawing the markers view transform for the painter, drawing the markers in scene
in scene coords, then restoring the view coords. coords, then restoring the view coords.
''' '''
# paint markers in native coordinate system # paint markers in native coordinate system
@ -295,3 +302,449 @@ def qgo_draw_markers(
p.setTransform(orig_tr) p.setTransform(orig_tr)
return max(sizes) return max(sizes)
class GapAnnotations(GraphicsObject):
'''
Batch-rendered gap annotations using Qt's efficient drawing
APIs.
Instead of creating individual `QGraphicsItem` instances per
gap (which is very slow for 1000+ gaps), this class stores all
gap rectangles and arrows in numpy-backed arrays and renders
them in single batch paint calls.
Performance: ~1000x faster than individual items for large gap
counts.
Based on patterns from:
- `pyqtgraph.BarGraphItem` (batch rect rendering)
- `pyqtgraph.ScatterPlotItem` (fragment rendering)
- `piker.ui._curve.FlowGraphic` (single path pattern)
'''
def __init__(
self,
gap_specs: list[dict],
array: np.ndarray|None = None,
color: str = 'dad_blue',
alpha: int = 169,
arrow_size: float = 10.0,
fqme: str|None = None,
timeframe: float|None = None,
) -> None:
'''
gap_specs: list of dicts with keys:
- start_pos: (x, y) tuple for left corner of rect
- end_pos: (x, y) tuple for right corner of rect
- arrow_x: x position for arrow
- arrow_y: y position for arrow
- pointing: 'up' or 'down' for arrow direction
- start_time: (optional) timestamp for repositioning
- end_time: (optional) timestamp for repositioning
array: optional OHLC numpy array for repositioning on
backfill updates (when abs-index changes)
fqme: symbol name for these gaps (for logging/debugging)
timeframe: period in seconds that these gaps were
detected on (used to skip reposition when
called with wrong timeframe's array)
'''
super().__init__()
self._gap_specs = gap_specs
self._array = array
self._fqme = fqme
self._timeframe = timeframe
n_gaps = len(gap_specs)
# shared pen/brush matching original SelectRect/ArrowItem style
base_color = pg.mkColor(hcolor(color))
# rect pen: base color, fully opaque for outline
self._rect_pen = pg.mkPen(base_color, width=1)
# rect brush: base color with alpha=66 (SelectRect default)
rect_fill = pg.mkColor(hcolor(color))
rect_fill.setAlpha(66)
self._rect_brush = pg.functions.mkBrush(rect_fill)
# arrow pen: same as rects
self._arrow_pen = pg.mkPen(base_color, width=1)
# arrow brush: base color with user-specified alpha (default 169)
arrow_fill = pg.mkColor(hcolor(color))
arrow_fill.setAlpha(alpha)
self._arrow_brush = pg.functions.mkBrush(arrow_fill)
# allocate rect array using Qt's efficient storage
self._rectarray = internals.PrimitiveArray(
QtCore.QRectF,
4,
)
self._rectarray.resize(n_gaps)
rect_memory = self._rectarray.ndarray()
# fill rect array from gap specs
for (
i,
spec,
) in enumerate(gap_specs):
(
start_x,
start_y,
) = spec['start_pos']
(
end_x,
end_y,
) = spec['end_pos']
# QRectF expects (x, y, width, height)
rect_memory[i, 0] = start_x
rect_memory[i, 1] = min(start_y, end_y)
rect_memory[i, 2] = end_x - start_x
rect_memory[i, 3] = abs(end_y - start_y)
# build single QPainterPath for all arrows
self._arrow_path = QtGui.QPainterPath()
self._arrow_size = arrow_size
for spec in gap_specs:
arrow_x = spec['arrow_x']
arrow_y = spec['arrow_y']
pointing = spec['pointing']
# create arrow polygon
if pointing == 'down':
# arrow points downward
arrow_poly = QtGui.QPolygonF([
QPointF(arrow_x, arrow_y), # tip
QPointF(
arrow_x - arrow_size/2,
arrow_y - arrow_size,
), # left
QPointF(
arrow_x + arrow_size/2,
arrow_y - arrow_size,
), # right
])
else: # up
# arrow points upward
arrow_poly = QtGui.QPolygonF([
QPointF(arrow_x, arrow_y), # tip
QPointF(
arrow_x - arrow_size/2,
arrow_y + arrow_size,
), # left
QPointF(
arrow_x + arrow_size/2,
arrow_y + arrow_size,
), # right
])
self._arrow_path.addPolygon(arrow_poly)
self._arrow_path.closeSubpath()
# cache bounding rect
self._br: QRectF|None = None
def boundingRect(self) -> QRectF:
'''
Compute bounding rect from rect array and arrow path.
'''
if self._br is not None:
return self._br
# get rect bounds
rect_memory = self._rectarray.ndarray()
if len(rect_memory) == 0:
self._br = QRectF()
return self._br
x_min = rect_memory[:, 0].min()
y_min = rect_memory[:, 1].min()
x_max = (rect_memory[:, 0] + rect_memory[:, 2]).max()
y_max = (rect_memory[:, 1] + rect_memory[:, 3]).max()
# expand for arrow path
arrow_br = self._arrow_path.boundingRect()
x_min = min(x_min, arrow_br.left())
y_min = min(y_min, arrow_br.top())
x_max = max(x_max, arrow_br.right())
y_max = max(y_max, arrow_br.bottom())
self._br = QRectF(
x_min,
y_min,
x_max - x_min,
y_max - y_min,
)
return self._br
def paint(
self,
p: QtGui.QPainter,
opt: QtWidgets.QStyleOptionGraphicsItem,
w: QtWidgets.QWidget,
) -> None:
'''
Batch render all rects and arrows in minimal paint calls.
'''
# draw all rects in single batch call (data coordinates)
p.setPen(self._rect_pen)
p.setBrush(self._rect_brush)
drawargs = self._rectarray.drawargs()
p.drawRects(*drawargs)
# draw arrows in scene/pixel coordinates so they maintain
# size regardless of zoom level
orig_tr = p.transform()
p.resetTransform()
# rebuild arrow path in scene coordinates
arrow_path_scene = QtGui.QPainterPath()
# arrow geometry matching pg.ArrowItem defaults
# headLen=10, headWidth=2.222
# headWidth is the half-width (center to edge distance)
head_len = self._arrow_size
head_width = head_len * 0.2222 # 2.222 at size=10
for spec in self._gap_specs:
if 'arrow_x' not in spec:
continue
arrow_x = spec['arrow_x']
arrow_y = spec['arrow_y']
pointing = spec['pointing']
# transform data coords to scene coords
scene_pt = orig_tr.map(QPointF(arrow_x, arrow_y))
sx = scene_pt.x()
sy = scene_pt.y()
# create arrow polygon in scene/pixel coords
# matching pg.ArrowItem geometry but rotated for up/down
if pointing == 'down':
# tip points downward (negative y direction)
arrow_poly = QtGui.QPolygonF([
QPointF(sx, sy), # tip
QPointF(
sx - head_width,
sy - head_len,
), # left base
QPointF(
sx + head_width,
sy - head_len,
), # right base
])
else: # up
# tip points upward (positive y direction)
arrow_poly = QtGui.QPolygonF([
QPointF(sx, sy), # tip
QPointF(
sx - head_width,
sy + head_len,
), # left base
QPointF(
sx + head_width,
sy + head_len,
), # right base
])
arrow_path_scene.addPolygon(arrow_poly)
arrow_path_scene.closeSubpath()
p.setPen(self._arrow_pen)
p.setBrush(self._arrow_brush)
p.drawPath(arrow_path_scene)
# restore original transform
p.setTransform(orig_tr)
def reposition(
self,
array: np.ndarray|None = None,
fqme: str|None = None,
timeframe: float|None = None,
) -> None:
'''
Reposition all annotations based on timestamps.
Used when viz is updated (eg during backfill) and abs-index
range changes - we need to lookup new indices from timestamps.
'''
# skip reposition if timeframe doesn't match
# (e.g., 1s gaps being repositioned with 60s array)
if (
timeframe is not None
and
self._timeframe is not None
and
timeframe != self._timeframe
):
log.debug(
f'Skipping reposition for {self._fqme} gaps:\n'
f' gap timeframe: {self._timeframe}s\n'
f' array timeframe: {timeframe}s\n'
)
return
if array is None:
array = self._array
if array is None:
log.warning(
'GapAnnotations.reposition() called but no array '
'provided'
)
return
# collect all unique timestamps we need to lookup
timestamps: set[float] = set()
for spec in self._gap_specs:
if spec.get('start_time') is not None:
timestamps.add(spec['start_time'])
if spec.get('end_time') is not None:
timestamps.add(spec['end_time'])
if spec.get('time') is not None:
timestamps.add(spec['time'])
# vectorized timestamp -> row lookup using binary search
time_to_row: dict[float, dict] = {}
if timestamps:
import numpy as np
time_arr = array['time']
ts_array = np.array(list(timestamps))
search_indices = np.searchsorted(
time_arr,
ts_array,
)
# vectorized bounds check and exact match verification
valid_mask = (
(search_indices < len(array))
& (time_arr[search_indices] == ts_array)
)
valid_indices = search_indices[valid_mask]
valid_timestamps = ts_array[valid_mask]
matched_rows = array[valid_indices]
time_to_row = {
float(ts): {
'index': float(row['index']),
'open': float(row['open']),
'close': float(row['close']),
}
for ts, row in zip(
valid_timestamps,
matched_rows,
)
}
# rebuild rect array from gap specs with new indices
rect_memory = self._rectarray.ndarray()
for (
i,
spec,
) in enumerate(self._gap_specs):
start_time = spec.get('start_time')
end_time = spec.get('end_time')
if (
start_time is None
or end_time is None
):
continue
start_row = time_to_row.get(start_time)
end_row = time_to_row.get(end_time)
if (
start_row is None
or end_row is None
):
log.warning(
f'Timestamp lookup failed for gap[{i}] during '
f'reposition:\n'
f' fqme: {fqme}\n'
f' timeframe: {timeframe}s\n'
f' start_time: {start_time}\n'
f' end_time: {end_time}\n'
f' array time range: '
f'{array["time"][0]} -> {array["time"][-1]}\n'
)
continue
start_idx = start_row['index']
end_idx = end_row['index']
start_close = start_row['close']
end_open = end_row['open']
from_idx: float = 0.16 - 0.06
start_x = start_idx + 1 - from_idx
end_x = end_idx + from_idx
# update rect in array
rect_memory[i, 0] = start_x
rect_memory[i, 1] = min(start_close, end_open)
rect_memory[i, 2] = end_x - start_x
rect_memory[i, 3] = abs(end_open - start_close)
# rebuild arrow path with new indices
self._arrow_path.clear()
for spec in self._gap_specs:
time_val = spec.get('time')
if time_val is None:
continue
arrow_row = time_to_row.get(time_val)
if arrow_row is None:
continue
arrow_x = arrow_row['index']
arrow_y = arrow_row['close']
pointing = spec['pointing']
# create arrow polygon
if pointing == 'down':
arrow_poly = QtGui.QPolygonF([
QPointF(arrow_x, arrow_y),
QPointF(
arrow_x - self._arrow_size/2,
arrow_y - self._arrow_size,
),
QPointF(
arrow_x + self._arrow_size/2,
arrow_y - self._arrow_size,
),
])
else: # up
arrow_poly = QtGui.QPolygonF([
QPointF(arrow_x, arrow_y),
QPointF(
arrow_x - self._arrow_size/2,
arrow_y + self._arrow_size,
),
QPointF(
arrow_x + self._arrow_size/2,
arrow_y + self._arrow_size,
),
])
self._arrow_path.addPolygon(arrow_poly)
self._arrow_path.closeSubpath()
# invalidate bounding rect cache
self._br = None
self.prepareGeometryChange()
self.update()

View File

@ -33,7 +33,10 @@ from . import _search
from ..accounting import unpack_fqme from ..accounting import unpack_fqme
from ..data._symcache import open_symcache from ..data._symcache import open_symcache
from ..data.feed import install_brokerd_search from ..data.feed import install_brokerd_search
from ..log import get_logger from ..log import (
get_logger,
get_console_log,
)
from ..service import maybe_spawn_brokerd from ..service import maybe_spawn_brokerd
from ._exec import run_qtractor from ._exec import run_qtractor
@ -87,6 +90,13 @@ async def _async_main(
Provision the "main" widget with initial symbol data and root nursery. Provision the "main" widget with initial symbol data and root nursery.
""" """
# enable chart's console logging
if loglevel:
get_console_log(
level=loglevel,
name=__name__,
)
# set as singleton # set as singleton
_chart._godw = main_widget _chart._godw = main_widget

View File

@ -413,9 +413,18 @@ class Cursor(pg.GraphicsObject):
self, self,
item: pg.GraphicsObject, item: pg.GraphicsObject,
) -> None: ) -> None:
assert getattr(item, 'delete'), f"{item} must define a ``.delete()``" assert getattr(
item,
'delete',
), f"{item} must define a ``.delete()``"
self._hovered.add(item) self._hovered.add(item)
def is_hovered(
self,
item: pg.GraphicsObject,
) -> bool:
return item in self._hovered
def add_plot( def add_plot(
self, self,
plot: ChartPlotWidget, # noqa plot: ChartPlotWidget, # noqa

View File

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

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. graphics update methods via our custom ``pyqtgraph`` charting api.
''' '''
from functools import partial
import itertools import itertools
from math import floor from math import floor
import time import time
@ -208,6 +209,7 @@ class DisplayState(Struct):
async def increment_history_view( async def increment_history_view(
# min_istream: tractor.MsgStream, # min_istream: tractor.MsgStream,
ds: DisplayState, ds: DisplayState,
loglevel: str = 'warning',
): ):
hist_chart: ChartPlotWidget = ds.hist_chart hist_chart: ChartPlotWidget = ds.hist_chart
hist_viz: Viz = ds.hist_viz hist_viz: Viz = ds.hist_viz
@ -229,7 +231,10 @@ async def increment_history_view(
hist_viz.reset_graphics() hist_viz.reset_graphics()
# hist_viz.update_graphics(force_redraw=True) # hist_viz.update_graphics(force_redraw=True)
async with open_sample_stream(1.) as min_istream: async with open_sample_stream(
period_s=1.,
loglevel=loglevel,
) as min_istream:
async for msg in min_istream: async for msg in min_istream:
profiler = Profiler( profiler = Profiler(
@ -310,7 +315,6 @@ async def increment_history_view(
async def graphics_update_loop( async def graphics_update_loop(
dss: dict[str, DisplayState], dss: dict[str, DisplayState],
nurse: trio.Nursery, nurse: trio.Nursery,
godwidget: GodWidget, godwidget: GodWidget,
@ -319,6 +323,7 @@ async def graphics_update_loop(
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {}, pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {},
vlm_charts: dict[str, ChartPlotWidget] = {}, vlm_charts: dict[str, ChartPlotWidget] = {},
loglevel: str = 'warning',
) -> None: ) -> None:
''' '''
@ -462,9 +467,12 @@ async def graphics_update_loop(
# }) # })
nurse.start_soon( nurse.start_soon(
increment_history_view, partial(
# min_istream, increment_history_view,
ds, # min_istream,
ds=ds,
loglevel=loglevel,
),
) )
await trio.sleep(0) await trio.sleep(0)
@ -511,14 +519,19 @@ async def graphics_update_loop(
fast_chart.linked.isHidden() fast_chart.linked.isHidden()
or not rt_pi.isVisible() or not rt_pi.isVisible()
): ):
print(f'{fqme} skipping update for HIDDEN CHART') log.debug(
f'{fqme} skipping update for HIDDEN CHART'
)
fast_chart.pause_all_feeds() fast_chart.pause_all_feeds()
continue continue
ic = fast_chart.view._in_interact ic = fast_chart.view._in_interact
if ic: if ic:
fast_chart.pause_all_feeds() fast_chart.pause_all_feeds()
print(f'{fqme} PAUSING DURING INTERACTION') log.debug(
f'Pausing chart updaates during interaction\n'
f'fqme: {fqme!r}'
)
await ic.wait() await ic.wait()
fast_chart.resume_all_feeds() fast_chart.resume_all_feeds()
@ -1591,15 +1604,18 @@ async def display_symbol_data(
# start update loop task # start update loop task
dss: dict[str, DisplayState] = {} dss: dict[str, DisplayState] = {}
ln.start_soon( ln.start_soon(
graphics_update_loop, partial(
dss, graphics_update_loop,
ln, dss=dss,
godwidget, nurse=ln,
feed, godwidget=godwidget,
# min_istream, feed=feed,
# min_istream,
pis, pis=pis,
vlm_charts, vlm_charts=vlm_charts,
loglevel=loglevel,
)
) )
# boot order-mode # boot order-mode

View File

@ -168,7 +168,7 @@ class ArrowEditor(Struct):
''' '''
uid: str = arrow._uid uid: str = arrow._uid
arrows: list[pg.ArrowItem] = self._arrows[uid] arrows: list[pg.ArrowItem] = self._arrows[uid]
log.info( log.debug(
f'Removing arrow from views\n' f'Removing arrow from views\n'
f'uid: {uid!r}\n' f'uid: {uid!r}\n'
f'{arrow!r}\n' f'{arrow!r}\n'
@ -286,7 +286,9 @@ class LineEditor(Struct):
for line in lines: for line in lines:
line.show_labels() line.show_labels()
line.hide_markers() line.hide_markers()
log.debug(f'Level active for level: {line.value()}') log.debug(
f'Line active @ level: {line.value()!r}'
)
# TODO: other flashy things to indicate the order is active # TODO: other flashy things to indicate the order is active
return lines return lines
@ -329,7 +331,11 @@ class LineEditor(Struct):
if line in hovered: if line in hovered:
hovered.remove(line) hovered.remove(line)
log.debug(f'deleting {line} with oid: {uuid}') log.debug(
f'Deleting level-line\n'
f'line: {line!r}\n'
f'oid: {uuid!r}\n'
)
line.delete() line.delete()
# make sure the xhair doesn't get left off # make sure the xhair doesn't get left off
@ -337,7 +343,11 @@ class LineEditor(Struct):
cursor.show_xhair() cursor.show_xhair()
else: else:
log.warning(f'Could not find line for {line}') log.warning(
f'Could not find line for removal ??\n'
f'\n'
f'{line!r}\n'
)
return lines return lines
@ -569,11 +579,11 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
if update_label: if update_label:
self.init_label(view_rect) self.init_label(view_rect)
print( log.debug(
'SelectRect modify:\n' f'SelectRect modify,\n'
f'QRectF: {view_rect}\n' f'QRectF: {view_rect}\n'
f'start_pos: {start_pos}\n' f'start_pos: {start_pos!r}\n'
f'end_pos: {end_pos}\n' f'end_pos: {end_pos!r}\n'
) )
self.show() self.show()
@ -640,8 +650,11 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
dmn=dmn, dmn=dmn,
)) ))
# print(f'x2, y2: {(x2, y2)}') # tracing
# print(f'xmn, ymn: {(xmn, ymx)}') # log.info(
# f'x2, y2: {(x2, y2)}\n'
# f'xmn, ymn: {(xmn, ymx)}\n'
# )
label_anchor = Point( label_anchor = Point(
xmx + 2, xmx + 2,

View File

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

View File

@ -43,6 +43,7 @@ from pyqtgraph import (
functions as fn, functions as fn,
) )
import numpy as np import numpy as np
import tractor
import trio import trio
from piker.ui.qt import ( from piker.ui.qt import (
@ -72,7 +73,10 @@ if TYPE_CHECKING:
GodWidget, GodWidget,
) )
from ._dataviz import Viz from ._dataviz import Viz
from .order_mode import OrderMode from .order_mode import (
OrderMode,
Dialog,
)
from ._display import DisplayState from ._display import DisplayState
@ -130,7 +134,12 @@ async def handle_viewmode_kb_inputs(
async for kbmsg in recv_chan: async for kbmsg in recv_chan:
event, etype, key, mods, text = kbmsg.to_tuple() event, etype, key, mods, text = kbmsg.to_tuple()
log.debug(f'key: {key}, mods: {mods}, text: {text}') log.debug(
f'View-mode kb-msg received,\n'
f'mods: {mods!r}\n'
f'key: {key!r}\n'
f'text: {text!r}\n'
)
now = time.time() now = time.time()
period = now - last period = now - last
@ -158,8 +167,12 @@ async def handle_viewmode_kb_inputs(
# have no previous keys or we do and the min_tap period is # have no previous keys or we do and the min_tap period is
# met # met
if ( if (
not fast_key_seq or not fast_key_seq
period <= min_tap and fast_key_seq or (
period <= min_tap
and
fast_key_seq
)
): ):
fast_key_seq.append(text) fast_key_seq.append(text)
log.debug(f'fast keys seqs {fast_key_seq}') log.debug(f'fast keys seqs {fast_key_seq}')
@ -174,7 +187,8 @@ async def handle_viewmode_kb_inputs(
# UI REPL-shell, with ctrl-p (for "pause") # UI REPL-shell, with ctrl-p (for "pause")
if ( if (
ctrl ctrl
and key in { and
key in {
Qt.Key_P, Qt.Key_P,
} }
): ):
@ -184,7 +198,6 @@ async def handle_viewmode_kb_inputs(
vlm_chart = chart.linked.subplots['volume'] # noqa vlm_chart = chart.linked.subplots['volume'] # noqa
vlm_viz = vlm_chart.main_viz # noqa vlm_viz = vlm_chart.main_viz # noqa
dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa
import tractor
await tractor.pause() await tractor.pause()
view.interact_graphics_cycle() view.interact_graphics_cycle()
@ -192,7 +205,8 @@ async def handle_viewmode_kb_inputs(
# shown data `Viz`s for the current chart app. # shown data `Viz`s for the current chart app.
if ( if (
ctrl ctrl
and key in { and
key in {
Qt.Key_R, Qt.Key_R,
} }
): ):
@ -231,7 +245,8 @@ async def handle_viewmode_kb_inputs(
key == Qt.Key_Escape key == Qt.Key_Escape
or ( or (
ctrl ctrl
and key == Qt.Key_C and
key == Qt.Key_C
) )
): ):
# ctrl-c as cancel # ctrl-c as cancel
@ -242,17 +257,35 @@ async def handle_viewmode_kb_inputs(
# cancel order or clear graphics # cancel order or clear graphics
if ( if (
key == Qt.Key_C key == Qt.Key_C
or key == Qt.Key_Delete or
key == Qt.Key_Delete
): ):
# log.info('Handling <c> hotkey!')
try:
dialogs: list[Dialog] = order_mode.cancel_orders_under_cursor()
except BaseException:
log.exception('Failed to cancel orders !?\n')
await tractor.pause()
order_mode.cancel_orders_under_cursor() if not dialogs:
log.warning(
'No orders were cancelled?\n'
'Is there an order-line under the cursor?\n'
'If you think there IS your DE might be "hiding the mouse" before '
'we rx the keyboard input via Qt..\n'
'=> Check your DE and/or TWM settings to be sure! <=\n'
)
# ^TODO?, some way to detect if there's lines and
# the DE is cuckin with things?
# await tractor.pause()
# View modes # View modes
if ( if (
ctrl ctrl
and ( and (
key == Qt.Key_Equal key == Qt.Key_Equal
or key == Qt.Key_I or
key == Qt.Key_I
) )
): ):
view.wheelEvent( view.wheelEvent(
@ -264,7 +297,8 @@ async def handle_viewmode_kb_inputs(
ctrl ctrl
and ( and (
key == Qt.Key_Minus key == Qt.Key_Minus
or key == Qt.Key_O or
key == Qt.Key_O
) )
): ):
view.wheelEvent( view.wheelEvent(
@ -275,7 +309,8 @@ async def handle_viewmode_kb_inputs(
elif ( elif (
not ctrl not ctrl
and key == Qt.Key_R and
key == Qt.Key_R
): ):
# NOTE: seems that if we don't yield a Qt render # NOTE: seems that if we don't yield a Qt render
# cycle then the m4 downsampled curves will show here # cycle then the m4 downsampled curves will show here
@ -477,7 +512,8 @@ async def handle_viewmode_mouse(
# view.raiseContextMenu(event) # view.raiseContextMenu(event)
if ( if (
view.order_mode.active and view.order_mode.active
and
button == QtCore.Qt.LeftButton button == QtCore.Qt.LeftButton
): ):
# when in order mode, submit execution # when in order mode, submit execution
@ -781,7 +817,8 @@ class ChartView(ViewBox):
# Scale or translate based on mouse button # Scale or translate based on mouse button
if btn & ( if btn & (
QtCore.Qt.LeftButton | QtCore.Qt.MidButton QtCore.Qt.LeftButton
| QtCore.Qt.MidButton
): ):
# zoom y-axis ONLY when click-n-drag on it # zoom y-axis ONLY when click-n-drag on it
# if axis == 1: # if axis == 1:

View File

@ -38,7 +38,6 @@ from piker.ui.qt import (
QtGui, QtGui,
QGraphicsPathItem, QGraphicsPathItem,
QStyleOptionGraphicsItem, QStyleOptionGraphicsItem,
QGraphicsItem,
QGraphicsScene, QGraphicsScene,
QWidget, QWidget,
QPointF, QPointF,
@ -52,10 +51,13 @@ from ._anchors import (
from ..calc import humanize from ..calc import humanize
from ._label import Label from ._label import Label
from ._style import hcolor, _font from ._style import hcolor, _font
from ..log import get_logger
if TYPE_CHECKING: if TYPE_CHECKING:
from ._cursor import Cursor from ._cursor import Cursor
log = get_logger(__name__)
# TODO: probably worth investigating if we can # TODO: probably worth investigating if we can
# make .boundingRect() faster: # make .boundingRect() faster:
@ -347,7 +349,7 @@ class LevelLine(pg.InfiniteLine):
) -> None: ) -> None:
# TODO: enter labels edit mode # TODO: enter labels edit mode
print(f'double click {ev}') log.debug(f'double click {ev}')
def paint( def paint(
self, self,
@ -461,10 +463,19 @@ class LevelLine(pg.InfiniteLine):
# hovered # hovered
if ( if (
not ev.isExit() not ev.isExit()
and ev.acceptDrags(QtCore.Qt.LeftButton) and
ev.acceptDrags(QtCore.Qt.LeftButton)
): ):
# if already hovered we don't need to run again # if already hovered we don't need to run again
if self.mouseHovering is True: if (
self.mouseHovering is True
and
cur.is_hovered(self)
):
log.debug(
f'Already hovering ??\n'
f'cur._hovered: {cur._hovered!r}\n'
)
return return
if self.only_show_markers_on_hover: if self.only_show_markers_on_hover:
@ -481,6 +492,7 @@ class LevelLine(pg.InfiniteLine):
cur._y_label_update = False cur._y_label_update = False
# add us to cursor state # add us to cursor state
log.debug(f'Adding line {self!r}\n')
cur.add_hovered(self) cur.add_hovered(self)
if self._hide_xhair_on_hover: if self._hide_xhair_on_hover:
@ -508,6 +520,7 @@ class LevelLine(pg.InfiniteLine):
self.currentPen = self.pen self.currentPen = self.pen
log.debug(f'Removing line {self!r}\n')
cur._hovered.remove(self) cur._hovered.remove(self)
if self.only_show_markers_on_hover: if self.only_show_markers_on_hover:

View File

@ -22,6 +22,7 @@ a chart from some other actor.
from __future__ import annotations from __future__ import annotations
from contextlib import ( from contextlib import (
asynccontextmanager as acm, asynccontextmanager as acm,
contextmanager as cm,
AsyncExitStack, AsyncExitStack,
) )
from functools import partial from functools import partial
@ -46,6 +47,7 @@ from piker.log import get_logger
from piker.types import Struct from piker.types import Struct
from piker.service import find_service from piker.service import find_service
from piker.brokers import SymbolNotFound from piker.brokers import SymbolNotFound
from piker.toolz import Profiler
from piker.ui.qt import ( from piker.ui.qt import (
QGraphicsItem, QGraphicsItem,
) )
@ -98,6 +100,8 @@ def rm_annot(
annot: ArrowEditor|SelectRect|pg.TextItem annot: ArrowEditor|SelectRect|pg.TextItem
) -> bool: ) -> bool:
global _editors global _editors
from piker.ui._annotate import GapAnnotations
match annot: match annot:
case pg.ArrowItem(): case pg.ArrowItem():
editor = _editors[annot._uid] editor = _editors[annot._uid]
@ -122,9 +126,35 @@ def rm_annot(
scene.removeItem(annot) scene.removeItem(annot)
return True return True
case GapAnnotations():
scene = annot.scene()
if scene:
scene.removeItem(annot)
return True
return False return False
@cm
def no_qt_updates(*items):
'''
Disable Qt widget/item updates during context to batch
render operations and only trigger single repaint on exit.
Accepts both QWidgets and QGraphicsItems.
'''
for item in items:
if hasattr(item, 'setUpdatesEnabled'):
item.setUpdatesEnabled(False)
try:
yield
finally:
for item in items:
if hasattr(item, 'setUpdatesEnabled'):
item.setUpdatesEnabled(True)
async def serve_rc_annots( async def serve_rc_annots(
ipc_key: str, ipc_key: str,
annot_req_stream: MsgStream, annot_req_stream: MsgStream,
@ -429,6 +459,333 @@ async def serve_rc_annots(
aids.add(aid) aids.add(aid)
await annot_req_stream.send(aid) await annot_req_stream.send(aid)
case {
'cmd': 'batch',
'fqme': fqme,
'timeframe': timeframe,
'rects': list(rect_specs),
'arrows': list(arrow_specs),
'texts': list(text_specs),
'show_individual_arrows': bool(show_individual_arrows),
}:
# batch submission handler - process multiple
# annotations in single IPC round-trip
ds: DisplayState = _dss[fqme]
try:
chart: ChartPlotWidget = {
60: ds.hist_chart,
1: ds.chart,
}[timeframe]
except KeyError:
msg: str = (
f'No chart for timeframe={timeframe}s, '
f'skipping batch annotation'
)
log.error(msg)
await annot_req_stream.send({'error': msg})
continue
cv: ChartView = chart.cv
viz: Viz = chart.get_viz(fqme)
shm = viz.shm
arr = shm.array
result: dict[str, list[int]] = {
'rects': [],
'arrows': [],
'texts': [],
}
profiler = Profiler(
msg=(
f'Batch annotate {len(rect_specs)} gaps '
f'on {fqme}@{timeframe}s'
),
disabled=False,
delayed=False,
)
aids_set: set[int] = ctxs[ipc_key][1]
# build unified gap_specs for GapAnnotations class
from piker.ui._annotate import GapAnnotations
gap_specs: list[dict] = []
n_gaps: int = max(
len(rect_specs),
len(arrow_specs),
)
profiler('setup batch annot creation')
# collect all unique timestamps for vectorized lookup
timestamps: list[float] = []
for rect_spec in rect_specs:
if start_time := rect_spec.get('start_time'):
timestamps.append(start_time)
if end_time := rect_spec.get('end_time'):
timestamps.append(end_time)
for arrow_spec in arrow_specs:
if time_val := arrow_spec.get('time'):
timestamps.append(time_val)
profiler('collect `timestamps: list` complet!')
# build timestamp -> row mapping using binary search
# O(m log n) instead of O(n*m) with np.isin
time_to_row: dict[float, dict] = {}
if timestamps:
import numpy as np
time_arr = arr['time']
ts_array = np.array(timestamps)
# binary search for each timestamp in sorted time array
search_indices = np.searchsorted(
time_arr,
ts_array,
)
profiler('`np.searchsorted()` complete!')
# vectorized bounds check and exact match verification
valid_mask = (
(search_indices < len(arr))
& (time_arr[search_indices] == ts_array)
)
# get all valid indices and timestamps
valid_indices = search_indices[valid_mask]
valid_timestamps = ts_array[valid_mask]
# use fancy indexing to get all rows at once
matched_rows = arr[valid_indices]
# extract fields to plain arrays BEFORE dict building
indices_arr = matched_rows['index'].astype(float)
opens_arr = matched_rows['open'].astype(float)
closes_arr = matched_rows['close'].astype(float)
profiler('extracted field arrays')
# build dict from plain arrays (much faster)
time_to_row: dict[float, dict] = {
float(ts): {
'index': idx,
'open': opn,
'close': cls,
}
for (
ts,
idx,
opn,
cls,
) in zip(
valid_timestamps,
indices_arr,
opens_arr,
closes_arr,
)
}
profiler('`time_to_row` creation complete!')
profiler(f'built timestamp lookup for {len(timestamps)} times')
# build gap_specs from rect+arrow specs
for i in range(n_gaps):
gap_spec: dict = {}
# get rect spec for this gap
if i < len(rect_specs):
rect_spec: dict = rect_specs[i].copy()
start_time = rect_spec.get('start_time')
end_time = rect_spec.get('end_time')
if (
start_time is not None
and end_time is not None
):
# lookup from pre-built mapping
start_row = time_to_row.get(start_time)
end_row = time_to_row.get(end_time)
if (
start_row is None
or end_row is None
):
log.warning(
f'Timestamp lookup failed for '
f'gap[{i}], skipping'
)
continue
start_idx = start_row['index']
end_idx = end_row['index']
start_close = start_row['close']
end_open = end_row['open']
from_idx: float = 0.16 - 0.06
gap_spec['start_pos'] = (
start_idx + 1 - from_idx,
start_close,
)
gap_spec['end_pos'] = (
end_idx + from_idx,
end_open,
)
gap_spec['start_time'] = start_time
gap_spec['end_time'] = end_time
gap_spec['color'] = rect_spec.get(
'color',
'dad_blue',
)
# get arrow spec for this gap
if i < len(arrow_specs):
arrow_spec: dict = arrow_specs[i].copy()
x: float = float(arrow_spec.get('x', 0))
y: float = float(arrow_spec.get('y', 0))
time_val: float|None = arrow_spec.get('time')
# timestamp-based index lookup (only for x, NOT y!)
# y is already set to the PREVIOUS bar's close
if time_val is not None:
arrow_row = time_to_row.get(time_val)
if arrow_row is not None:
x = arrow_row['index']
# NOTE: do NOT update y! it's the
# previous bar's close, not current
else:
log.warning(
f'Arrow timestamp {time_val} not '
f'found for gap[{i}], using x={x}'
)
gap_spec['arrow_x'] = x
gap_spec['arrow_y'] = y
gap_spec['time'] = time_val
gap_spec['pointing'] = arrow_spec.get(
'pointing',
'down',
)
gap_spec['alpha'] = arrow_spec.get('alpha', 169)
gap_specs.append(gap_spec)
profiler(f'built {len(gap_specs)} gap_specs')
# create single GapAnnotations item for all gaps
if gap_specs:
gaps_item = GapAnnotations(
gap_specs=gap_specs,
array=arr,
color=gap_specs[0].get('color', 'dad_blue'),
alpha=gap_specs[0].get('alpha', 169),
arrow_size=10.0,
fqme=fqme,
timeframe=timeframe,
)
chart.plotItem.addItem(gaps_item)
# register single item for repositioning
aid: int = id(gaps_item)
annots[aid] = gaps_item
aids_set.add(aid)
result['rects'].append(aid)
profiler(
f'created GapAnnotations item for {len(gap_specs)} '
f'gaps'
)
# A/B comparison: optionally create individual arrows
# alongside batch for visual comparison
if show_individual_arrows:
godw = chart.linked.godwidget
arrows: ArrowEditor = ArrowEditor(godw=godw)
for i, spec in enumerate(gap_specs):
if 'arrow_x' not in spec:
continue
aid_str: str = str(uuid4())
arrow: pg.ArrowItem = arrows.add(
plot=chart.plotItem,
uid=aid_str,
x=spec['arrow_x'],
y=spec['arrow_y'],
pointing=spec['pointing'],
color='bracket', # different color
alpha=spec.get('alpha', 169),
headLen=10.0,
headWidth=2.222,
pxMode=True,
)
arrow._abs_x = spec['arrow_x']
arrow._abs_y = spec['arrow_y']
annots[aid_str] = arrow
_editors[aid_str] = arrows
aids_set.add(aid_str)
result['arrows'].append(aid_str)
profiler(
f'created {len(gap_specs)} individual arrows '
f'for comparison'
)
# handle text items separately (less common, keep
# individual items)
n_texts: int = 0
for text_spec in text_specs:
kwargs: dict = text_spec.copy()
text: str = kwargs.pop('text')
x: float = float(kwargs.pop('x'))
y: float = float(kwargs.pop('y'))
time_val: float|None = kwargs.pop('time', None)
# timestamp-based index lookup
if time_val is not None:
matches = arr[arr['time'] == time_val]
if len(matches) > 0:
x = float(matches[0]['index'])
y = float(matches[0]['close'])
color = kwargs.pop('color', 'dad_blue')
anchor = kwargs.pop('anchor', (0, 1))
font_size = kwargs.pop('font_size', None)
text_item: pg.TextItem = pg.TextItem(
text,
color=hcolor(color),
anchor=anchor,
)
if font_size is None:
from ._style import get_fonts
font, font_small = get_fonts()
font_size = font_small.px_size - 1
qfont: QFont = text_item.textItem.font()
qfont.setPixelSize(font_size)
text_item.setFont(qfont)
text_item.setPos(float(x), float(y))
chart.plotItem.addItem(text_item)
text_item._abs_x = float(x)
text_item._abs_y = float(y)
aid: str = str(uuid4())
annots[aid] = text_item
aids_set.add(aid)
result['texts'].append(aid)
n_texts += 1
profiler(
f'created text annotations: {n_texts} texts'
)
profiler.finish()
await annot_req_stream.send(result)
case { case {
'cmd': 'remove', 'cmd': 'remove',
'aid': int(aid)|str(aid), 'aid': int(aid)|str(aid),
@ -471,10 +828,26 @@ async def serve_rc_annots(
# XXX: reposition all annotations to ensure they # XXX: reposition all annotations to ensure they
# stay aligned with viz data after reset (eg during # stay aligned with viz data after reset (eg during
# backfill when abs-index range changes) # backfill when abs-index range changes)
chart: ChartPlotWidget = {
60: ds.hist_chart,
1: ds.chart,
}[timeframe]
viz: Viz = chart.get_viz(fqme)
arr = viz.shm.array
n_repositioned: int = 0 n_repositioned: int = 0
for aid, annot in annots.items(): for aid, annot in annots.items():
# GapAnnotations batch items have .reposition()
if hasattr(annot, 'reposition'):
annot.reposition(
array=arr,
fqme=fqme,
timeframe=timeframe,
)
n_repositioned += 1
# arrows and text items use abs x,y coords # arrows and text items use abs x,y coords
if ( elif (
hasattr(annot, '_abs_x') hasattr(annot, '_abs_x')
and and
hasattr(annot, '_abs_y') hasattr(annot, '_abs_y')
@ -539,12 +912,21 @@ async def remote_annotate(
finally: finally:
# ensure all annots for this connection are deleted # ensure all annots for this connection are deleted
# on any final teardown # on any final teardown
profiler = Profiler(
msg=f'Annotation teardown for ctx {ctx.cid}',
disabled=False,
ms_threshold=0.0,
)
(_ctx, aids) = _ctxs[ctx.cid] (_ctx, aids) = _ctxs[ctx.cid]
assert _ctx is ctx assert _ctx is ctx
profiler(f'got {len(aids)} aids to remove')
for aid in aids: for aid in aids:
annot: QGraphicsItem = _annots[aid] annot: QGraphicsItem = _annots[aid]
assert rm_annot(annot) assert rm_annot(annot)
profiler(f'removed all {len(aids)} annotations')
class AnnotCtl(Struct): class AnnotCtl(Struct):
''' '''
@ -746,6 +1128,64 @@ class AnnotCtl(Struct):
) )
return aid return aid
async def add_batch(
self,
fqme: str,
timeframe: float,
rects: list[dict]|None = None,
arrows: list[dict]|None = None,
texts: list[dict]|None = None,
show_individual_arrows: bool = False,
from_acm: bool = False,
) -> dict[str, list[int]]:
'''
Batch submit multiple annotations in single IPC msg for
much faster remote annotation vs. per-annot round-trips.
Returns dict of annotation IDs:
{
'rects': [aid1, aid2, ...],
'arrows': [aid3, aid4, ...],
'texts': [aid5, aid6, ...],
}
'''
ipc: MsgStream = self._get_ipc(fqme)
with trio.fail_after(10):
await ipc.send({
'fqme': fqme,
'cmd': 'batch',
'timeframe': timeframe,
'rects': rects or [],
'arrows': arrows or [],
'texts': texts or [],
'show_individual_arrows': show_individual_arrows,
})
result: dict = await ipc.receive()
match result:
case {'error': str(msg)}:
log.error(msg)
return {
'rects': [],
'arrows': [],
'texts': [],
}
# register all AIDs with their IPC streams
for aid_list in result.values():
for aid in aid_list:
self._ipcs[aid] = ipc
if not from_acm:
self._annot_stack.push_async_callback(
partial(
self.remove,
aid,
)
)
return result
async def add_text( async def add_text(
self, self,
fqme: str, fqme: str,
@ -881,3 +1321,14 @@ async def open_annot_ctl(
_annot_stack=annots_stack, _annot_stack=annots_stack,
) )
yield client yield client
# client exited, measure teardown time
teardown_profiler = Profiler(
msg='Client AnnotCtl teardown',
disabled=False,
ms_threshold=0.0,
)
teardown_profiler('exiting annots_stack')
teardown_profiler('annots_stack exited')
teardown_profiler('exiting gather_contexts')

View File

@ -300,7 +300,10 @@ class GodWidget(QWidget):
getattr(widget, 'on_resize') getattr(widget, 'on_resize')
self._widgets[widget.mode_name] = widget self._widgets[widget.mode_name] = widget
def on_win_resize(self, event: QtCore.QEvent) -> None: def on_win_resize(
self,
event: QtCore.QEvent,
) -> None:
''' '''
Top level god widget handler from window (the real yaweh) resize Top level god widget handler from window (the real yaweh) resize
events such that any registered widgets which wish to be events such that any registered widgets which wish to be
@ -315,7 +318,10 @@ class GodWidget(QWidget):
self._resizing = True self._resizing = True
log.info('God widget resize') log.debug(
f'God widget resize\n'
f'{event}\n'
)
for name, widget in self._widgets.items(): for name, widget in self._widgets.items():
widget.on_resize() widget.on_resize()

View File

@ -255,8 +255,16 @@ class MainWindow(QMainWindow):
current: QWidget, current: QWidget,
) -> None: ) -> None:
'''
Focus handler.
log.info(f'widget focus changed from {last} -> {current}') For now updates the "current mode" name.
'''
log.debug(
f'widget focus changed from,\n'
f'{last} -> {current}'
)
if current is not None: if current is not None:
# cursor left window? # cursor left window?

View File

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

View File

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

View File

@ -75,6 +75,7 @@ dependencies = [
"trio-typing>=0.10.0", "trio-typing>=0.10.0",
"numba>=0.61.0", "numba>=0.61.0",
"pyvnc", "pyvnc",
"exchange-calendars>=4.13.1",
] ]
# ------ dependencies ------ # ------ dependencies ------
# NOTE, by default we ship only a "headless" deps set bc # NOTE, by default we ship only a "headless" deps set bc
@ -98,12 +99,13 @@ python-downloads = 'manual'
# https://docs.astral.sh/uv/concepts/projects/dependencies/#default-groups # https://docs.astral.sh/uv/concepts/projects/dependencies/#default-groups
default-groups = [ default-groups = [
'uis', 'uis',
'repl',
] ]
# ------ tool.uv ------ # ------ tool.uv ------
[dependency-groups] [dependency-groups]
uis = [ uis = [
"pyqtgraph", "pyqtgraph >= 0.14.0",
"qdarkstyle >=3.0.2, <4.0.0", "qdarkstyle >=3.0.2, <4.0.0",
"pyqt6 >=6.7.0, <7.0.0", "pyqt6 >=6.7.0, <7.0.0",
@ -129,7 +131,7 @@ repl = [
"greenback >=1.1.1, <2.0.0", "greenback >=1.1.1, <2.0.0",
# @goodboy's preferred console toolz # @goodboy's preferred console toolz
"xonsh", "xonsh>=0.22.2",
"prompt-toolkit ==3.0.40", "prompt-toolkit ==3.0.40",
"pyperclip>=1.9.0", "pyperclip>=1.9.0",
@ -190,19 +192,22 @@ include = ["piker"]
[tool.uv.sources] [tool.uv.sources]
pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" }
tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" } tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" }
pyvnc = { git = "https://github.com/regulad/pyvnc.git" } pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
# pyqtgraph = { git = "https://github.com/pyqtgraph/pyqtgraph.git", branch = 'master' }
# pyqtgraph = { path = '../pyqtgraph', editable = true }
# ?TODO, resync our fork?
# pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" }
# to get fancy next-cmd/suggestion feats prior to 0.22.2 B) # to get fancy next-cmd/suggestion feats prior to 0.22.2 B)
# https://github.com/xonsh/xonsh/pull/6037 # https://github.com/xonsh/xonsh/pull/6037
# https://github.com/xonsh/xonsh/pull/6048 # https://github.com/xonsh/xonsh/pull/6048
xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' } # xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
# XXX since, we're like, always hacking new shite all-the-time. Bp # XXX since, we're like, always hacking new shite all-the-time. Bp
# tractor = { git = "https://github.com/goodboy/tractor.git", branch ="piker_pin" } tractor = { git = "https://github.com/goodboy/tractor.git", branch ="piker_pin" }
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" } # tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "main" } # tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "main" }
# ------ goodboy ------ # ------ goodboy ------
# hackin dev-envs, usually there's something new he's hackin in.. # hackin dev-envs, usually there's something new he's hackin in..
tractor = { path = "../tractor", editable = true } # tractor = { path = "../tractor", editable = true }

184
uv.lock
View File

@ -2,8 +2,12 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.12" requires-python = ">=3.12"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.14'", "python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version < '3.14'", "python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
] ]
[[package]] [[package]]
@ -416,6 +420,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
] ]
[[package]]
name = "exchange-calendars"
version = "4.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "korean-lunar-calendar" },
{ name = "numpy" },
{ name = "pandas" },
{ name = "pyluach" },
{ name = "toolz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/fd/1bda66b3c2fefbf54b8cf765c9d8001b12654b5a897a21b0c6c9f55de5e3/exchange_calendars-4.13.1.tar.gz", hash = "sha256:42a4c7296da1f71b9625c668c9b3359cf5de4a2ffca28842b230e062bb4961ba", size = 4119843, upload-time = "2026-02-05T00:15:03.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/b7/fffe7d5a6da6be10b43be96640f31d4191e746de66b046cc1a6ea5fc4f26/exchange_calendars-4.13.1-py3-none-any.whl", hash = "sha256:cf39d2128a4da3ac253283f91ab63d79930a68196a3aac811091a4e38b6cbe49", size = 211538, upload-time = "2026-02-05T00:15:05.694Z" },
]
[[package]] [[package]]
name = "frozenlist" name = "frozenlist"
version = "1.8.0" version = "1.8.0"
@ -659,6 +680,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d3/c3db0b92a0ff39c3e08f168cd382c24bf021d4a96fc89b47a3e55294f883/keysymdef-1.2.0-py2.py3-none-any.whl", hash = "sha256:19a5c2263a861f3ff884a1f58e2b4f7efa319ffc9d11f9ba8e20129babc31a9e", size = 20146, upload-time = "2023-02-25T00:22:36.318Z" }, { url = "https://files.pythonhosted.org/packages/42/d3/c3db0b92a0ff39c3e08f168cd382c24bf021d4a96fc89b47a3e55294f883/keysymdef-1.2.0-py2.py3-none-any.whl", hash = "sha256:19a5c2263a861f3ff884a1f58e2b4f7efa319ffc9d11f9ba8e20129babc31a9e", size = 20146, upload-time = "2023-02-25T00:22:36.318Z" },
] ]
[[package]]
name = "korean-lunar-calendar"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/93/a0bd2bd53ab19330e83ecc5652b7774ae86fd2fee19bc05ad220cf9db08b/korean_lunar_calendar-0.3.1.tar.gz", hash = "sha256:eb2c485124a061016926bdea6d89efdf9b9fdbf16db55895b6cf1e5bec17b857", size = 9877, upload-time = "2022-09-16T10:53:25.713Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/96/30f3fe51b336bb6da4714f4fdad7bbdce8f13af79af2eb75e22908f3f9f4/korean_lunar_calendar-0.3.1-py3-none-any.whl", hash = "sha256:392757135c492c4f42a604e6038042953c35c6f449dda5f27e3f86a7f9c943e5", size = 9033, upload-time = "2022-09-16T10:53:23.771Z" },
]
[[package]] [[package]]
name = "llvmlite" name = "llvmlite"
version = "0.45.1" version = "0.45.1"
@ -953,6 +983,58 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
] ]
[[package]]
name = "pandas"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" },
{ url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" },
{ url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" },
{ url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" },
{ url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" },
{ url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" },
{ url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" },
{ url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" },
{ url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" },
{ url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" },
{ url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" },
{ url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" },
{ url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" },
{ url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" },
{ url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" },
{ url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" },
{ url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" },
{ url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" },
{ url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" },
{ url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" },
{ url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" },
{ url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" },
{ url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" },
{ url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" },
{ url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" },
{ url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" },
{ url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" },
{ url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" },
{ url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" },
{ url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" },
{ url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" },
{ url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" },
{ url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" },
{ url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" },
{ url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" },
{ url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" },
{ url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" },
{ url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" },
]
[[package]] [[package]]
name = "pdbp" name = "pdbp"
version = "1.8.2" version = "1.8.2"
@ -1023,6 +1105,7 @@ dependencies = [
{ name = "colorama" }, { name = "colorama" },
{ name = "colorlog" }, { name = "colorlog" },
{ name = "cryptofeed" }, { name = "cryptofeed" },
{ name = "exchange-calendars" },
{ name = "httpx" }, { name = "httpx" },
{ name = "ib-insync" }, { name = "ib-insync" },
{ name = "msgspec" }, { name = "msgspec" },
@ -1098,6 +1181,7 @@ requires-dist = [
{ name = "colorama", specifier = ">=0.4.6,<0.5.0" }, { name = "colorama", specifier = ">=0.4.6,<0.5.0" },
{ name = "colorlog", specifier = ">=6.7.0,<7.0.0" }, { name = "colorlog", specifier = ">=6.7.0,<7.0.0" },
{ name = "cryptofeed", specifier = ">=2.4.0,<3.0.0" }, { name = "cryptofeed", specifier = ">=2.4.0,<3.0.0" },
{ name = "exchange-calendars", specifier = ">=4.13.1" },
{ name = "httpx", specifier = ">=0.27.0,<0.28.0" }, { name = "httpx", specifier = ">=0.27.0,<0.28.0" },
{ name = "ib-insync", specifier = ">=0.9.86,<0.10.0" }, { name = "ib-insync", specifier = ">=0.9.86,<0.10.0" },
{ name = "msgspec", specifier = ">=0.19.0,<0.20" }, { name = "msgspec", specifier = ">=0.19.0,<0.20" },
@ -1113,7 +1197,7 @@ requires-dist = [
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" }, { name = "tomli", specifier = ">=2.0.1,<3.0.0" },
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" }, { name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" }, { name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
{ name = "tractor", editable = "../tractor" }, { name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=piker_pin" },
{ name = "trio", specifier = ">=0.27" }, { name = "trio", specifier = ">=0.27" },
{ name = "trio-typing", specifier = ">=0.10.0" }, { name = "trio-typing", specifier = ">=0.10.0" },
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" }, { name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
@ -1134,11 +1218,11 @@ dev = [
{ name = "prompt-toolkit", specifier = "==3.0.40" }, { name = "prompt-toolkit", specifier = "==3.0.40" },
{ name = "pyperclip", specifier = ">=1.9.0" }, { name = "pyperclip", specifier = ">=1.9.0" },
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" }, { name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
{ name = "pyqtgraph", git = "https://github.com/pikers/pyqtgraph.git" }, { name = "pyqtgraph", specifier = ">=0.14.0" },
{ name = "pytest" }, { name = "pytest" },
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" }, { name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" }, { name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
{ name = "xonsh", git = "https://github.com/xonsh/xonsh.git?branch=main" }, { name = "xonsh", specifier = ">=0.22.2" },
] ]
lint = [{ name = "ruff", specifier = ">=0.9.6" }] lint = [{ name = "ruff", specifier = ">=0.9.6" }]
repl = [ repl = [
@ -1147,23 +1231,23 @@ repl = [
{ name = "pexpect", specifier = ">=4.9.0" }, { name = "pexpect", specifier = ">=4.9.0" },
{ name = "prompt-toolkit", specifier = "==3.0.40" }, { name = "prompt-toolkit", specifier = "==3.0.40" },
{ name = "pyperclip", specifier = ">=1.9.0" }, { name = "pyperclip", specifier = ">=1.9.0" },
{ name = "xonsh", git = "https://github.com/xonsh/xonsh.git?branch=main" }, { name = "xonsh", specifier = ">=0.22.2" },
] ]
testing = [{ name = "pytest" }] testing = [{ name = "pytest" }]
uis = [ uis = [
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" }, { name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
{ name = "pyqtgraph", git = "https://github.com/pikers/pyqtgraph.git" }, { name = "pyqtgraph", specifier = ">=0.14.0" },
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" }, { name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" }, { name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
] ]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.5.1" version = "4.6.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } sdist = { url = "https://files.pythonhosted.org/packages/20/e5/474d0a8508029286b905622e6929470fb84337cfa08f9d09fbb624515249/platformdirs-4.6.0.tar.gz", hash = "sha256:4a13c2db1071e5846c3b3e04e5b095c0de36b2a24be9a3bc0145ca66fce4e328", size = 23433, upload-time = "2026-02-12T14:36:21.288Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, { url = "https://files.pythonhosted.org/packages/da/10/1b0dcf51427326f70e50d98df21b18c228117a743a1fc515a42f8dc7d342/platformdirs-4.6.0-py3-none-any.whl", hash = "sha256:dd7f808d828e1764a22ebff09e60f175ee3c41876606a6132a688d809c7c9c73", size = 19549, upload-time = "2026-02-12T14:36:19.743Z" },
] ]
[[package]] [[package]]
@ -1446,6 +1530,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
] ]
[[package]]
name = "pyluach"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/11/42568c1568a75f8803c59f26d29af01a0890352b7a8e03d41ecda8bfb5dd/pyluach-2.3.0.tar.gz", hash = "sha256:ec6e30669d1df50c9ca160486da44a8195bb4c7a5d3d533990d0c5b03accd281", size = 26910, upload-time = "2025-09-09T20:24:39.651Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/c8/f96208ade3ca4c23b372497d0788bcf0f2e0ff4310e5ee693366bc33fdf0/pyluach-2.3.0-py3-none-any.whl", hash = "sha256:4497b731aef59508b079dbf5f00bc5bf4329ac45090a6cd37b5a83756f0e69ab", size = 25914, upload-time = "2025-09-09T20:24:37.831Z" },
]
[[package]] [[package]]
name = "pyperclip" name = "pyperclip"
version = "1.11.0" version = "1.11.0"
@ -1510,11 +1603,15 @@ wheels = [
[[package]] [[package]]
name = "pyqtgraph" name = "pyqtgraph"
version = "0.12.3" version = "0.14.0"
source = { git = "https://github.com/pikers/pyqtgraph.git#373f9561ea8ec4fef9b4e8bdcdd4bbf372dd6512" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama" },
{ name = "numpy" }, { name = "numpy" },
] ]
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" },
]
[[package]] [[package]]
name = "pyreadline3" name = "pyreadline3"
@ -1865,10 +1962,19 @@ name = "tomlkit"
version = "0.11.8" version = "0.11.8"
source = { git = "https://github.com/pikers/tomlkit.git?branch=piker_pin#8e0239a766e96739da700cd87cc00b48dbe7451f" } source = { git = "https://github.com/pikers/tomlkit.git?branch=piker_pin#8e0239a766e96739da700cd87cc00b48dbe7451f" }
[[package]]
name = "toolz"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" },
]
[[package]] [[package]]
name = "tractor" name = "tractor"
version = "0.1.0a6.dev0" version = "0.1.0a6.dev0"
source = { editable = "../tractor" } source = { git = "https://github.com/goodboy/tractor.git?branch=piker_pin#36307c59175a1d04fecc77ef2c28f5c943b5f3d1" }
dependencies = [ dependencies = [
{ name = "bidict" }, { name = "bidict" },
{ name = "cffi" }, { name = "cffi" },
@ -1881,48 +1987,6 @@ dependencies = [
{ name = "wrapt" }, { name = "wrapt" },
] ]
[package.metadata]
requires-dist = [
{ name = "bidict", specifier = ">=0.23.1" },
{ name = "cffi", specifier = ">=1.17.1" },
{ name = "colorlog", specifier = ">=6.8.2,<7" },
{ name = "msgspec", specifier = ">=0.19.0" },
{ name = "pdbp", specifier = ">=1.8.2,<2" },
{ name = "platformdirs", specifier = ">=4.4.0" },
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
{ name = "trio", specifier = ">0.27" },
{ name = "wrapt", specifier = ">=1.16.0,<2" },
]
[package.metadata.requires-dev]
dev = [
{ name = "greenback", specifier = ">=1.2.1,<2" },
{ name = "pexpect", specifier = ">=4.9.0,<5" },
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "pyperclip", specifier = ">=1.9.0" },
{ 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" },
]
devx = [
{ name = "greenback", specifier = ">=1.2.1,<2" },
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
{ name = "typing-extensions", specifier = ">=4.14.1" },
]
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
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" },
]
testing = [
{ name = "pexpect", specifier = ">=4.9.0,<5" },
{ name = "pytest", specifier = ">=8.3.5" },
]
[[package]] [[package]]
name = "tricycle" name = "tricycle"
version = "0.4.1" version = "0.4.1"
@ -2162,8 +2226,14 @@ wheels = [
[[package]] [[package]]
name = "xonsh" name = "xonsh"
version = "0.22.1" version = "0.22.4"
source = { git = "https://github.com/xonsh/xonsh.git?branch=main#336658ff0919f8d7bb96d581136d37d470a8fe99" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/48/df/1fc9ed62b3d7c14612e1713e9eb7bd41d54f6ad1028a8fbb6b7cddebc345/xonsh-0.22.4.tar.gz", hash = "sha256:6be346563fec2db75778ba5d2caee155525e634e99d9cc8cc347626025c0b3fa", size = 826665, upload-time = "2026-02-17T07:53:39.424Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/00/7cbc0c1fb64365a0a317c54ce3a151c9644eea5a509d9cbaae61c9fd1426/xonsh-0.22.4-py311-none-any.whl", hash = "sha256:38b29b29fa85aa756462d9d9bbcaa1d85478c2108da3de6cc590a69a4bcd1a01", size = 654375, upload-time = "2026-02-17T07:53:37.702Z" },
{ url = "https://files.pythonhosted.org/packages/2e/c2/3dd498dc28d8f89cdd52e39950c5e591499ae423f61694c0bb4d03ed1d82/xonsh-0.22.4-py312-none-any.whl", hash = "sha256:4e538fac9f4c3d866ddbdeca068f0c0515469c997ed58d3bfee963878c6df5a5", size = 654300, upload-time = "2026-02-17T07:53:35.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/7d/1f9c7147518e9f03f6ce081b5bfc4f1aceb6ec5caba849024d005e41d3be/xonsh-0.22.4-py313-none-any.whl", hash = "sha256:cc5fabf0ad0c56a2a11bed1e6a43c4ec6416a5b30f24f126b8e768547c3793e2", size = 654818, upload-time = "2026-02-17T07:53:33.477Z" },
]
[[package]] [[package]]
name = "yapic-json" name = "yapic-json"