Compare commits
74 Commits
main
...
wkt/fix_br
| Author | SHA1 | Date |
|---|---|---|
|
|
4af885d90f | |
|
|
ddac3553a3 | |
|
|
b691422b26 | |
|
|
12b0d66e85 | |
|
|
b73300c820 | |
|
|
0404e4230e | |
|
|
ba32f286b9 | |
|
|
b0766764f0 | |
|
|
456c6a5567 | |
|
|
f084e89991 | |
|
|
f15f8178a3 | |
|
|
eee19de090 | |
|
|
59d5d9a66d | |
|
|
6418121923 | |
|
|
119d2c0495 | |
|
|
bc6e18d7b4 | |
|
|
4485f2b9ce | |
|
|
75cefe10f1 | |
|
|
b547a33da4 | |
|
|
0df3943f3c | |
|
|
5466acb764 | |
|
|
604e5fcf9c | |
|
|
887e1ea6b7 | |
|
|
54da297304 | |
|
|
9f7c38a37b | |
|
|
8299c65818 | |
|
|
170c95da28 | |
|
|
e1cd3fd955 | |
|
|
8cefc1bdf8 | |
|
|
709269fcf7 | |
|
|
5387538ba9 | |
|
|
d4dc8854e0 | |
|
|
a0586f8219 | |
|
|
923b4de296 | |
|
|
ebc5bbd42b | |
|
|
4cfe0a9dac | |
|
|
f210a478c6 | |
|
|
79be47635f | |
|
|
26aaec2e0c | |
|
|
4bfe8d99b3 | |
|
|
611597ee18 | |
|
|
94ac2ee82a | |
|
|
bf43036fe1 | |
|
|
8424c368f6 | |
|
|
84c742a885 | |
|
|
7f8a198c98 | |
|
|
de82df727d | |
|
|
5df511f7d2 | |
|
|
f4c4f1e2d5 | |
|
|
24f2712d58 | |
|
|
f9979956a0 | |
|
|
31859e9d03 | |
|
|
e30957b62f | |
|
|
b87710e999 | |
|
|
392ea6162a | |
|
|
d98c1706d6 | |
|
|
88d309a613 | |
|
|
87707e4239 | |
|
|
073176a4c2 | |
|
|
8c08ddd38c | |
|
|
b4c905b592 | |
|
|
494bc4ce85 | |
|
|
ce8ad59f7c | |
|
|
2e42d7e1c4 | |
|
|
17e7232d12 | |
|
|
88f66baeed | |
|
|
3f77180b63 | |
|
|
ccc5a745de | |
|
|
5b1c80a8a5 | |
|
|
e600d61eef | |
|
|
5c7f00a3b8 | |
|
|
3b0c27ec44 | |
|
|
574fb80d79 | |
|
|
cdb0e8411a |
|
|
@ -1,291 +0,0 @@
|
|||
# Test Harness Reference: piker
|
||||
|
||||
This repository-local file supplements the canonical `/run-tests` skill.
|
||||
Keep shared execution, worktree, failure-inspection, and cleanup policy in the
|
||||
canonical `SKILL.md`; keep Piker commands, paths, fixtures, and known outcomes
|
||||
here.
|
||||
|
||||
## Project And Environment
|
||||
|
||||
- Project/import: `piker`
|
||||
- Test root: `tests/`
|
||||
- Supported Python: `>=3.12,<3.14`
|
||||
- Preferred complete environment: worktree-local `py313` inside the current
|
||||
`nix develop` shell
|
||||
- The flake shell pins CPython 3.13 and sets
|
||||
`UV_PROJECT_ENVIRONMENT=py313`.
|
||||
- Verify the interpreter, package resolution, and dependency import before
|
||||
running tests. A bare `py313` may lack the Qt binding supplied by Nix.
|
||||
|
||||
Use an already-provisioned `py313` only when `import piker` succeeds:
|
||||
|
||||
```text
|
||||
py313/bin/python -m pytest -p no:xonsh
|
||||
```
|
||||
|
||||
If direct environment paths are unavailable, an existing uv environment can
|
||||
be used without changing it, subject to the same import check:
|
||||
|
||||
```text
|
||||
UV_PROJECT_ENVIRONMENT=py313 uv run --frozen --no-sync python -m pytest -p no:xonsh
|
||||
```
|
||||
|
||||
Ask before running provisioning commands such as:
|
||||
|
||||
```text
|
||||
UV_PROJECT_ENVIRONMENT=py313 uv sync --dev --all-extras --no-group lint
|
||||
nix develop
|
||||
nix-shell default.nix
|
||||
```
|
||||
|
||||
`nix develop` is the current Wayland/Qt 6 shell. `default.nix` is the current
|
||||
X11 shell. Do not use `develop.nix` for current testing; it retains the old
|
||||
Python 3.11, Poetry, and Qt 5 stack.
|
||||
|
||||
The current root-checkout `py313` resolves `piker` locally but fails
|
||||
`import piker` outside Nix because PyQtGraph cannot import PyQt or PySide. Do
|
||||
not treat that environment as test-ready and do not enter `nix develop`
|
||||
without approval: its shell hook may recreate and sync `py313`.
|
||||
|
||||
Plain `uv sync` does not include the testing group. The `dbs` dependency group
|
||||
is also absent from normal dev-shell provisioning.
|
||||
|
||||
## Commands
|
||||
|
||||
Base command in the preferred environment:
|
||||
|
||||
```text
|
||||
py313/bin/python -m pytest -p no:xonsh
|
||||
```
|
||||
|
||||
The explicit `-p no:xonsh` is required. The tracked comments-only
|
||||
`pytest.ini` takes precedence over `pyproject.toml`, so the intended
|
||||
`addopts = "-p no:xonsh"` and `testpaths = ["tests"]` are inactive.
|
||||
Always pass a test path or node ID explicitly.
|
||||
|
||||
Package-resolution check that does not import Piker's dependencies:
|
||||
|
||||
```text
|
||||
py313/bin/python -c 'import importlib.util, pathlib, sys; root = pathlib.Path.cwd().resolve(); spec = importlib.util.find_spec("piker"); mod = pathlib.Path(spec.origin).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)'
|
||||
```
|
||||
|
||||
Dependency import check, required before collection or execution:
|
||||
|
||||
```text
|
||||
py313/bin/python -c 'import pathlib, piker, sys; root = pathlib.Path.cwd().resolve(); mod = pathlib.Path(piker.__file__).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)'
|
||||
```
|
||||
|
||||
Safe core collection check:
|
||||
|
||||
```text
|
||||
py313/bin/python -m pytest -p no:xonsh -q --collect-only tests/test_watchlists.py tests/test_accounting.py tests/test_services.py tests/test_ems.py tests/test_feeds.py tests/test_cli.py
|
||||
```
|
||||
|
||||
Default first-pass flags are `-q -x --tb=short --no-header` unless the user
|
||||
requests otherwise. For actor-heavy tests, use one file or node per process
|
||||
with an outer timeout:
|
||||
|
||||
```text
|
||||
timeout -k 5 300 py313/bin/python -m pytest -p no:xonsh -q <one-file-or-node>
|
||||
```
|
||||
|
||||
If an actor-heavy command exits `124` or `143`, retry that exact command once
|
||||
and report both attempts. Never convert a retry pass into an unconditional
|
||||
pass, and do not retry assertion, import, collection, or configuration
|
||||
failures.
|
||||
|
||||
## Scope And Path Resolution
|
||||
|
||||
Resolve bare test filenames beneath `tests/`. Preserve complete node IDs and
|
||||
apply `-k` only within explicitly selected paths. There is no marker-based
|
||||
offline/full-suite split, so never use `pytest tests` as a deterministic
|
||||
default.
|
||||
|
||||
Deterministic or local first-pass targets:
|
||||
|
||||
- `tests/test_watchlists.py`
|
||||
- `tests/test_storage_audit.py`
|
||||
- `tests/test_store_cli.py`
|
||||
- `tests/test_backfill_audit_snippet.py`
|
||||
- `tests/test_ib_history.py`
|
||||
- `tests/test_ib_method_proxy.py`
|
||||
- `tests/test_history_backfill.py`
|
||||
- `tests/test_ldshm.py`
|
||||
- `tests/test_accounting.py::test_account_file_default_empty`
|
||||
- `tests/test_services.py::test_runtime_boot`
|
||||
- `tests/test_services.py::test_datad_spawn`
|
||||
- `tests/test_ems.py::test_ems_err_on_bad_broker`
|
||||
|
||||
Require explicit authorization before running:
|
||||
|
||||
- `tests/test_feeds.py` - live Binance/Kraken feeds;
|
||||
- `tests/test_services.py::test_ensure_datafeed_actors` - live Kraken feed;
|
||||
- `tests/test_services.py::test_ensure_ems_in_paper_actors` - paper EMS with
|
||||
live Kraken symbology/feed access;
|
||||
- `tests/test_ems.py::test_multi_fill_positions` - live backend setup and
|
||||
persisted state;
|
||||
- `tests/test_accounting.py::test_paper_ledger_position_calcs` - tracked
|
||||
fixtures plus possible live symcache generation;
|
||||
- `tests/test_accounting.py::test_ib_account_with_duplicated_mktids` - active
|
||||
broker/account configuration and state writes;
|
||||
- `tests/test_dpi_font.py` - Qt/UI import and user-config side effects;
|
||||
- `tests/test_docker_services.py` - optional dependencies and containers;
|
||||
- `tests/test_questrade.py` - obsolete credentialed imports.
|
||||
|
||||
`tests/test_cli.py` is currently hard-skipped. It is not an active CLI
|
||||
regression gate.
|
||||
|
||||
Never execute `piker store anal` or `piker store shm --write-parquet`
|
||||
as tests. They are mutating or interactive operational commands.
|
||||
|
||||
## Project-Specific Flags And Backend Matrix
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `--ll LEVEL` | Piker log level |
|
||||
| `--confdir PATH` | Override `piker.config._config_dir` |
|
||||
| `--spawn-backend trio|mp_spawn|mp_forkserver` | Tractor process backend |
|
||||
| `--tpt-proto PROTO` | Tractor transport; one protocol per session |
|
||||
| `--tpdb` / `--debug-mode` | Tractor plugin debug mode |
|
||||
| `--pdb` | Standard pytest debugger |
|
||||
| `-s` | No capture; required with `--pdb` and `open_test_pikerd` |
|
||||
|
||||
Do not invent `network`, `offline`, `docker`, `gui`, or `broker` markers; none
|
||||
currently exists. `CI=1` is not an offline selector: feed tests still leave a
|
||||
live Kraken case enabled. Current Piker tests exercise TCP; do not assume the
|
||||
whole suite supports UDS merely because the Tractor plugin exposes it.
|
||||
|
||||
## Fixture Invariants
|
||||
|
||||
- The session `confdir` fixture does nothing unless `--confdir` is passed.
|
||||
Its claimed `tests/data` fallback is not implemented and that directory is
|
||||
absent.
|
||||
- Function-scoped `tmpconfdir` changes process-global config state and does
|
||||
not restore the previous path. Use separate pytest processes when
|
||||
diagnosing state leakage.
|
||||
- `open_test_pikerd` passes the temporary config path to child actors through
|
||||
`tractor_runtime_overrides`.
|
||||
- `tests/_inputs/trades_binance_paper.toml` and
|
||||
`tests/_inputs/account.binance.paper.toml` are used in place. Accounting
|
||||
contexts can write them on exit. Inspect `git diff -- tests/_inputs` after
|
||||
any selected accounting case.
|
||||
- Importing `tests/test_dpi_font.py` constructs module-global font objects
|
||||
before fixtures can isolate config. If explicitly requested, isolate
|
||||
`XDG_CONFIG_HOME` before Python starts and use the proper Qt/Nix shell.
|
||||
- Piker and the installed Tractor pytest plugin do not provide a
|
||||
repository-local process or socket reaper. Never apply historical broad
|
||||
`pkill -f tractor._child` guidance automatically.
|
||||
- The function-scoped autouse `shm_leak_tracker` fixture wraps Tractor's
|
||||
current-process `SharedMemory` factory. It tracks only successful
|
||||
`create=True` calls, restores the pre-test `_known_tokens` cache, and
|
||||
unlinks exact surviving names before failing the leaking test. It never
|
||||
scans `/dev/shm` or unlinks attachments created by another process.
|
||||
|
||||
## Test Layout
|
||||
|
||||
```text
|
||||
tests/
|
||||
conftest.py Piker options, config fixtures, Tractor plugin
|
||||
_inputs/ tracked ledger/account fixtures
|
||||
test_accounting.py config, ledgers, accounts, and position math
|
||||
test_cli.py legacy CLI suite; hard-skipped
|
||||
test_docker_services.py container integrations; optional deps, skipped
|
||||
test_dpi_font.py Qt DPI/font behavior
|
||||
test_ems.py actor, EMS, and paper-position behavior
|
||||
test_feeds.py live Binance/Kraken feeds and shared memory
|
||||
test_ib_history.py deterministic IB history request formatting
|
||||
test_ib_method_proxy.py deterministic IB asyncio proxy routing
|
||||
test_history_backfill.py deterministic history/SHM orchestration
|
||||
test_ldshm.py SHM unpublished-slot guard
|
||||
test_questrade.py obsolete credentialed tests; skipped
|
||||
test_services.py pikerd/datad/feed/EMS actor lifecycle
|
||||
test_store_cli.py storage command help and diagnostics UX
|
||||
test_storage_audit.py read-only NativeDB audit and JSON CLI
|
||||
test_backfill_audit_snippet.py
|
||||
disposable xonsh qualification helpers
|
||||
test_watchlists.py deterministic watchlist JSON operations
|
||||
```
|
||||
|
||||
## Change-To-Test Mapping
|
||||
|
||||
| Changed area | Run first | Caveat |
|
||||
|---|---|---|
|
||||
| `piker/watchlists/` | `tests/test_watchlists.py` | CLI suite is skipped |
|
||||
| `piker/storage/_audit.py`, `piker/storage/cli.py` | `tests/test_storage_audit.py` | direct Typer app, no actor |
|
||||
| `piker/storage/cli.py` command UX | `tests/test_store_cli.py` | fake SHM/runtime, no mutation |
|
||||
| `snippets/nativedb_backfill_audit.xsh` | `tests/test_backfill_audit_snippet.py` | disposable paths only |
|
||||
| `piker/brokers/ib/api.py`, `feed.py` history | `tests/test_ib_history.py` | fake client, no network |
|
||||
| `piker/brokers/ib/api.py` method proxy | `tests/test_ib_method_proxy.py` | fake channel, no network |
|
||||
| `piker/tsp/_history.py` | `tests/test_history_backfill.py` | fake provider/storage/SHM |
|
||||
| `piker/storage/cli.py` SHM null-slot guard | `tests/test_ldshm.py` | synthetic timestamps, no SHM mutation |
|
||||
| `piker/config.py` | `test_account_file_default_empty` | root-network test has a known mismatch |
|
||||
| `piker/accounting/` | targeted accounting node | some cases use live/configured state |
|
||||
| `piker/ui/_style.py`, `piker/ui/qt.py` | `tests/test_dpi_font.py` | GUI/config-isolated opt-in |
|
||||
| `piker/service/_actor_runtime.py`, `_registry.py`, `_mngr.py` | `test_runtime_boot` | then `test_datad_spawn` |
|
||||
| `piker/service/`, `piker/data/_daemon.py` | `test_datad_spawn` | feed lifecycle cases are live |
|
||||
| `piker/data/feed.py`, `flows.py`, `_sharedmem.py`, `_sampling.py` | collect first | feed execution needs live permission |
|
||||
| `piker/clearing/` | `test_ems_err_on_bad_broker` | multi-fill case is live/persisted |
|
||||
| `piker/brokers/binance/`, `kraken/` | selected feed/accounting node | live network |
|
||||
| `piker/brokers/ib/` | duplicated-market-ID node | controlled account config required |
|
||||
| Docker/service adapters | `tests/test_docker_services.py` | optional deps and containers |
|
||||
| project, lock, or Nix files | import check and safe collection | full collection is not safe by default |
|
||||
|
||||
Prefer deterministic filesystem/config tests, then local actor-runtime nodes,
|
||||
then explicitly approved live broker, GUI, or container coverage.
|
||||
|
||||
## Quick Checks
|
||||
|
||||
```text
|
||||
py313/bin/python -c 'import importlib.util, pathlib, sys; root = pathlib.Path.cwd().resolve(); spec = importlib.util.find_spec("piker"); mod = pathlib.Path(spec.origin).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)'
|
||||
py313/bin/python -c 'import pathlib, piker, sys; root = pathlib.Path.cwd().resolve(); mod = pathlib.Path(piker.__file__).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)'
|
||||
py313/bin/python -m pytest -p no:xonsh -q tests/test_watchlists.py
|
||||
py313/bin/python -m pytest -p no:xonsh -q tests/test_accounting.py::test_account_file_default_empty
|
||||
timeout -k 5 300 py313/bin/python -m pytest -p no:xonsh -q tests/test_services.py::test_runtime_boot
|
||||
timeout -k 5 300 py313/bin/python -m pytest -p no:xonsh -q tests/test_services.py::test_datad_spawn
|
||||
timeout -k 5 300 py313/bin/python -m pytest -p no:xonsh -q tests/test_ems.py::test_ems_err_on_bad_broker
|
||||
```
|
||||
|
||||
## Known Outcomes
|
||||
|
||||
- The current root-checkout `py313` fails `import piker` outside the Nix shell
|
||||
with `ImportError: PyQtGraph requires one of PyQt5, PyQt6, PySide2 or
|
||||
PySide6`. This is an incomplete environment, not an application regression.
|
||||
- `tests/test_accounting.py::test_root_conf_networking_section` currently
|
||||
expects `network.tsdb`, which is absent from the tracked config template.
|
||||
Match the current `KeyError: 'tsdb'` before classifying it as the known
|
||||
repository mismatch.
|
||||
- `tests/test_docker_services.py` is marked skipped but imports
|
||||
`elasticsearch` first. Without the `dbs` group it fails collection rather
|
||||
than skipping.
|
||||
- `tests/test_questrade.py` is marked skipped but imports undeclared `asks`
|
||||
through the legacy broker module before marks apply.
|
||||
- `test_open_orders_reloaded` and `test_dark_order_clearing` in
|
||||
`tests/test_ems.py` contain only ellipsis bodies. A pass does not verify the
|
||||
named behavior.
|
||||
- Treat `.pytest_cache` feed IDs using old FQME forms as stale historical
|
||||
cache, not current known failures.
|
||||
|
||||
Do not classify every `TooSlowError`, timeout, or child survivor as the known
|
||||
Tractor teardown wedge. Match the selected node and the documented
|
||||
second-runtime/lingering-child signature.
|
||||
|
||||
## Tractor Runtime Notes
|
||||
|
||||
The suite loads `tractor._testing.pytest`. Defaults are the `trio` spawn
|
||||
backend and TCP transport. Registry addresses are normally session-unique,
|
||||
except `tests/test_services.py::test_runtime_boot`, which binds
|
||||
`127.0.0.1:6666`. Check that fixed port only for that node; ordinary tests do
|
||||
not require Piker's production `127.0.0.1:6116` registry address.
|
||||
|
||||
Current repository concurrency notes document an intermittent second
|
||||
in-process `pikerd` boot wedge with a lingering broker child and unread parent
|
||||
IPC bytes. Use the outer timeout for actor-heavy nodes. Retry an exact command
|
||||
once only after status `124` or `143`, and report both attempts.
|
||||
|
||||
Ordinary actor tests support normal capture. Diagnose a capture-dependent hang
|
||||
by retrying only the exact node with `-s`. Standard `--pdb` with
|
||||
`open_test_pikerd` requires `-s`; Tractor's `--tpdb` is a separate option.
|
||||
|
||||
After abnormal exit, inspect only descendants, sockets, and shared-memory
|
||||
objects attributable to the exact pytest session. Ask before signaling or
|
||||
unlinking anything.
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"version": 1, "backend": "neutral"}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(chmod:*)",
|
||||
"Bash(/tmp/piker_commits.txt)",
|
||||
"Bash(python:*)",
|
||||
"Bash(ls:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
name: commit-msg
|
||||
description: >
|
||||
Generate piker-style git commit messages from
|
||||
staged changes or prompt input, following the
|
||||
style guide learned from 500 repo commits.
|
||||
argument-hint: "[optional-scope-or-description]"
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Bash(git *), Read, Grep, Glob, Write
|
||||
---
|
||||
|
||||
## Current staged changes
|
||||
!`git diff --staged --stat`
|
||||
|
||||
## Recent commit style reference
|
||||
!`git log --oneline -10`
|
||||
|
||||
# Piker Git Commit Message Generator
|
||||
|
||||
Generate a commit message from the staged diff above
|
||||
following the piker project's conventions (learned from
|
||||
analyzing 500 repo commits).
|
||||
|
||||
If `$ARGUMENTS` is provided, use it as scope or
|
||||
description context for the commit message.
|
||||
|
||||
For the full style guide with verb frequencies,
|
||||
section markers, abbreviations, piker-specific terms,
|
||||
and examples, see
|
||||
[style-guide-reference.md](./style-guide-reference.md).
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Subject**: ~50 chars, present tense verb, use
|
||||
backticks for code refs
|
||||
- **Body**: only for complex/multi-file changes,
|
||||
67 char line max
|
||||
- **Section markers**: Also, / Deats, / Other,
|
||||
- **Bullets**: use `-` style
|
||||
- **Tone**: technical but casual (piker style)
|
||||
|
||||
## Claude-code Footer
|
||||
|
||||
When the written **patch** was assisted by
|
||||
claude-code, include:
|
||||
|
||||
```
|
||||
(this patch was generated in some part by [`claude-code`][claude-code-gh])
|
||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
||||
```
|
||||
|
||||
When only the **commit msg** was written by
|
||||
claude-code (human wrote the patch), use:
|
||||
```
|
||||
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
|
||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
||||
```
|
||||
|
||||
## Output Instructions
|
||||
|
||||
When generating a commit message:
|
||||
|
||||
1. Analyze the staged diff (injected above via
|
||||
dynamic context) to understand all changes.
|
||||
2. If `$ARGUMENTS` provides a scope (e.g.,
|
||||
`.ib.feed`) or description, incorporate it into
|
||||
the subject line.
|
||||
3. Write the subject line following verb + backtick
|
||||
conventions from the
|
||||
[style guide](./style-guide-reference.md).
|
||||
4. Add body only for multi-file or complex changes.
|
||||
5. Write the message to a file in the repo's
|
||||
`.claude/` subdir with filename format:
|
||||
`<timestamp>_<first-7-chars-of-last-commit-hash>_commit_msg.md`
|
||||
where `<timestamp>` is from `date --iso-8601=seconds`.
|
||||
Also write a copy to
|
||||
`.claude/git_commit_msg_LATEST.md`
|
||||
(overwrite if exists).
|
||||
|
||||
---
|
||||
|
||||
**Analysis date:** 2026-01-27
|
||||
**Commits analyzed:** 500 from piker repository
|
||||
**Maintained by:** Tyler Goodlet
|
||||
|
|
@ -150,18 +150,21 @@ Common in piker commits (33.0% use colons):
|
|||
- File:line references not used (0 occurrences)
|
||||
- No WIP commits in analyzed set
|
||||
|
||||
### Coding-harness Footer
|
||||
When a coding harness assisted with the written patch,
|
||||
identify the active harness, model, and provider:
|
||||
### Claude-code Footer
|
||||
When the written **patch** was assisted by claude-code,
|
||||
include:
|
||||
|
||||
```
|
||||
(this patch was generated in some part by `<harness>` using `<model>` (`<provider>`))
|
||||
(this patch was generated in some part by [`claude-code`][claude-code-gh])
|
||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
||||
```
|
||||
|
||||
When it generated only the commit message, use:
|
||||
When only the **commit msg** was written by claude-code
|
||||
(human wrote the patch), use:
|
||||
|
||||
```
|
||||
(this commit msg was generated in some part by `<harness>` using `<model>` (`<provider>`))
|
||||
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
|
||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
||||
```
|
||||
|
||||
## Piker-Specific Terms
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
---
|
||||
name: piker-conc-expert
|
||||
description: >
|
||||
Distributed-runtime and structured-concurrency
|
||||
expertise for piker's `tractor` actor-tree. Apply
|
||||
when working on daemon/service architecture, actor
|
||||
spawning/discovery, cross-actor RPC (ctx/stream
|
||||
eps), `to_asyncio` integration, cancellation
|
||||
semantics, or debugging hangs/wedges/skews in the
|
||||
actor system.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Piker Concurrency & Runtime Expertise
|
||||
|
||||
The distilled mental model for piker's distributed
|
||||
runtime: a `trio`-structured actor tree supervised by
|
||||
`tractor` (pinned to git main) where every long-lived
|
||||
subsystem is a named daemon-actor talking over
|
||||
ctx/stream IPC.
|
||||
|
||||
## Actor tree & daemon taxonomy
|
||||
|
||||
```
|
||||
pikerd root supervisor + registry
|
||||
├── datad.<broker> feed bus, shm writers, tsp
|
||||
│ history, symbol search
|
||||
├── brokerd.<broker> live order-ctl ONLY; lazily
|
||||
│ spawned by emsd, credentialed
|
||||
├── emsd dark-clearing + order routing
|
||||
│ └── paperboi.<broker> sim-clearing (paper mode)
|
||||
└── samplerd singleton OHLC clock/increment
|
||||
```
|
||||
|
||||
Key invariants:
|
||||
- `datad` hosts all `piker.data.validate._eps['datad']`
|
||||
eps; `brokerd` only the `['brokerd']` (order-ctl)
|
||||
ones. The `_eps` table in `piker/data/validate.py`
|
||||
is the authoritative contract; `get_eps(mod, kind)`
|
||||
introspects a backend's support.
|
||||
- `brokerd.<broker>` is booted in EXACTLY one place:
|
||||
`open_brokerd_dialog()` in `piker/clearing/_ems.py`
|
||||
(with a `portal:` override for the `piker ledger`
|
||||
ad-hoc actor). Chart-only + paper sessions run with
|
||||
ZERO brokerd procs. Never add a data-path spawn!
|
||||
- backends declare per-daemon-kind submods via
|
||||
`_datad_mods`/`_brokerd_mods` in their
|
||||
`__init__.py` (fallback: `__enable_modules__`).
|
||||
|
||||
## Daemon lifecycle conventions
|
||||
|
||||
Every daemon-kind follows the same trio of fns (see
|
||||
`piker/brokers/_daemon.py` + `piker/data/_daemon.py`
|
||||
as the canonical pair):
|
||||
|
||||
- `_setup_persistent_<kind>()`: a `@tractor.context`
|
||||
"lifetime fixture" run via
|
||||
`Services.start_service_task()`; does console-log
|
||||
setup ONCE for the actor, allocs any actor-global
|
||||
state (eg. datad's `_FeedsBus`), then
|
||||
`await ctx.started()` + `trio.sleep_forever()`.
|
||||
- `<kind>_init()`: builds `enable_modules` + actor
|
||||
name `f'<kind>.{brokername}'` and copies backend
|
||||
`_spawn_kwargs` (CRITICAL: `ib` needs
|
||||
`infect_asyncio=True` in EVERY daemon-kind).
|
||||
- `spawn_<kind>()` + `maybe_spawn_<kind>()`: thin
|
||||
wrappers over `Services.actor_n.start_actor()` and
|
||||
`piker.service.maybe_spawn_daemon()` (registry
|
||||
find-or-spawn w/ per-service-name locking).
|
||||
|
||||
Caps-sec model: `enable_modules` gates RPC entry ONLY
|
||||
— python imports are unrestricted in-proc. Keep each
|
||||
daemon's enable set minimal; the (credentialed)
|
||||
`brokerd` must never RPC-enable `piker.data.*` feed
|
||||
mods.
|
||||
|
||||
## Actor-local state: the #1 split hazard
|
||||
|
||||
Module-globals and instance caches are PER-ACTOR.
|
||||
Anything that "just worked" because two subsystems
|
||||
shared a process will break when they're split into
|
||||
sibling actors. Canonical example: `ib`'s
|
||||
`Client._contracts` was warmed by feed-side
|
||||
`get_mkt_info()` in-proc; post datad/brokerd-split
|
||||
the trading actor must warm it itself (eagerly at
|
||||
`open_trade_dialog()` startup for open pps/orders +
|
||||
lazily per order request via
|
||||
`symbols.cache_contract()`).
|
||||
|
||||
When moving code across actor boundaries ALWAYS audit:
|
||||
- module-global registries (`feed._bus`,
|
||||
`_accounts2clients`, `_client_cache`, ..)
|
||||
- `@async_lifo_cache`/`maybe_open_context` caches
|
||||
(NOTE: `async_lifo_cache` keys on POSITIONAL args
|
||||
only; a cache-hit SKIPS the fn body and thus any
|
||||
side-effect writes!)
|
||||
- logging handler placement (see gotchas.md)
|
||||
|
||||
## tractor primitives as used here
|
||||
|
||||
- `@tractor.context` eps: `await ctx.started(val)`
|
||||
unblocks the caller w/ `val`; long-lived eps then
|
||||
`ctx.open_stream()` or `sleep_forever()`.
|
||||
- discovery: `tractor.find_actor()` via
|
||||
`piker.service.find_service()`;
|
||||
`wait_for_actor(name, registry_addr=...)`;
|
||||
`query_actor(name, regaddr=...)` yields
|
||||
`(sockaddr, portal)`. Addrs are wrapped
|
||||
`tractor.discovery._addr.Address` types — use
|
||||
`wrap_address()` to normalize raw tuples and
|
||||
`.unwrap()` for comparisons.
|
||||
- runtime-vars: `_runtime_vars['piker_vars']` is
|
||||
inherited down the spawn tree; used eg. for
|
||||
`piker_test_dir` config isolation — read LAZILY at
|
||||
use-time, never at import time (subactors only get
|
||||
vars post runtime-boot).
|
||||
- cancellation semantics (modern tractor): a
|
||||
`ContextCancelled` whose `.canceller` is your own
|
||||
actor is ABSORBED (clean exit, nothing raised);
|
||||
single-exc groups collapse (`collapse_eg`) so eg.
|
||||
a KBI propagates bare. Exc attrs:
|
||||
`RemoteActorError.boxed_type` (not `.type`).
|
||||
|
||||
## `to_asyncio` (infect-asyncio) integration
|
||||
|
||||
For `ib` (and `deribit`) the backend client runs on
|
||||
an embedded `asyncio` loop via
|
||||
`tractor.to_asyncio.open_channel_from()` +
|
||||
`LinkedTaskChannel`.
|
||||
|
||||
Rules learned the hard way:
|
||||
- a shared req/resp channel MUST correlate responses
|
||||
to requests (see `MethodProxy._run_method()`'s
|
||||
`mid` protocol in `piker/brokers/ib/api.py`):
|
||||
caller cancellation (eg. `move_on_after` timeouts)
|
||||
otherwise orphans a response and silently skews
|
||||
every later result off-by-one.
|
||||
- the aio-side relay must catch + ship back ALL
|
||||
(non-cancel) exceptions as `{'exception': err}`
|
||||
resps; an escaping error kills the relay task ->
|
||||
channel -> proxy nursery -> the whole dialog,
|
||||
bypassing every caller-side guard.
|
||||
- `TrioTaskExited` ("child asyncio task is still
|
||||
running?") on teardown is a known wart family;
|
||||
prefer upstream `tractor` fixes over piker-side
|
||||
bandaids.
|
||||
|
||||
See [gotchas.md](gotchas.md) for the symptom->cause
|
||||
registry and [debug-recipes.md](debug-recipes.md) for
|
||||
forensics techniques.
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# Debug recipes: actor-system forensics
|
||||
|
||||
Field-tested techniques for diagnosing hangs, wedges
|
||||
and cross-actor state bugs WITHOUT a debugger attached
|
||||
(or when `py-spy` ain't installed).
|
||||
|
||||
## Wedged actor triage (no REPL)
|
||||
|
||||
1. find the tree:
|
||||
`ps -eo pid,etime,args | grep -E 'pytest|tractor._child'`
|
||||
— long-`etime` `tractor._child` procs w/ a stuck
|
||||
parent = wedge.
|
||||
2. kernel state:
|
||||
`cat /proc/<pid>/wchan` + `status | grep -E
|
||||
'State|Threads'` — `do_epoll_wait` + sleeping =
|
||||
idle event loop, NOT cpu-spin.
|
||||
3. **the money read** — socket queues:
|
||||
`ss -tnp | grep <pid>`
|
||||
- `Recv-Q > 0` on the parent-IPC conn = the actor
|
||||
STOPPED CONSUMING its msg loop (runtime bug),
|
||||
parent is waiting on it.
|
||||
- zero external (api/ws) conns = wedged before/
|
||||
without provider IO; don't blame the network.
|
||||
- `CLOSE-WAIT` lingerers = unclean peer teardown.
|
||||
4. cleanup: `pkill -f tractor._child` (NB: in
|
||||
compound shell cmds `pkill`'s exit code poisons
|
||||
`&&` chains — run it standalone).
|
||||
|
||||
## Hang-proof test gating
|
||||
|
||||
- per-suite, never combined (cross-suite session
|
||||
state interacts w/ the 2nd-boot wedge):
|
||||
`timeout -k 5 300 python -m pytest tests/<one>.py -q`
|
||||
- rc 124/143 = hang-kill -> retry ONCE before
|
||||
investigating.
|
||||
- isolate a flaky test w/ a 3x loop; ~50% hit-rate
|
||||
signatures match the known 2nd-boot wedge (see
|
||||
gotchas.md).
|
||||
|
||||
## Regression vs pre-existing attribution
|
||||
|
||||
When a failure appears mid-refactor:
|
||||
1. `git stash -u` (or checkout the file subset) and
|
||||
re-run the EXACT failing case at baseline.
|
||||
2. if baseline can't even run, selectively revert
|
||||
ONLY the suspect layer:
|
||||
`git diff <files> > /tmp/x.patch;
|
||||
git checkout <files>` -> test ->
|
||||
`git apply /tmp/x.patch`.
|
||||
3. flake-rate compare (3x runs) beats single-shot
|
||||
conclusions.
|
||||
|
||||
## Off-by-one / stale IPC resp detection
|
||||
|
||||
Mismatched query->result content in logs (resp
|
||||
payload obviously for a prior request) = shared
|
||||
req/resp channel w/o correlation + a cancelled
|
||||
caller. Grep the ep for `move_on_after`/`fail_after`
|
||||
around proxied calls. Fix = req-id (`mid`) tagging,
|
||||
never "just a lock" (cancellation still orphans).
|
||||
|
||||
## Logging-chain audits
|
||||
|
||||
When records double-print or go bare (see gotchas.md):
|
||||
|
||||
```python
|
||||
import logging
|
||||
l = logging.getLogger('piker.brokers.ib.broker')
|
||||
while l:
|
||||
print(l.name, l.level, l.handlers, l.propagate)
|
||||
l = l.parent
|
||||
```
|
||||
|
||||
Exactly ONE stderr handler should exist in the chain,
|
||||
attached by the actor's daemon fixture.
|
||||
|
||||
## Live actor-tree smoke (headless)
|
||||
|
||||
Boot against an ALT registry port so a user's running
|
||||
stack is untouched; script in a REAL file (tractor
|
||||
children re-exec `__main__` from path — stdin scripts
|
||||
crash w/ `FileNotFoundError: .../<stdin>`):
|
||||
|
||||
```python
|
||||
async with maybe_open_pikerd(
|
||||
registry_addrs=[('127.0.0.1', 6979)],
|
||||
):
|
||||
async with open_feed(['xbtusdt.kraken']) as feed:
|
||||
assert await check_for_service('datad.kraken')
|
||||
assert not await check_for_service(
|
||||
'brokerd.kraken'
|
||||
)
|
||||
```
|
||||
|
||||
## In-proc fail-fast unit checks
|
||||
|
||||
Spawn-path guards that raise BEFORE touching the
|
||||
runtime can be tested w/ a bare `trio.run()` (eg
|
||||
`spawn_brokerd('kucoin')` raising the datad-only
|
||||
error) — no pikerd needed.
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
# Known gotchas: symptom -> cause -> fix
|
||||
|
||||
A registry of distributed-runtime failure modes hit
|
||||
(and diagnosed) in the field; check here FIRST when a
|
||||
log/traceback matches.
|
||||
|
||||
## "Can not order ..., no qualified contract cached"
|
||||
|
||||
- **Symptom**: `RuntimeError` from
|
||||
`ib.api.Client.submit_limit()` w/ empty
|
||||
`Client._contracts` in `brokerd.ib`.
|
||||
- **Cause**: per-actor cache never warmed; feed-side
|
||||
qualification now lives in `datad.ib`.
|
||||
- **Fix(ed)**: eager warmup at `open_trade_dialog()`
|
||||
start + lazy per-order `get_mkt_info()` +
|
||||
`cache_contract()` (writes BOTH `mkt.bs_fqme` and
|
||||
`mkt.fqme` keys; different consumers read each!).
|
||||
|
||||
## Search returns results for the WRONG pattern
|
||||
|
||||
- **Symptom**: fqme search for 'gld' returns nvda
|
||||
results; next query returns the prior query's set.
|
||||
- **Cause**: `MethodProxy` channel off-by-one — a
|
||||
caller cancelled (search `move_on_after` timeout)
|
||||
after sending its request orphans the response;
|
||||
every later caller consumes the previous resp.
|
||||
- **Fix(ed)**: `mid` req-id correlation in
|
||||
`_run_method()` + relay; stale resps are dropped w/
|
||||
a "Dropping stale method-resp" warning. If that
|
||||
warning spams, some caller is being cancelled
|
||||
mid-call habitually — find + fix its timeout.
|
||||
|
||||
## One bad request crashes a whole dialog/actor
|
||||
|
||||
- **Symptom**: `TrioTaskExited` storm + nursery
|
||||
teardown after a single method error (eg ambiguous
|
||||
contract `AttributeError`).
|
||||
- **Cause**: exception escaped the aio-side relay
|
||||
loop (`open_aio_client_method_relay()`) killing
|
||||
channel + proxy nursery; caller-side `try/except`
|
||||
CANNOT catch it.
|
||||
- **Fix(ed)**: relay catches `Exception` -> ships
|
||||
`{'exception': err, 'mid': ...}` resp; order
|
||||
handler converts to EMS `BrokerdError` msgs.
|
||||
|
||||
## Ambiguous ib contracts -> `NoneType` attr errors
|
||||
|
||||
- **Symptom**: `'NoneType' object has no attribute
|
||||
'primaryExchange'` in `find_contracts()`.
|
||||
- **Cause**: `qualifyContractsAsync()` returns `None`
|
||||
entries for ambiguous (eg venue-less stonk fqme
|
||||
matching multiple listings: 'gld' -> ARCA/USD +
|
||||
VENTURE/CAD).
|
||||
- **Fix(ed)**: filter `None`s + raise descriptive
|
||||
`ValueError` ("use 'gld.arca.ib'").
|
||||
|
||||
## Double-printed log records (same task id, 2x)
|
||||
|
||||
- **Symptom**: every record from some subsys printed
|
||||
twice w/ identical task ids.
|
||||
- **Cause**: stderr handlers attached at TWO levels
|
||||
of one logger-propagation chain (eg daemon fixture
|
||||
on `piker.brokers.ib` + an ep calling
|
||||
`get_console_log(name=__name__)` on the child).
|
||||
tractor's handler-dedup only checks the SAME
|
||||
logger, not ancestors.
|
||||
- **Rule**: console handlers are attached ONCE per
|
||||
actor in the `_setup_persistent_*()` fixture; eps
|
||||
needing a different level use `log.setLevel()`
|
||||
ONLY, never `get_console_log()`.
|
||||
|
||||
## Bare/non-colorized log lines
|
||||
|
||||
- **Symptom**: records w/ no timestamp/actor prefix.
|
||||
- **Cause**: NO handler anywhere in the emitting
|
||||
logger's chain -> stdlib `logging.lastResort`. Post
|
||||
actor-splits, a daemon fixture may only cover its
|
||||
own subsys subtree (eg datad's `piker.data.*` but
|
||||
not the backend's `piker.brokers.<broker>.*`).
|
||||
- **Fix(ed)**: `_setup_persistent_datad()` enables
|
||||
BOTH `piker.data.<broker>` and
|
||||
`piker.brokers.<broker>` subtrees.
|
||||
|
||||
## 2nd in-proc runtime boot wedges (~50%)
|
||||
|
||||
- **Symptom**: test hangs when one test proc boots a
|
||||
2nd `pikerd` (eg `test_multi_fill_positions`'s
|
||||
persistence re-check); a zombie `*.{broker}` child
|
||||
lingers w/ unread bytes in its parent-IPC Recv-Q.
|
||||
- **Cause**: pre-existing `tractor`-main runtime
|
||||
teardown bug (confirmed independent of piker-layer
|
||||
changes via revert-testing 2026-06).
|
||||
- **Mitigation**: run suites per-file wrapped in
|
||||
`timeout -k 5 300 ...`; retry once on rc 124/143.
|
||||
Do NOT chase as a regression of unrelated changes.
|
||||
|
||||
## ib client-id collisions post-split
|
||||
|
||||
- **Symptom**: 2nd ib daemon burns the full
|
||||
conn-timeout retry cycle connecting to gw/tws.
|
||||
- **Cause**: `datad.ib` + `brokerd.ib` both default
|
||||
`client_id=6116` w/ linear `+i` retries.
|
||||
- **Fix(ed)**: role-based offsets in
|
||||
`load_aio_clients()`: datad +16, ad-hoc (test/cli)
|
||||
actors +32.
|
||||
|
||||
## `async_lifo_cache` skipped side-effects
|
||||
|
||||
- **Symptom**: a fn's cache-write side effect
|
||||
(eg `get_mkt_info()` -> `_contracts`) missing for
|
||||
a 2nd client/proxy.
|
||||
- **Cause**: cache keys on POSITIONAL args only; a
|
||||
hit skips the body entirely.
|
||||
- **Rule**: never rely on cached-fn side effects;
|
||||
perform required writes explicitly at the call
|
||||
site (eg `cache_contract()` after `get_mkt_info`).
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../.agents/skills/piker-profiling
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../.agents/skills/piker-slang
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../.agents/skills/pyqtgraph-optimization
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../.agents/skills/timeseries-optimization
|
||||
|
|
@ -107,9 +107,6 @@ ENV/
|
|||
.git/
|
||||
|
||||
# any commit-msg gen tmp files
|
||||
.claude/skills/commit-msg/msgs/
|
||||
.claude/skills/commit-msg/conf.toml
|
||||
.claude/git_commit_msg_LATEST.md
|
||||
.claude/*_commit_*.md
|
||||
.claude/*_commit*.toml
|
||||
|
||||
|
|
@ -133,362 +130,3 @@ gitea/
|
|||
|
||||
# LLM conversations that should remain private
|
||||
docs/conversations/
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:gish
|
||||
/.claude/skills/gish
|
||||
# END ai.skillz: direct:symlink:claude:gish
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:gish
|
||||
/.opencode/skills/gish
|
||||
# END ai.skillz: direct:symlink:opencode:gish
|
||||
|
||||
# BEGIN ai.skillz: runtime:open-wkt
|
||||
/wkts/
|
||||
# END ai.skillz: runtime:open-wkt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:open-wkt
|
||||
/.claude/skills/open-wkt
|
||||
# END ai.skillz: direct:symlink:claude:open-wkt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:open-wkt
|
||||
/.opencode/skills/open-wkt
|
||||
# END ai.skillz: direct:symlink:opencode:open-wkt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:run-tests
|
||||
/.claude/skills/run-tests/SKILL.md
|
||||
# END ai.skillz: direct:symlink:claude:run-tests
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:run-tests
|
||||
/.opencode/skills/run-tests/SKILL.md
|
||||
# END ai.skillz: direct:symlink:opencode:run-tests
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:close-wkt
|
||||
/.claude/skills/close-wkt
|
||||
# END ai.skillz: direct:symlink:claude:close-wkt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:close-wkt
|
||||
/.opencode/skills/close-wkt
|
||||
# END ai.skillz: direct:symlink:opencode:close-wkt
|
||||
|
||||
# BEGIN ai.skillz: runtime:code-review
|
||||
.ai/code-review/reports/
|
||||
# END ai.skillz: runtime:code-review
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:code-review
|
||||
/.claude/skills/code-review
|
||||
# END ai.skillz: direct:symlink:claude:code-review
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:code-review
|
||||
/.opencode/skills/code-review
|
||||
# END ai.skillz: direct:symlink:opencode:code-review
|
||||
|
||||
# BEGIN ai.skillz: runtime:code-review-changes
|
||||
.ai/state/review/
|
||||
.claude/review_context.md
|
||||
.claude/review_regression.md
|
||||
.claude/review_replies/
|
||||
# END ai.skillz: runtime:code-review-changes
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:code-review-changes
|
||||
/.claude/skills/code-review-changes
|
||||
# END ai.skillz: direct:symlink:claude:code-review-changes
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:code-review-changes
|
||||
/.opencode/skills/code-review-changes
|
||||
# END ai.skillz: direct:symlink:opencode:code-review-changes
|
||||
|
||||
# BEGIN ai.skillz: runtime:commit-msg
|
||||
.ai/state/commit-msg/
|
||||
.ai/state/migrations/
|
||||
.ai/commit-msg/conf.toml
|
||||
.claude/skills/commit-msg/msgs/
|
||||
.claude/git_commit_msg_LATEST.md
|
||||
# END ai.skillz: runtime:commit-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:commit-msg
|
||||
/.claude/skills/commit-msg/SKILL.md
|
||||
# END ai.skillz: direct:symlink:claude:commit-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:commit-msg
|
||||
/.opencode/skills/commit-msg/SKILL.md
|
||||
# END ai.skillz: direct:symlink:opencode:commit-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:commit-plan
|
||||
/.claude/skills/commit-plan
|
||||
# END ai.skillz: direct:symlink:claude:commit-plan
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:commit-plan
|
||||
/.opencode/skills/commit-plan
|
||||
# END ai.skillz: direct:symlink:opencode:commit-plan
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:dep-supersede-scan
|
||||
/.claude/skills/dep-supersede-scan
|
||||
# END ai.skillz: direct:symlink:claude:dep-supersede-scan
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:dep-supersede-scan
|
||||
/.opencode/skills/dep-supersede-scan
|
||||
# END ai.skillz: direct:symlink:opencode:dep-supersede-scan
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:resolve-conflicts
|
||||
/.claude/skills/resolve-conflicts
|
||||
# END ai.skillz: direct:symlink:claude:resolve-conflicts
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:resolve-conflicts
|
||||
/.opencode/skills/resolve-conflicts
|
||||
# END ai.skillz: direct:symlink:opencode:resolve-conflicts
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:git-mgmt
|
||||
/.claude/skills/git-mgmt
|
||||
# END ai.skillz: direct:symlink:claude:git-mgmt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:git-mgmt
|
||||
/.opencode/skills/git-mgmt
|
||||
# END ai.skillz: direct:symlink:opencode:git-mgmt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:harness-perf
|
||||
/.claude/skills/harness-perf
|
||||
# END ai.skillz: direct:symlink:claude:harness-perf
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:harness-perf
|
||||
/.opencode/skills/harness-perf
|
||||
# END ai.skillz: direct:symlink:opencode:harness-perf
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:inter-skill-review
|
||||
/.claude/skills/inter-skill-review
|
||||
# END ai.skillz: direct:symlink:claude:inter-skill-review
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:inter-skill-review
|
||||
/.opencode/skills/inter-skill-review
|
||||
# END ai.skillz: direct:symlink:opencode:inter-skill-review
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:opencode-cleaning
|
||||
/.claude/skills/opencode-cleaning
|
||||
# END ai.skillz: direct:symlink:claude:opencode-cleaning
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:opencode-cleaning
|
||||
/.opencode/skills/opencode-cleaning
|
||||
# END ai.skillz: direct:symlink:opencode:opencode-cleaning
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:plan-io
|
||||
/.claude/skills/plan-io
|
||||
# END ai.skillz: direct:symlink:claude:plan-io
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:plan-io
|
||||
/.opencode/skills/plan-io
|
||||
# END ai.skillz: direct:symlink:opencode:plan-io
|
||||
|
||||
# BEGIN ai.skillz: runtime:pr-msg
|
||||
.ai/state/pr-msg/
|
||||
.ai/pr-msg/conf.toml
|
||||
.claude/skills/pr-msg/msgs/
|
||||
.claude/skills/pr-msg/pr_msg_LATEST.md
|
||||
# END ai.skillz: runtime:pr-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:pr-msg
|
||||
/.claude/skills/pr-msg/SKILL.md
|
||||
/.claude/skills/pr-msg/references
|
||||
/.claude/skills/pr-msg/scripts
|
||||
# END ai.skillz: direct:symlink:claude:pr-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:pr-msg
|
||||
/.opencode/skills/pr-msg/SKILL.md
|
||||
/.opencode/skills/pr-msg/references
|
||||
/.opencode/skills/pr-msg/scripts
|
||||
# END ai.skillz: direct:symlink:opencode:pr-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:prompt-io
|
||||
/.claude/skills/prompt-io
|
||||
# END ai.skillz: direct:symlink:claude:prompt-io
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:prompt-io
|
||||
/.opencode/skills/prompt-io
|
||||
# END ai.skillz: direct:symlink:opencode:prompt-io
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:py-codestyle
|
||||
/.claude/skills/py-codestyle
|
||||
# END ai.skillz: direct:symlink:claude:py-codestyle
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:py-codestyle
|
||||
/.opencode/skills/py-codestyle
|
||||
# END ai.skillz: direct:symlink:opencode:py-codestyle
|
||||
|
||||
# BEGIN ai.skillz: runtime:taken-export
|
||||
.ai/taken/exports/
|
||||
# END ai.skillz: runtime:taken-export
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:taken-export
|
||||
/.claude/skills/taken-export
|
||||
# END ai.skillz: direct:symlink:claude:taken-export
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:taken-export
|
||||
/.opencode/skills/taken-export
|
||||
# END ai.skillz: direct:symlink:opencode:taken-export
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:yt-url-lookup
|
||||
/.claude/skills/yt-url-lookup
|
||||
# END ai.skillz: direct:symlink:claude:yt-url-lookup
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:yt-url-lookup
|
||||
/.opencode/skills/yt-url-lookup
|
||||
# END ai.skillz: direct:symlink:opencode:yt-url-lookup
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:run-tests
|
||||
/.opencode/commands/run-tests.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:run-tests
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:code-review
|
||||
/.opencode/commands/code-review.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:code-review
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:code-review-changes
|
||||
/.opencode/commands/code-review-changes.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:code-review-changes
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:commit-msg
|
||||
/.opencode/commands/commit-msg.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:commit-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:commit-plan
|
||||
/.opencode/commands/commit-plan.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:commit-plan
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:opencode-cleaning
|
||||
/.opencode/commands/opencode-cleaning.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:opencode-cleaning
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:pr-msg
|
||||
/.opencode/commands/pr-msg.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:pr-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:taken-export
|
||||
/.opencode/commands/taken-export.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:taken-export
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:claude:code-nav-refs
|
||||
/.claude/skills/code-nav-refs
|
||||
# END ai.skillz: direct:symlink:claude:code-nav-refs
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:code-nav-refs
|
||||
/.opencode/skills/code-nav-refs
|
||||
# END ai.skillz: direct:symlink:opencode:code-nav-refs
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:gish
|
||||
/.opencode/commands/gish.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:gish
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:resolve-conflicts
|
||||
/.opencode/commands/resolve-conflicts.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:resolve-conflicts
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:git-mgmt
|
||||
/.opencode/commands/git-mgmt.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:git-mgmt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:open-wkt
|
||||
/.opencode/commands/open-wkt.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:open-wkt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:close-wkt
|
||||
/.opencode/commands/close-wkt.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:close-wkt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:dep-supersede-scan
|
||||
/.opencode/commands/dep-supersede-scan.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:dep-supersede-scan
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:harness-perf
|
||||
/.opencode/commands/harness-perf.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:harness-perf
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:opencode:command:yt-url-lookup
|
||||
/.opencode/commands/yt-url-lookup.md
|
||||
# END ai.skillz: direct:symlink:opencode:command:yt-url-lookup
|
||||
# BEGIN ai.skillz: direct:symlink:agents:gish
|
||||
/.agents/skills/gish
|
||||
# END ai.skillz: direct:symlink:agents:gish
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:resolve-conflicts
|
||||
/.agents/skills/resolve-conflicts
|
||||
# END ai.skillz: direct:symlink:agents:resolve-conflicts
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:git-mgmt
|
||||
/.agents/skills/git-mgmt
|
||||
# END ai.skillz: direct:symlink:agents:git-mgmt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:open-wkt
|
||||
/.agents/skills/open-wkt
|
||||
# END ai.skillz: direct:symlink:agents:open-wkt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:run-tests
|
||||
/.agents/skills/run-tests
|
||||
# END ai.skillz: direct:symlink:agents:run-tests
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:close-wkt
|
||||
/.agents/skills/close-wkt
|
||||
# END ai.skillz: direct:symlink:agents:close-wkt
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:code-review
|
||||
/.agents/skills/code-review
|
||||
# END ai.skillz: direct:symlink:agents:code-review
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:code-nav-refs
|
||||
/.agents/skills/code-nav-refs
|
||||
# END ai.skillz: direct:symlink:agents:code-nav-refs
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:code-review-changes
|
||||
/.agents/skills/code-review-changes
|
||||
# END ai.skillz: direct:symlink:agents:code-review-changes
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:commit-msg
|
||||
/.agents/skills/commit-msg
|
||||
# END ai.skillz: direct:symlink:agents:commit-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:commit-plan
|
||||
/.agents/skills/commit-plan
|
||||
# END ai.skillz: direct:symlink:agents:commit-plan
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:dep-supersede-scan
|
||||
/.agents/skills/dep-supersede-scan
|
||||
# END ai.skillz: direct:symlink:agents:dep-supersede-scan
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:harness-perf
|
||||
/.agents/skills/harness-perf
|
||||
# END ai.skillz: direct:symlink:agents:harness-perf
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:inter-skill-review
|
||||
/.agents/skills/inter-skill-review
|
||||
# END ai.skillz: direct:symlink:agents:inter-skill-review
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:opencode-cleaning
|
||||
/.agents/skills/opencode-cleaning
|
||||
# END ai.skillz: direct:symlink:agents:opencode-cleaning
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:plan-io
|
||||
/.agents/skills/plan-io
|
||||
# END ai.skillz: direct:symlink:agents:plan-io
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:pr-msg
|
||||
/.agents/skills/pr-msg
|
||||
# END ai.skillz: direct:symlink:agents:pr-msg
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:prompt-io
|
||||
/.agents/skills/prompt-io
|
||||
# END ai.skillz: direct:symlink:agents:prompt-io
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:py-codestyle
|
||||
/.agents/skills/py-codestyle
|
||||
# END ai.skillz: direct:symlink:agents:py-codestyle
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:taken-export
|
||||
/.agents/skills/taken-export
|
||||
# END ai.skillz: direct:symlink:agents:taken-export
|
||||
|
||||
# BEGIN ai.skillz: direct:symlink:agents:yt-url-lookup
|
||||
/.agents/skills/yt-url-lookup
|
||||
# END ai.skillz: direct:symlink:agents:yt-url-lookup
|
||||
|
||||
# BEGIN ai.skillz: workflow-state
|
||||
/.ai/state/
|
||||
/.ai/commit-msg/conf.toml
|
||||
/.ai/pr-msg/conf.toml
|
||||
# END ai.skillz: workflow-state
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
# Split `brokerd.<broker>` into trading-only `brokerd` + new `datad.<broker>`
|
||||
|
||||
## Context
|
||||
|
||||
Today a single `brokerd.<broker>` actor hosts BOTH concerns:
|
||||
|
||||
- **data feed service tasks**: the `_FeedsBus` + `open_feed_bus()` ep (`piker/data/feed.py:464`), per-symbol `allocate_persistent_feed()` tasks (shm writers via `sample_and_broadcast()`, history backfill via `piker.tsp`), plus backend eps `stream_quotes`, `open_history_client`, `open_symbol_search`, `get_mkt_info` from `piker/brokers/<backend>/feed.py`/`symbols.py`,
|
||||
- **live order-control tasks**: `open_trade_dialog()` from `piker/brokers/<backend>/broker.py`, driven by `emsd`.
|
||||
|
||||
The codebase already anticipates this split: `piker/data/validate.py:70-91` groups backend eps into `'datad'` vs `'brokerd'` kinds, `piker/brokers/ib/__init__.py:62-70` already declares `_brokerd_mods`/`_datad_mods`, and `piker/brokers/_daemon.py:62` carries the literal TODO *"rename the daemon to datad prolly once we split up broker vs. data tasks into separate actors?"*. This work executes that split.
|
||||
|
||||
**User-decided constraints:**
|
||||
|
||||
- `datad.<broker>` is a **sibling** of `brokerd.<broker>` under `pikerd`, spawned via the existing `Services` + `maybe_spawn_daemon()` machinery (`piker/service/_daemon.py:46`).
|
||||
- **Hard cutover, staged by layer** — no dual-mode runtime flag; every stage lands fully working.
|
||||
- Post-split `brokerd` is **trading-only and lazily spawned solely by emsd's** `open_brokerd_dialog` path; UI/CLI/feed code never spawns it. Chart-only and paper sessions run with **zero** brokerd processes.
|
||||
|
||||
## Target topology
|
||||
|
||||
```
|
||||
pikerd
|
||||
├── datad.ib feed bus, shm writers, tsp history, symbol search
|
||||
├── brokerd.ib open_trade_dialog only (EMS-spawned, lazy)
|
||||
├── emsd
|
||||
│ └── paperboi.ib (paper mode; opens its feed via datad)
|
||||
└── samplerd
|
||||
```
|
||||
|
||||
## Key verified facts (load-bearing)
|
||||
|
||||
- `feed.portals` has exactly ONE trading consumer: `piker/clearing/_ems.py:671` (`portal = feed.portals[brokermod]` → handed to `open_brokerd_dialog`). This is the single coupling forcing feed + trading into one actor.
|
||||
- `assert 'brokerd' in servicename` at `piker/data/feed.py:502` is the only actor-name assert in the tree.
|
||||
- `piker ledger` (`piker/accounting/cli.py:100-157`) is a hidden consumer: it calls `broker_init()` directly, spawns its own ad-hoc actor, enters `_setup_persistent_brokerd`, then calls `open_brokerd_dialog(brokermod, portal, ...)` **with an explicit portal** — the new signature must keep a `portal:` override param.
|
||||
- kraken's feed→broker coupling is mild: `kraken/broker.py:77-81` imports `NoBsWs`/`open_autorecon_ws` (really from `piker.data._web_bs`) and `stream_messages` (pure parser) from `feed.py`. In-process imports only — `enable_modules` gates RPC, not imports. No live shared state; each actor opens its own WS.
|
||||
- ib: default `client_id=6116` (`ib/api.py:1320`) with linear retry `clientId=client_id + i` (`:1403`). Two ib actors connecting concurrently will collide → needs a role-based id offset. Both daemons need `_spawn_kwargs={'infect_asyncio': True}` (copied by both init fns).
|
||||
- binance has no `symbols.py` (`get_mkt_info`/`open_symbol_search` live in `binance/feed.py`); kucoin/questrade/robinhood are flat single-file with NO `__enable_modules__`; deribit has no `broker.py`.
|
||||
- `tests/test_services.py:80,185` assert `brokerd` actor names incl. the paper-mode flow asserting `brokerd.kraken` spawns (`:229,:237`) — must invert post-split.
|
||||
- `_root_modules` (`piker/service/_actor_runtime.py:156`) must gain the new datad daemon mod so `pikerd_portal.run(spawn_datad, ...)` resolves.
|
||||
- `piker/brokers/core.py:175` `symbol_search` does `portal.run(search_w_brokerd)` where the target fn lives in `piker.brokers.core` → that mod must stay RPC-enabled in datad.
|
||||
|
||||
## Module decision
|
||||
|
||||
New file **`piker/data/_daemon.py`** hosts all datad machinery (`_setup_persistent_datad`, `datad_init`, `spawn_datad`, `maybe_spawn_datad`, `_datad_service_mods`). Mirrors the `piker.brokers._daemon` / `piker.data._sampling` (samplerd) per-subsystem convention and satisfies the existing TODO at `brokers/_daemon.py:49` ("move this def to the `.data` subpkg"). `piker.brokers._daemon` keeps brokerd-only code, slimmed. Do NOT over-DRY the two ~40-line init fns into a shared factory yet (samplerd precedent accepts the duplication).
|
||||
|
||||
---
|
||||
|
||||
## Stage 0 — prep: backend module grouping + ep introspection (no topology change)
|
||||
|
||||
1. `piker/data/validate.py`: add a `get_eps(mod, kind) -> dict[str, Callable]` helper returning the backend's defined eps for a daemon-kind from `_eps` (missing eps excluded). Used later by both init fns + fail-fast checks.
|
||||
2. Declare daemon-kind module groups in each split backend's `__init__.py`, keeping `__enable_modules__` as the (deduped) union so behavior is unchanged:
|
||||
- `kraken`: `_brokerd_mods = ['api', 'broker']`, `_datad_mods = ['api', 'feed', 'symbols']`
|
||||
- `binance`: `_brokerd_mods = ['api', 'broker']`, `_datad_mods = ['api', 'feed']`
|
||||
- `deribit`: `_brokerd_mods = []`, `_datad_mods = ['api', 'feed']`
|
||||
- `ib`: adjust existing groups so each includes `'api'`
|
||||
- flat backends (kucoin etc.): no attrs — init fns fall back to enabling just `modpath` (existing behavior).
|
||||
3. Hygiene: `kraken/broker.py:77-81` imports `NoBsWs`/`open_autorecon_ws` from `piker.data._web_bs` directly (keep the `stream_messages` import from `.feed`).
|
||||
|
||||
**Gate**: full pytest green, zero behavior change.
|
||||
|
||||
## Stage 1 — introduce `datad` daemon machinery (additive)
|
||||
|
||||
New `piker/data/_daemon.py`:
|
||||
|
||||
- `_datad_service_mods: list[str]` — datad-always-enabled mods (successor to the data side of `_data_mods` from `brokers/_daemon.py:52`): `['piker.brokers.core', 'piker.brokers.data', 'piker.data', 'piker.data.feed', 'piker.data._sampling', 'piker.data._daemon']`.
|
||||
- `_setup_persistent_datad(ctx, brokername, loglevel)` — `@tractor.context` fixture; logging boilerplate (as `_setup_persistent_brokerd:81-88`), then allocates the actor-global feed bus exactly as the brokerd fixture does today (`brokers/_daemon.py:105-121`): `assert not feed._bus`, open service nursery, `feed.get_feed_bus(brokername, service_nursery)`, `ctx.started()`, `sleep_forever()`.
|
||||
- `datad_init(brokername, ...)` — mirrors `broker_init()` (`brokers/_daemon.py:132`): actor name `f'datad.{brokername}'`, copies backend `_spawn_kwargs` (**critical for ib infect_asyncio**), builds `enable_modules` from `getattr(brokermod, '_datad_mods', getattr(brokermod, '__enable_modules__', []))`.
|
||||
- `spawn_datad(brokername, ...)` — mirrors `spawn_brokerd()` (`brokers/_daemon.py:202`): `Services.actor_n.start_actor(dname, enable_modules=_datad_service_mods + backend_mods, ...)` + `Services.start_service_task(dname, portal, _setup_persistent_datad, ...)`.
|
||||
- `maybe_spawn_datad(brokername, ...)` — wraps `maybe_spawn_daemon(service_name=f'datad.{brokername}', service_task_target=spawn_datad, ...)` exactly like `maybe_spawn_brokerd` (`brokers/_daemon.py:256`).
|
||||
|
||||
Supporting edits:
|
||||
|
||||
- `piker/service/_actor_runtime.py:156`: add `'piker.data._daemon'` to `_root_modules` (keep `'piker.brokers._daemon'`).
|
||||
- `piker/service/__init__.py`: re-export `spawn_datad`/`maybe_spawn_datad` next to the brokerd ones (`:56-57`).
|
||||
- `tests/test_services.py`: new `test_datad_spawn` — `open_test_pikerd` + `maybe_spawn_datad('kraken')` + `ensure_service('datad.kraken')`. (Do NOT route a feed through it yet — `open_feed_bus`'s assert still says brokerd.)
|
||||
|
||||
**Gate**: suite green + new spawn test; nothing routes through datad yet.
|
||||
|
||||
## Stage 2 — clearing layer: emsd self-spawns brokerd (decouple from `feed.portals`)
|
||||
|
||||
Sequenced BEFORE the feed cutover so live trading works at every boundary (otherwise `feed.portals` would hand emsd a datad portal).
|
||||
|
||||
`piker/clearing/_ems.py`:
|
||||
|
||||
1. `open_brokerd_dialog()` (`:336`) new signature: `(brokermod, exec_mode, fqme=None, portal: tractor.Portal|None = None, loglevel=None)`. Internals: keep trades-ep detection (`:400-417`); acquire the brokerd actor ONLY when a live ep will actually open, via a small inner `@acm _acquire_live_portal()` that yields the passed `portal` if given (the `piker ledger` path) else `async with maybe_spawn_brokerd(brokermod.name, loglevel=loglevel)` — **the single place brokerd boots post-split**. Move the eager `portal.open_context(trades_endpoint, ...)` construction (`:425`) inside that block; the paper short-circuit (`:432`) never touches it.
|
||||
2. `Router.maybe_open_brokerd_dialog()` (`:581`) — drop the `portal` param; `Router.open_trade_relays()` (`:640`) — delete `portal = feed.portals[brokermod]` (`:671`) and the pass-through (`:685`).
|
||||
3. `piker/accounting/cli.py:144`: switch to keyword form `open_brokerd_dialog(brokermod, exec_mode=..., portal=portal, loglevel=...)`.
|
||||
|
||||
Pre-cutover this is a pure refactor: emsd's `maybe_spawn_brokerd` just *finds* the already-running brokerd via the registry.
|
||||
|
||||
**Gate**: `tests/test_ems.py` + services suite green; manual paper order on kraken; `piker ledger` smoke.
|
||||
|
||||
## Stage 3 — feed layer hard cutover to datad
|
||||
|
||||
1. `piker/data/feed.py`: import `maybe_spawn_datad` (replacing `maybe_spawn_brokerd`, `:56-58`); `open_feed()` `:895` → `maybe_spawn_datad(...)` (rename `brokerd_ctxs` → `datad_ctxs`); `open_feed_bus()` `:502` → `assert 'datad' in servicename`; comment sweep.
|
||||
2. `piker/brokers/_daemon.py` `_setup_persistent_brokerd()`: slim to trading-only fixture — logging setup, `ctx.started()`, `sleep_forever()`. Drop the bus alloc, `assert not feed._bus`, the service nursery (backend `open_trade_dialog` ctxs own their task trees), the `eg.ExceptionGroup` handler, and the stale `_FeedsBus` TYPE_CHECKING import. (`piker ledger`'s ad-hoc actor enters this same slimmed fixture — exactly what it needs.)
|
||||
3. Repoint remaining data-flavored spawn sites `maybe_spawn_brokerd` → `maybe_spawn_datad`:
|
||||
- `piker/ui/_app.py:40,57` (symbol search; optionally rename `install_brokerd_search` → `install_datad_search`, def at `piker/data/feed.py:766`)
|
||||
- `piker/brokers/core.py:33,175` (`symbol_search`)
|
||||
- `piker/brokers/cli.py:38,190,320` (`brokercheck`, `record`)
|
||||
- `piker/ui/cli.py:30,72,121` (legacy kivy monitor/optschain) + best-effort `piker/ui/kivy/option_chain.py:498` `wait_for_actor('brokerd')` → `'datad'`
|
||||
4. `tests/test_services.py`: `:80,:185` `actor_name = 'datad'`; paper-mode test asserts `datad.kraken` + `paperboi.kraken` + `emsd` AND adds the headline negative check: `assert 'brokerd.kraken' not in services.service_tasks`.
|
||||
|
||||
**Gate**: full suite with inverted assertions; manual: chart on binance/kraken shows `datad.<broker>` in `piker services` and NO brokerd; paper order fills; symbol search works; history backfill sane.
|
||||
|
||||
## Stage 4 — caps-sec slimming, validation, ib client-id
|
||||
|
||||
1. `piker/brokers/_daemon.py`: delete `_data_mods` (`:52`); `spawn_brokerd()` `:236` → `enable_modules=tractor_kwargs.pop('enable_modules')`; `broker_init()` `:184` reads `getattr(brokermod, '_brokerd_mods', getattr(brokermod, '__enable_modules__', []))`. Resulting brokerd enable set has no `piker.data.*` at all.
|
||||
2. Fail-fast ep validation via Stage-0 `validate.get_eps()`: `broker_init` raises a clear error when `get_eps(mod, 'brokerd')` is empty (e.g. "kucoin is datad-only — use paper mode"); `datad_init` warns analogously.
|
||||
3. ib client-id mitigation in `load_aio_clients()` (`ib/api.py:~1316`): role-based offset when `client_id` is the 6116 default (e.g. `+16` when `'datad' in tractor.current_actor().name`), optionally configurable via `brokers.toml` keys.
|
||||
4. Docs/comment sweep: resolve `brokers/_daemon.py:62` TODO, `piker/cli/__init__.py:253` TODO, daemon list in `piker/service/__init__.py:21` docstring.
|
||||
|
||||
**Gate**: full suite; audit greps clean: `grep -rn maybe_spawn_brokerd piker/` hits only `clearing/_ems.py`, `service/__init__.py`, `brokers/_daemon.py`; no spawn-path `'brokerd'` literals left in `piker/data`, `piker/ui`, `piker/brokers/cli.py`.
|
||||
|
||||
---
|
||||
|
||||
## Risk register
|
||||
|
||||
| Risk | Sev | Mitigation |
|
||||
|---|---|---|
|
||||
| ib: `datad.ib` + `brokerd.ib` both connect to TWS/gw; `client_id=6116` collision burns `connect_timeout` retries | High (live ib only) | Stage 4 role-based id offset; both daemons inherit `infect_asyncio` via `_spawn_kwargs` copy in both init fns |
|
||||
| `piker ledger` ad-hoc actor path silently broken by signature change | Med | `portal:` override kept on `open_brokerd_dialog` (Stage 2); explicit smoke test |
|
||||
| datad-only backends (kucoin/deribit) → useless brokerd spawn | Low | already covered: `open_brokerd_dialog` forces `exec_mode='paper'` when no trades ep (`_ems.py:413-417`) BEFORE the lazy spawn; Stage 4 fail-fast |
|
||||
| brokerd in-process symbology/ledger needs (ib `broker.py` imports `.symbols`; kraken uses `stream_messages`) | None | verified pure in-process imports; `enable_modules` gates RPC only |
|
||||
| paper-mode test asserts brokerd spawns (`test_services.py:237`) | Low | Stage 3 deliberately inverts it |
|
||||
| doubled per-broker clients (HTTP/WS, symcache loads) | Low | each actor already opens its own WS today; symcache is disk-read-mostly |
|
||||
|
||||
## Verification (per stage gates above, plus end-to-end)
|
||||
|
||||
- `pytest tests/test_services.py tests/test_feeds.py tests/test_ems.py` at every stage (kraken/binance public endpoints, no creds needed).
|
||||
- Manual smoke matrix post-Stage-3: `pikerd -l info` + `piker chart btcusdt.spot.binance` → `piker services` shows `datad.binance`, no brokerd; submit paper order → `paperboi.binance` appears, still no brokerd; symbol search; `piker ledger <broker>.paper`.
|
||||
- If ib creds/gateway available: live ib chart + small live order to exercise dual-actor client-id path (Stage 4).
|
||||
|
||||
## Critical files
|
||||
|
||||
- **new** `piker/data/_daemon.py` — all datad machinery
|
||||
- `piker/brokers/_daemon.py` — slimmed brokerd fixture/init/spawn
|
||||
- `piker/data/feed.py` — spawn cutover + actor-name assert
|
||||
- `piker/clearing/_ems.py` — emsd lazy brokerd spawn (`open_brokerd_dialog`, `Router`)
|
||||
- `piker/service/_actor_runtime.py`, `piker/service/__init__.py` — root mods + re-exports
|
||||
- `piker/data/validate.py` — `get_eps()` helper
|
||||
- backend `__init__.py`s (kraken/binance/deribit/ib) — `_datad_mods`/`_brokerd_mods`
|
||||
- `piker/accounting/cli.py` — keyword-form dialog call
|
||||
- `tests/test_services.py` — inverted + new assertions
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T17:08:59Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T170859Z_75cefe10_prompt_io.md
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260610T170859Z_75cefe10_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Session-initiating instruction (driving all 7 commits in
|
||||
this series):
|
||||
|
||||
> ok i want you to become the distributed runtime and
|
||||
> concurrency expert for this project - namely acquire
|
||||
> a deep understanding of tractor and how it's used.
|
||||
> then i want you to attempt to factor our current
|
||||
> brokerd service daemon into 2 daemons: a brokerd
|
||||
> which only hosts live/paper trading endpoint tasks
|
||||
> [..] a new `datad` subdaemon which in a separate
|
||||
> subactor serves all the data feed service tasks [..]
|
||||
> give me a mega detailed plan on how to approach this,
|
||||
> and a staged approach for the implementation.
|
||||
|
||||
Proximate driver for THIS commit: during the approved
|
||||
plan's stage-0 test gate the agent discovered test
|
||||
subactors were writing into the user's REAL
|
||||
`~/.config/piker/accounting/` files (a bogus paper fill
|
||||
landed in `account.kraken.paper.toml`) because the old
|
||||
test-dir override in `config.get_app_dir()` was
|
||||
commented out and could never work in spawned
|
||||
subactors anyway. Fix applied autonomously under the
|
||||
approved plan; no explicit per-fix user prompt.
|
||||
|
||||
## Response summary
|
||||
|
||||
Restore `pytest` config-dir isolation by resolving the
|
||||
per-test tmp dir lazily (at conf-path access time) from
|
||||
`tractor` runtime-vars, which propagate down the actor
|
||||
tree; route all conf-path derivation through
|
||||
`config.get_conf_dir()` so the override is effective in
|
||||
every (sub)actor.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/config.py` — add `_maybe_use_test_dir()`;
|
||||
hook in `get_conf_dir()`; route `get_conf_path()` +
|
||||
`load()` mkdir through it
|
||||
- `piker/accounting/_ledger.py` — derive ledger dir
|
||||
via `config.get_conf_dir()` (not the global)
|
||||
- `piker/accounting/_pos.py` — same for account files
|
||||
|
||||
## Human edits
|
||||
|
||||
None — committed as generated.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T17:08:59Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T170859Z_75cefe10_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry (code committed in the same
|
||||
commit as this log); backfilled from the live dev
|
||||
session transcript per the `/prompt-io` skill rules.
|
||||
|
||||
> `git log -1 -p --follow -- piker/config.py`
|
||||
|
||||
Generated: `config._maybe_use_test_dir()` — lazily
|
||||
reads `piker_test_dir` from
|
||||
`tractor.runtime._state._runtime_vars['piker_vars']`
|
||||
(pre-loaded by `open_piker_runtime()` from the
|
||||
`tests.conftest._open_test_pikerd()` overrides) and
|
||||
calls `_override_config_dir()` when set. Hooked at the
|
||||
top of `get_conf_dir()`; `get_conf_path()` and
|
||||
`load()`'s dir-creation rerouted through
|
||||
`get_conf_dir()`.
|
||||
|
||||
> `git log -1 -p --follow -- piker/accounting/_ledger.py`
|
||||
> `git log -1 -p --follow -- piker/accounting/_pos.py`
|
||||
|
||||
Generated: ledger/account dir derivation switched from
|
||||
the `config._config_dir` module global to
|
||||
`config.get_conf_dir()` + `mkdir(parents=True,
|
||||
exist_ok=True)` for nested tmp-dir creation.
|
||||
|
||||
Key diagnostic reasoning (verbatim from session):
|
||||
|
||||
The old (commented-out) `get_app_dir()` override gated
|
||||
on `'pytest' in sys.modules` which can NEVER work in
|
||||
spawned subactors (fresh procs, no pytest import); as
|
||||
a result test `paperboi`/daemon actors were writing
|
||||
into the user's REAL `~/.config/piker/accounting/`
|
||||
files. Evidence: `account.kraken.paper.toml` gained a
|
||||
single 0.001 xbtusdt clear stamped 2026-06-09T18:47Z
|
||||
(a test fill), and `trades_kraken_paper.toml` (252B)
|
||||
contained only that fill. The resolution must be
|
||||
checked lazily at config-path access time (NOT import
|
||||
time) since sub-actors only receive runtime-vars once
|
||||
their `tractor` runtime has fully booted.
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T17:10:22Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171022Z_4485f2b9_prompt_io.md
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260610T171022Z_4485f2b9_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Same session-initiating `brokerd`-split instruction (see
|
||||
`20260610T170859Z_75cefe10_prompt_io.md`). Proximate
|
||||
driver: the approved plan's per-stage test gates could
|
||||
not run AT ALL — the branch base was broken vs.
|
||||
`tractor` git `main` (`AttributeError: 'tuple' object
|
||||
has no attribute 'unwrap'` at `pikerd` boot, stale
|
||||
`arbiter_sockaddr`/`.type`/`.size` API refs). The agent
|
||||
fixed forward autonomously to (re)establish the gate
|
||||
baseline, continuing the `repair_tests` branch lineage
|
||||
(verified already merged in ancestry via `git cherry`
|
||||
during a user-requested branch-overlap survey).
|
||||
|
||||
## Response summary
|
||||
|
||||
Port the service layer + test suites to current
|
||||
`tractor` APIs: addr-type normalization in
|
||||
`open_pikerd()`, `query_actor()`/`wait_for_actor()`
|
||||
kwarg renames, modern self-cancel absorption semantics
|
||||
in the cancel-method test harness, exc/position attr
|
||||
renames, a paper-EMS startup-budget bump and a syntax
|
||||
fix in `.deribit.api`.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/service/_actor_runtime.py` — `wrap_address()`
|
||||
normalize before `.unwrap()` in `open_pikerd()`
|
||||
- `piker/service/_registry.py` — `check_for_service()`
|
||||
-> `query_actor(regaddr=)` + 2-tuple yield +
|
||||
`open_registry(addrs=)`
|
||||
- `piker/brokers/deribit/api.py` — missing comma in
|
||||
`tractor.trionics` import tuple
|
||||
- `tests/test_services.py` — `registry_addr=` kwarg,
|
||||
raddr unwraps, cancel-semantics harness rewrite,
|
||||
`fail_after` 9 -> 19s
|
||||
- `tests/test_ems.py` — `.boxed_type`, `pp.cumsize`
|
||||
|
||||
## Human edits
|
||||
|
||||
None — committed as generated.
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T17:10:22Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171022Z_4485f2b9_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry (code committed in the same
|
||||
commit as this log); backfilled from the live dev
|
||||
session transcript per the `/prompt-io` skill rules.
|
||||
|
||||
> `git log -1 -p --follow -- piker/service/_actor_runtime.py`
|
||||
|
||||
Generated: normalize each registry addr via
|
||||
`tractor.discovery._addr.wrap_address()` before
|
||||
`.unwrap()`-ing for the `accept_addrs` bind check —
|
||||
entries may be raw `tuple`s when passed in from (test)
|
||||
client code. Import-path precedent taken from
|
||||
`piker/cli/__init__.py:336`.
|
||||
|
||||
> `git log -1 -p --follow -- piker/service/_registry.py`
|
||||
|
||||
Generated: `check_for_service()` ported to
|
||||
`tractor.query_actor(name, regaddr=...)` (kwarg was
|
||||
`arbiter_sockaddr=`), unpacking the new
|
||||
`(sockaddr, portal)` yield, and passing the
|
||||
now-required `open_registry(addrs=Registry.addrs)`.
|
||||
|
||||
> `git log -1 -p --follow -- tests/test_services.py`
|
||||
> `git log -1 -p --follow -- tests/test_ems.py`
|
||||
> `git log -1 -p --follow -- piker/brokers/deribit/api.py`
|
||||
|
||||
Key diagnostic reasoning (verbatim from session):
|
||||
|
||||
- the "DID NOT RAISE ContextCancelled" failure: in this
|
||||
test the client actor IS pikerd (in-proc), and
|
||||
current `tractor` main absorbs a `ContextCancelled`
|
||||
whose canceller is your own actor — self-requested
|
||||
cancels now exit cleanly instead of raising; the
|
||||
'sigint' variant propagates a bare collapsed
|
||||
`KeyboardInterrupt` rather than a
|
||||
`BaseExceptionGroup`.
|
||||
- the hard-coded `trio.fail_after(9)` startup budget is
|
||||
marginal — full stack boot (pikerd -> emsd ->
|
||||
brokerd.kraken -> paperboi + live kraken symbology
|
||||
fetch) occasionally exceeds 9s -> bumped to 19s.
|
||||
- `RemoteActorError.type` -> `.boxed_type`;
|
||||
`Position.size` -> `.cumsize` (the paper engine
|
||||
populates `BrokerdPosition.size` from `pp.cumsize`).
|
||||
- overlap survey (user-requested): all of the
|
||||
`repair_tests` branch commits are already in this
|
||||
stack's ancestry; this commit finishes that branch's
|
||||
port mission (its f4c4f1e2 fixed `conftest.py`'s
|
||||
`arbiter_sockaddr` usage; this fixes the remaining
|
||||
`test_services.py` + `check_for_service()` sites).
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T17:11:05Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171105Z_bc6e18d7_prompt_io.md
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260610T171105Z_bc6e18d7_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Same session-initiating `brokerd`-split instruction (see
|
||||
`20260610T170859Z_75cefe10_prompt_io.md`); this is the
|
||||
approved plan's "stage 0" prep. Plan-shaping user
|
||||
decisions captured via in-session Q&A:
|
||||
|
||||
- `datad.<broker>` topology: sibling of
|
||||
`brokerd.<broker>` under `pikerd` (vs. child
|
||||
subactor).
|
||||
- migration: hard cutover, staged by layer (no
|
||||
dual-mode runtime flag).
|
||||
- post-split `brokerd` scope: trading-only +
|
||||
EMS-lazy-spawned (charts/CLI never touch it).
|
||||
|
||||
## Response summary
|
||||
|
||||
Declare per-daemon-kind backend submod groups
|
||||
(`_datad_mods`/`_brokerd_mods`) in the split-style
|
||||
backends, keyed to the pre-existing
|
||||
`piker.data.validate._eps` contract; add a
|
||||
`validate.get_eps()` introspection helper; ws-import
|
||||
hygiene in `.kraken.broker`. Zero behavior change
|
||||
(`__enable_modules__` unions unchanged).
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/data/validate.py` — add `get_eps()`
|
||||
- `piker/brokers/kraken/__init__.py` — mod groups
|
||||
- `piker/brokers/binance/__init__.py` — mod groups
|
||||
(note: no `symbols.py`; search eps live in `.feed`)
|
||||
- `piker/brokers/deribit/__init__.py` — datad-only
|
||||
groups (no `broker.py` yet)
|
||||
- `piker/brokers/ib/__init__.py` — add `'api'` to the
|
||||
pre-existing `_datad_mods`
|
||||
- `piker/brokers/kraken/broker.py` — import
|
||||
`NoBsWs`/`open_autorecon_ws` from
|
||||
`piker.data._web_bs` directly
|
||||
|
||||
## Human edits
|
||||
|
||||
None — committed as generated.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T17:11:05Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171105Z_bc6e18d7_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry (code committed in the same
|
||||
commit as this log); backfilled from the live dev
|
||||
session transcript per the `/prompt-io` skill rules.
|
||||
|
||||
> `git log -1 -p --follow -- piker/data/validate.py`
|
||||
|
||||
Generated: `get_eps(mod, kind) -> dict[str, Callable]`
|
||||
returning the daemon-kind's ep funcs defined by a
|
||||
backend mod, keyed by ep name, missing eps excluded;
|
||||
sourced from the existing `_eps` grouping table
|
||||
(`'middleware' | 'datad' | 'brokerd'`).
|
||||
|
||||
> `git log -1 -p --follow -- piker/brokers/kraken/__init__.py`
|
||||
> `git log -1 -p --follow -- piker/brokers/binance/__init__.py`
|
||||
> `git log -1 -p --follow -- piker/brokers/deribit/__init__.py`
|
||||
> `git log -1 -p --follow -- piker/brokers/ib/__init__.py`
|
||||
> `git log -1 -p --follow -- piker/brokers/kraken/broker.py`
|
||||
|
||||
Key exploration findings driving the design (from the
|
||||
planning phase, 3 parallel explore agents + 1 architect
|
||||
agent):
|
||||
|
||||
- the codebase already anticipated this split:
|
||||
`validate._eps` groups eps into 'datad' vs 'brokerd'
|
||||
kinds and `ib/__init__.py` already declared
|
||||
`_brokerd_mods`/`_datad_mods`; `brokers/_daemon.py`
|
||||
carried the literal TODO "rename the daemon to datad
|
||||
prolly once we split up broker vs. data tasks into
|
||||
separate actors?".
|
||||
- the feared kraken feed<->broker "shared ws state"
|
||||
hazard is mild: `NoBsWs`/`open_autorecon_ws`
|
||||
originate in `piker.data._web_bs` (re-exported via
|
||||
`.kraken.feed`); only `stream_messages` is feed-local
|
||||
and it's a pure parser — in-process imports only,
|
||||
each actor opens its own ws conn.
|
||||
- `enable_modules` gates RPC entry, NOT python imports,
|
||||
so backend trading mods may keep importing feed-side
|
||||
helpers in-process post-split.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T17:11:42Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171142Z_119d2c04_prompt_io.md
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260610T171142Z_119d2c04_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Same session-initiating `brokerd`-split instruction (see
|
||||
`20260610T170859Z_75cefe10_prompt_io.md`); this is the
|
||||
approved plan's "stage 1": introduce the `datad` daemon
|
||||
machinery additively (nothing routes through it yet).
|
||||
User-decided constraint applied: `datad.<broker>` is a
|
||||
SIBLING of `brokerd.<broker>` under `pikerd`, spawned
|
||||
via the existing `Services` + `maybe_spawn_daemon()`
|
||||
machinery.
|
||||
|
||||
## Response summary
|
||||
|
||||
New `piker/data/_daemon.py` hosting the
|
||||
`datad.<broker>` feed-only daemon-actor: lifetime
|
||||
fixture owning the actor-global `_FeedsBus`, init/spawn
|
||||
fns mirroring `.brokers._daemon` conventions and the
|
||||
`samplerd` sub-daemon precedent, plus root-mod
|
||||
registration, `piker.service` re-exports and a spawn
|
||||
test.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/data/_daemon.py` — NEW:
|
||||
`_setup_persistent_datad()`, `datad_init()`,
|
||||
`spawn_datad()`, `maybe_spawn_datad()`,
|
||||
`_datad_service_mods`
|
||||
- `piker/service/_actor_runtime.py` — add
|
||||
`piker.data._daemon` to `_root_modules`
|
||||
- `piker/service/__init__.py` — re-export spawn eps
|
||||
- `tests/test_services.py` — add `test_datad_spawn`
|
||||
|
||||
## Human edits
|
||||
|
||||
None — committed as generated.
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T17:11:42Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171142Z_119d2c04_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry (code committed in the same
|
||||
commit as this log); backfilled from the live dev
|
||||
session transcript per the `/prompt-io` skill rules.
|
||||
|
||||
> `git log -1 -p --follow -- piker/data/_daemon.py`
|
||||
|
||||
Generated symbols + key design decisions:
|
||||
|
||||
- `_datad_service_mods: list[str]` — datad-always
|
||||
enabled mods, the data-side successor to the old
|
||||
`piker.brokers._daemon._data_mods` set; kept minimal
|
||||
per the caps-sec model.
|
||||
- `_setup_persistent_datad()` — `@tractor.context`
|
||||
lifetime fixture: console-log setup then allocates
|
||||
the actor-global feed bus via
|
||||
`feed.get_feed_bus(brokername, service_nursery)`
|
||||
exactly as the old brokerd fixture did, pinned open
|
||||
with `ctx.started()` + `sleep_forever()`.
|
||||
- `datad_init()` — actor name `f'datad.{brokername}'`;
|
||||
copies backend `_spawn_kwargs` (CRITICAL for `ib`'s
|
||||
`infect_asyncio=True`); builds `enable_modules` from
|
||||
`getattr(brokermod, '_datad_mods',
|
||||
getattr(brokermod, '__enable_modules__', []))` as
|
||||
the flat-backend fallback.
|
||||
- `spawn_datad()` — `Services.actor_n.start_actor()` +
|
||||
`Services.start_service_task()` exactly mirroring
|
||||
`spawn_brokerd()`; dedup-composes enable mods via
|
||||
`list(dict.fromkeys(...))`.
|
||||
- `maybe_spawn_datad()` — wraps `maybe_spawn_daemon(
|
||||
service_name=f'datad.{brokername}', ...)`.
|
||||
|
||||
> `git log -1 -p --follow -- piker/service/_actor_runtime.py`
|
||||
> `git log -1 -p --follow -- piker/service/__init__.py`
|
||||
> `git log -1 -p --follow -- tests/test_services.py`
|
||||
|
||||
Design rationale (verbatim from session):
|
||||
|
||||
- `_root_modules` must gain `piker.data._daemon` so
|
||||
`pikerd_portal.run(spawn_datad, ...)` resolves in
|
||||
the root.
|
||||
- the `Services`-based impl style deliberately mirrors
|
||||
`spawn_brokerd()` so the eventual `tractor.hilevel`
|
||||
`ServiceMngr` port (see the `service_mng_to_tractor`
|
||||
branch's d8c21d44 prep, surfaced by the
|
||||
user-requested branch-overlap survey) lands
|
||||
symmetrically on both spawn fns.
|
||||
- mod placement (`piker/data/_daemon.py` vs.
|
||||
generalizing `piker.brokers._daemon`) follows the
|
||||
per-subsystem daemon-mod convention
|
||||
(`.clearing._ems`, `.data._sampling`) and resolves
|
||||
the existing TODO at `brokers/_daemon.py:49` ("move
|
||||
this def to the `.data` subpkg").
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T17:12:26Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171226Z_64181219_prompt_io.md
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260610T171226Z_64181219_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Same session-initiating `brokerd`-split instruction (see
|
||||
`20260610T170859Z_75cefe10_prompt_io.md`); this is the
|
||||
approved plan's "stage 2": decouple the clearing layer
|
||||
from `feed.portals` BEFORE the feed cutover so live
|
||||
trading works at every stage boundary. User-decided
|
||||
constraint applied: post-split `brokerd` is
|
||||
trading-only and spawned LAZILY only by `emsd`'s
|
||||
`open_brokerd_dialog()` path.
|
||||
|
||||
## Response summary
|
||||
|
||||
Kill the single coupling forcing feed + trading eps
|
||||
into one actor (`Router.open_trade_relays()` pulling
|
||||
its trades portal from `feed.portals[brokermod]`):
|
||||
`open_brokerd_dialog()` now (maybe) spawns/finds
|
||||
`brokerd.<broker>` itself and ONLY when a live
|
||||
trades-ep will actually open; paper mode never touches
|
||||
it. Pre-cutover this is a pure refactor (registry
|
||||
lookup finds the same feed-spawned daemon).
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/clearing/_ems.py` — `open_brokerd_dialog()`
|
||||
re-sig + inner `acquire_live_portal()`;
|
||||
`Router.maybe_open_brokerd_dialog()` drops `portal`
|
||||
param; `open_trade_relays()` drops the
|
||||
`feed.portals` lookup
|
||||
- `piker/accounting/cli.py` — keyword-form `portal=`
|
||||
override kept for the `piker ledger` ad-hoc actor
|
||||
|
||||
## Human edits
|
||||
|
||||
None — committed as generated.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T17:12:26Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171226Z_64181219_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry (code committed in the same
|
||||
commit as this log); backfilled from the live dev
|
||||
session transcript per the `/prompt-io` skill rules.
|
||||
|
||||
> `git log -1 -p --follow -- piker/clearing/_ems.py`
|
||||
|
||||
Generated: new `open_brokerd_dialog()` signature
|
||||
`(brokermod, exec_mode, fqme=None, portal=None,
|
||||
loglevel=None)` with an inner `@acm
|
||||
acquire_live_portal()` that yields the caller-provided
|
||||
`portal` override (the `piker ledger` path) else
|
||||
`maybe_spawn_brokerd(brokermod.name)` — designated THE
|
||||
one place a live, credentialed `brokerd.<broker>` gets
|
||||
booted post-split. The eager
|
||||
`portal.open_context(trades_endpoint, ...)`
|
||||
construction moved inside that block so the paper
|
||||
short-circuit never acquires a live portal.
|
||||
|
||||
> `git log -1 -p --follow -- piker/accounting/cli.py`
|
||||
|
||||
Key analysis (verbatim from session):
|
||||
|
||||
- `feed.portals` had exactly ONE trading consumer:
|
||||
`piker/clearing/_ems.py:671` (`portal =
|
||||
feed.portals[brokermod]`) — the single coupling
|
||||
forcing feed + trading into one actor.
|
||||
- `piker ledger` (`accounting/cli.py:100-157`) is a
|
||||
hidden consumer: it calls `broker_init()` directly,
|
||||
spawns its own ad-hoc actor and passes the portal
|
||||
positionally — the new signature must keep an
|
||||
explicit `portal:` override param for this path.
|
||||
- stage sequencing: clearing decouple lands BEFORE the
|
||||
feed cutover, otherwise `feed.portals` would hand
|
||||
emsd a `datad` portal and live `open_trade_dialog`
|
||||
RPC would fail.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T17:12:59Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171259Z_59d5d9a6_prompt_io.md
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260610T171259Z_59d5d9a6_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Same session-initiating `brokerd`-split instruction (see
|
||||
`20260610T170859Z_75cefe10_prompt_io.md`); this is the
|
||||
approved plan's "stage 3": the feed-layer hard cutover
|
||||
(user-decided: no dual-mode runtime flag, each stage
|
||||
lands fully working).
|
||||
|
||||
## Response summary
|
||||
|
||||
The topology flip: all data-feed consumers route to
|
||||
`datad.<broker>`; the `brokerd` lifetime fixture slims
|
||||
to trading-only; UI/CLI data-flavoured spawn sites
|
||||
repointed; test expectations inverted incl. an explicit
|
||||
negative assert that paper sessions spawn NO
|
||||
`brokerd.<broker>`.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/data/feed.py` — `open_feed()` ->
|
||||
`maybe_spawn_datad()`; `open_feed_bus()` actor-name
|
||||
assert -> `'datad'`; `install_brokerd_search` ->
|
||||
`install_datad_search`; comment sweep
|
||||
- `piker/brokers/_daemon.py` —
|
||||
`_setup_persistent_brokerd()` slimmed (feed-bus
|
||||
alloc moved to the datad fixture)
|
||||
- `piker/ui/_app.py`, `piker/ui/cli.py`,
|
||||
`piker/brokers/core.py`, `piker/brokers/cli.py`,
|
||||
`piker/ui/kivy/option_chain.py` — spawn-site
|
||||
repoints
|
||||
- `tests/test_services.py` — datad assertions + the
|
||||
no-`brokerd.kraken` negative check
|
||||
|
||||
## Human edits
|
||||
|
||||
None — committed as generated.
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T17:12:59Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171259Z_59d5d9a6_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry (code committed in the same
|
||||
commit as this log); backfilled from the live dev
|
||||
session transcript per the `/prompt-io` skill rules.
|
||||
|
||||
> `git log -1 -p --follow -- piker/data/feed.py`
|
||||
|
||||
Generated: `open_feed()` spawn cutover with the
|
||||
`maybe_spawn_datad` import done "relative-direct" from
|
||||
`._daemon` — NOT via `piker.service` — to dodge a
|
||||
partial-init cycle: when `piker.data.feed` loads as
|
||||
part of `piker.service.__init__` executing its own
|
||||
`piker.data._daemon` import, the service pkg is
|
||||
mid-init and its `maybe_spawn_datad` binding does not
|
||||
exist yet. The `open_feed_bus()` local-state sanity
|
||||
assert flips `'brokerd' in servicename` ->
|
||||
`'datad' in servicename` (the only actor-name assert
|
||||
in the tree, verified by grep).
|
||||
|
||||
> `git log -1 -p --follow -- piker/brokers/_daemon.py`
|
||||
|
||||
Generated: `_setup_persistent_brokerd()` slimmed to
|
||||
console-log setup + pinned-open ctx; drops the bus
|
||||
alloc, the `assert not feed._bus`, the service nursery
|
||||
(backend `open_trade_dialog()` ctxs own their task
|
||||
trees) and the `eg.ExceptionGroup` handler. The
|
||||
`piker ledger` ad-hoc actor enters this same slimmed
|
||||
fixture — exactly what it needs.
|
||||
|
||||
> `git log -1 -p --follow -- piker/ui/_app.py`
|
||||
> `git log -1 -p --follow -- piker/ui/cli.py`
|
||||
> `git log -1 -p --follow -- piker/brokers/core.py`
|
||||
> `git log -1 -p --follow -- piker/brokers/cli.py`
|
||||
> `git log -1 -p --follow -- piker/ui/kivy/option_chain.py`
|
||||
> `git log -1 -p --follow -- tests/test_services.py`
|
||||
|
||||
Verification (from session): per-suite gates green at
|
||||
this commit (services 5-passed incl. the new negative
|
||||
assert, feeds 3-passed); a headless live smoke
|
||||
(`maybe_open_pikerd` + `open_feed(['xbtusdt.kraken'])`
|
||||
on an alt registry port) confirmed quotes flowing via
|
||||
`datad.kraken` + `samplerd` with `check_for_service(
|
||||
'brokerd.kraken') is None`. Known pre-existing flake
|
||||
documented: `test_multi_fill_positions`' second
|
||||
in-proc runtime boot wedges ~50% (zombie subactor w/
|
||||
unread parent-IPC bytes); reproduced with the split
|
||||
fully reverted so NOT a regression of this work.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T17:13:44Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171344Z_eee19de0_prompt_io.md
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260610T171344Z_eee19de0_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Same session-initiating `brokerd`-split instruction (see
|
||||
`20260610T170859Z_75cefe10_prompt_io.md`); this is the
|
||||
approved plan's final "stage 4": caps-sec slimming of
|
||||
the (live, credentialed) trading actor + the `ib`
|
||||
dual-daemon `client_id` collision mitigation flagged in
|
||||
the plan's risk register.
|
||||
|
||||
## Response summary
|
||||
|
||||
`brokerd` loses ALL `piker.data.*` (feed) RPC mods;
|
||||
spawn fails fast for datad-only backends with a "use
|
||||
paper-mode" error; `ib`'s default api-gw `client_id`
|
||||
gets a per-daemon-kind offset so `datad.ib` +
|
||||
`brokerd.ib` don't collide on connect.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/brokers/_daemon.py` — `_data_mods` -> minimal
|
||||
`_brokerd_service_mods`; `broker_init()` reads
|
||||
`_brokerd_mods` (fallback `__enable_modules__`);
|
||||
`spawn_brokerd()` fail-fast via `validate.get_eps()`
|
||||
- `piker/brokers/ib/api.py` — role-based `client_id`
|
||||
offset in `load_aio_clients()`
|
||||
- `piker/cli/__init__.py` — resolved "expose datad"
|
||||
TODO
|
||||
|
||||
## Human edits
|
||||
|
||||
None — committed as generated.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T17:13:44Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171344Z_eee19de0_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry (code committed in the same
|
||||
commit as this log); backfilled from the live dev
|
||||
session transcript per the `/prompt-io` skill rules.
|
||||
|
||||
> `git log -1 -p --follow -- piker/brokers/_daemon.py`
|
||||
|
||||
Generated: the fail-fast originally landed in
|
||||
`broker_init()` but was relocated to `spawn_brokerd()`
|
||||
mid-implementation after realizing `piker ledger`
|
||||
calls `broker_init()` directly even for paper accounts
|
||||
on datad-only backends (would have crashed the cli);
|
||||
the service-spawn path is the correct enforcement
|
||||
seam. Error text:
|
||||
|
||||
Backend 'kucoin' offers NO `brokerd` (live
|
||||
order-control) eps!? It is likely a datad-only
|
||||
provider, use paper-mode for clearing instead.
|
||||
|
||||
(verified live via a `trio.run()` unit check.)
|
||||
|
||||
> `git log -1 -p --follow -- piker/brokers/ib/api.py`
|
||||
|
||||
Generated: in `load_aio_clients()`, when `client_id`
|
||||
is the 6116 default: `datad`-named actors offset +16
|
||||
(disjoint from `brokerd`'s linear `client_id + i`
|
||||
retry range), other non-`brokerd` (ad-hoc test/cli)
|
||||
actors +32. Rationale from the plan's risk register:
|
||||
post-split BOTH per-broker daemons connect to the same
|
||||
TWS/gw endpoint; a shared default id collides and
|
||||
burns up to `connect_timeout * retries` (90s) in
|
||||
retry cycles.
|
||||
|
||||
> `git log -1 -p --follow -- piker/cli/__init__.py`
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T17:33:09Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T173309Z_f15f8178_prompt_io.md
|
||||
scope: docs
|
||||
substantive: true
|
||||
raw_file: 20260610T173309Z_f15f8178_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
The session-initiating instruction, verbatim (this doc
|
||||
IS its "mega detailed plan" deliverable):
|
||||
|
||||
> ok i want you to become the distributed runtime and
|
||||
> concurrency expert for this project - namely acquire
|
||||
> a deep understanding of tractor and how it's used.
|
||||
> then i want you to atttempt to factor our current
|
||||
> brokerd service daemon into 2 daemons:
|
||||
>
|
||||
> - a brokerd which only hosts live/paper trading
|
||||
> endoint tasks as defined namely within all
|
||||
> piker/brokers/<backend>/broker.py mods
|
||||
> - a new `datad` subdaemon which in a separate
|
||||
> subactor serves all the data feed service tasks
|
||||
> namely delivered by endpoints in
|
||||
> piker/brokers/<backend>/feed.py (or similar for
|
||||
> less mature backends) and as more rigorously
|
||||
> defined by the validation machinery in
|
||||
> piker.data.validate.
|
||||
>
|
||||
> give me a mega detailed plan on how to approach
|
||||
> this, and a staged approach for the implementation.
|
||||
|
||||
Plan-shaping user decisions (in-session Q&A): sibling
|
||||
topology under `pikerd`; hard cutover staged by layer;
|
||||
trading-only EMS-lazy-spawned `brokerd`. The human
|
||||
staged the doc copy into `ai/claude-code/plans/` and
|
||||
requested this commit after the 7 implementation
|
||||
commits landed.
|
||||
|
||||
## Response summary
|
||||
|
||||
The staged design doc for the `brokerd` -> (`datad` +
|
||||
`brokerd`) split: context, target supervision topology,
|
||||
load-bearing verified facts, per-stage file-level
|
||||
changes with gates, risk register and an end-to-end
|
||||
verification matrix. Produced via 3 parallel explore
|
||||
agents + 1 architect agent + human Q&A before any code
|
||||
was written; all 7 implementation commits in this
|
||||
branch reference its stages in their provenance
|
||||
entries.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `ai/claude-code/plans/datad_service.md` — the design
|
||||
plan doc (human-staged copy of the AI-authored plan)
|
||||
|
||||
## Human edits
|
||||
|
||||
None to the doc content — committed as generated. NB:
|
||||
the implementation deviated from the plan-as-written in
|
||||
4 places, see the raw file's deviation log.
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T17:33:09Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T173309Z_f15f8178_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry; the AI output here is the
|
||||
committed doc itself (a design plan, not code) so no
|
||||
verbatim copy is duplicated:
|
||||
|
||||
> `git log -1 -p --follow -- ai/claude-code/plans/datad_service.md`
|
||||
|
||||
## Implementation deviation log
|
||||
|
||||
Where the landed 7-commit series differs from the plan
|
||||
as written (recorded for reviewer transparency; the
|
||||
plan doc is committed unmodified):
|
||||
|
||||
1. Commit ordering: the `pytest` config-dir isolation
|
||||
fix lands BEFORE the `tractor`-API drift port. The
|
||||
plan's stage gates assumed a runnable+isolated test
|
||||
baseline; per-commit gating exposed that without
|
||||
isolation the paper-EMS test reads the user's real
|
||||
(polluted) `account.kraken.paper.toml` and reddens.
|
||||
2. The plan's "stage 0: full pytest green" gate
|
||||
required first repairing pre-existing branch
|
||||
breakage vs `tractor` git `main` (boot
|
||||
`AttributeError`, stale discovery/exc/position
|
||||
APIs) — that repair became its own commit
|
||||
("Port service+tests to latest `tractor` APIs")
|
||||
rather than plan-stage work.
|
||||
3. Stage-4 fail-fast placement: the plan said
|
||||
`broker_init()` raises on brokerd-ep-less backends;
|
||||
implementation moved the raise to `spawn_brokerd()`
|
||||
since `piker ledger` calls `broker_init()` directly
|
||||
even for paper accounts on datad-only backends.
|
||||
4. `brokers/_daemon.py` change grouping: the
|
||||
import-cleanup hunks (`exceptiongroup`, `_FeedsBus`
|
||||
type-only import) landed with the caps-sec slim
|
||||
commit instead of the fixture-slim commit, keeping
|
||||
each intermediate tree import-clean without
|
||||
sub-hunk surgery.
|
||||
|
||||
Also of record: the plan's "Verification" matrix was
|
||||
executed as written (per-suite gates each stage, the
|
||||
headless datad-feed smoke, the kucoin fail-fast unit
|
||||
check); the known pre-existing ~50% second-runtime-boot
|
||||
test wedge was characterized and excluded as a
|
||||
regression via revert-testing.
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
|
||||
timestamp: 2026-06-10T21:35:49Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T213549Z_f084e899_prompt_io.md
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260610T213549Z_f084e899_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
User bug report from live `ib` testing of the
|
||||
(datad|brokerd)-split branch:
|
||||
|
||||
> ok doing some testing and noticing live orders do not
|
||||
> work otb, pikerd show this on live submission,
|
||||
> [pdb traceback: `RuntimeError("Can not order
|
||||
> {symbol}, no live feed?")` at `ib/api.py:1152`
|
||||
> `submit_limit()` with `self._contracts == {}` in the
|
||||
> `brokerd.ib` actor + a `TrioTaskExited` teardown
|
||||
> cascade]
|
||||
|
||||
Follow-up user direction fixing the design intent:
|
||||
|
||||
> i think the main thing here is that [brokerd] should
|
||||
> be able to always submit orders without a live feed
|
||||
> being up and registered in the same subactor right?
|
||||
|
||||
## Response summary
|
||||
|
||||
Root cause: pre-split the feed code populated the
|
||||
actor-local `Client._contracts` cache (via
|
||||
`get_mkt_info()` writing `mkt.bs_fqme` keys) in the
|
||||
SAME process as order submission; post-split the
|
||||
trading actor's client is never warmed. Fix: lazily
|
||||
qualify + cache the contract per order request in
|
||||
`handle_order_requests()` by running the same
|
||||
`get_mkt_info(fqme, proxy=...)` ep the feed side uses,
|
||||
plus per-order error relay (`BrokerdError`) so one bad
|
||||
submission can't crash the whole trades dialog (the
|
||||
`TrioTaskExited` storm was teardown cascade from the
|
||||
original raise).
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/brokers/ib/broker.py` — thread `proxies` into
|
||||
`handle_order_requests()`; lazy contract qualify on
|
||||
cache-miss; guard `submit_limit()` w/ `BrokerdError`
|
||||
relay; uncomment the (anticipatory) `get_mkt_info`
|
||||
import
|
||||
- `piker/brokers/ib/api.py` — fix the non-f-string
|
||||
raise msg + document the new qualification contract
|
||||
|
||||
## Human edits
|
||||
|
||||
None — committed as generated. Live `ib` order retest
|
||||
performed by the human post-commit.
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
model: claude-fable-5[1m]
|
||||
service: claude
|
||||
timestamp: 2026-06-10T21:35:49Z
|
||||
git_ref: datad_service
|
||||
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T213549Z_f084e899_prompt_io.md
|
||||
---
|
||||
|
||||
NOTE: diff-ref mode entry (code committed in the same
|
||||
commit as this log); recorded from the live debug
|
||||
session per the `/prompt-io` skill rules.
|
||||
|
||||
> `git log -1 -p --follow -- piker/brokers/ib/broker.py`
|
||||
> `git log -1 -p --follow -- piker/brokers/ib/api.py`
|
||||
|
||||
Key diagnostic chain (from session):
|
||||
|
||||
- pdb showed `Client._contracts == {}` inside
|
||||
`brokerd.ib`'s `submit_limit()`; the cache has
|
||||
exactly TWO write sites: `Client.find_contracts()`
|
||||
(api.py, keys `f'{sym}.{exch}.ib'`) and
|
||||
`symbols.get_mkt_info()` (key `mkt.bs_fqme`, eg.
|
||||
'nvda.nasdaq' — NO `.ib` suffix).
|
||||
- `BrokerdOrder.symbol` arrives as the bs_fqme form
|
||||
('nvda.nasdaq') so ONLY the `get_mkt_info()` write
|
||||
site produces the key `submit_limit()` reads —
|
||||
ie. pre-split it was the feed's in-proc
|
||||
`get_mkt_info(sym, proxy=proxy)` call keeping orders
|
||||
working, NOT `find_contracts()`.
|
||||
- the existing TODO at `symbols.py:642-644` literally
|
||||
predicted this: "this is going to be problematic
|
||||
if/when we split out the datad vs. brokerd actors
|
||||
since the mktmap lookup table will now be
|
||||
inaccessible.."
|
||||
- instance identity verified: `proxy._aio_ns` IS the
|
||||
same `Client` obj as `_accounts2clients[account]`
|
||||
(both sourced from the `load_aio_clients()` cache via
|
||||
`open_client_proxies()`), so a brokerd-side
|
||||
`get_mkt_info(fqme, proxy=proxies[account])` warms
|
||||
exactly the dict `submit_limit()` reads. It also
|
||||
populates `client._cons2mkts` which the
|
||||
position-audit path (`broker.py` backup-table code)
|
||||
needs in this actor anyway.
|
||||
- the `TrioTaskExited` storm in the user's log
|
||||
(`recv_trade_updates`, `open_aio_client_method_relay`
|
||||
aio tasks) is teardown cascade: the raise crashed
|
||||
`handle_order_requests` -> nursery teardown ripped
|
||||
the trio sides of still-running aio relay tasks.
|
||||
Hence the added per-order try/except ->
|
||||
`BrokerdError` relay hardening so a single bad
|
||||
submission degrades to an EMS error msg instead of
|
||||
killing the backend's entire order-ctl dialog.
|
||||
|
||||
Verification: `tests/test_services.py` (5 passed) +
|
||||
`tests/test_ems.py` (6 passed) regression-green; live
|
||||
`ib` submission retest delegated to the human (needs a
|
||||
running TWS/gw).
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# AI Prompt I/O Log — claude
|
||||
|
||||
This directory tracks prompt inputs and model
|
||||
outputs for AI-assisted development using
|
||||
`claude` (claude-code CLI).
|
||||
|
||||
## Policy
|
||||
|
||||
Prompt logging follows the
|
||||
[NLNet generative AI policy][nlnet-ai].
|
||||
All substantive AI contributions are logged
|
||||
with:
|
||||
- Model name and version
|
||||
- Timestamps
|
||||
- The prompts that produced the output
|
||||
- Unedited model output (`.raw.md` files)
|
||||
|
||||
[nlnet-ai]: https://nlnet.nl/foundation/policies/generativeAI/
|
||||
|
||||
## Usage
|
||||
|
||||
Entries are created by the `/prompt-io` skill
|
||||
or automatically via `/commit-msg` integration.
|
||||
|
||||
Each commit carrying AI-generated changes links
|
||||
to its provenance entry via a `Prompt-IO:`
|
||||
commit-msg trailer; entries use "diff-ref mode"
|
||||
(pointers into `git log -p` instead of verbatim
|
||||
code copies) to avoid duplicating committed
|
||||
code.
|
||||
|
||||
Human contributors remain accountable for all
|
||||
code decisions. AI-generated content is never
|
||||
presented as human-authored work.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: unavailable
|
||||
timestamp: 2026-08-13T20:24:02Z
|
||||
git_ref: 12b0d66e
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260813T202402Z_12b0d66e_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue the paired piker broadcast-consumer work after Tractor PR 490
|
||||
stabilization. Stop cached feed callers from mutating or consuming shared
|
||||
quote-stream roots, then review, verify and prepare a commit boundary.
|
||||
|
||||
## Response summary
|
||||
|
||||
Made provider root streams private to the cached `Feed`, gave every caller a
|
||||
local subscribed view, corrected feed cache identity, and made multi-provider
|
||||
relays own synchronized child subscriptions and independent send-channel
|
||||
clones.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/data/feed.py` - caller-local feed views, complete cache keys, and
|
||||
subscribed multi-provider relays with independent channel lifetimes.
|
||||
- `tests/test_feed_broadcast.py` - real broadcast ownership, lifecycle and
|
||||
cache-identity regressions, including sibling survival after provider EOC.
|
||||
|
||||
## Human edits
|
||||
|
||||
None - generated output follows the requested cached-feed ownership item.
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-13T20:24:02Z
|
||||
git_ref: 12b0d66e
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
The user asked to continue the paired piker broadcast-consumer work after
|
||||
Tractor PR 490 stabilization. The next item was to stop cached feed callers
|
||||
from mutating or consuming shared `Feed`/`Flume` quote-stream roots, then
|
||||
review, verify and prepare a commit boundary.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- piker/data/feed.py`
|
||||
|
||||
Changed `maybe_open_feed()` so the cached `Feed` exclusively owns provider
|
||||
root streams and every caller, including the first cache owner, receives a
|
||||
caller-local `Feed`/`Flume` view backed by one child subscription per provider.
|
||||
The cache identity now includes normalized full symbol shape and stream policy.
|
||||
|
||||
Changed `Feed.open_multi_stream()` so every provider relay subscribes instead
|
||||
of directly consuming a shared stream, and synchronizes relay startup before
|
||||
yielding the merged channel. Each relay owns a cloned send channel so one
|
||||
provider EOC cannot terminate its siblings. The module-level relay returns its
|
||||
live child receiver through `TaskStatus` for typed retention and introspection.
|
||||
Documented `Feed.pause()`/`Feed.resume()` as provider-context-wide controls
|
||||
shared by local cache users.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tests/test_feed_broadcast.py`
|
||||
|
||||
Added real `Feed`, `Flume`, and Tractor broadcast regressions covering both
|
||||
cache-miss owner and cache-hit paths, one-provider-many-market mapping,
|
||||
multi-provider relay fan-out and startup, duplex controls, lexical child
|
||||
closure, sibling survival after provider EOC, cached-root immutability, and
|
||||
full cache-key differentiation.
|
||||
|
||||
Verification output:
|
||||
|
||||
```text
|
||||
... [100%]
|
||||
3 passed in 0.02s
|
||||
|
||||
. [100%]
|
||||
1 passed in 0.01s
|
||||
```
|
||||
|
||||
The second result is the existing EMS broadcast ownership regression. Ruff,
|
||||
E501, compilation and whitespace checks passed. Repeated adversarial review
|
||||
found and resolved shared `Feed.streams`, incomplete cache-key, fake-lifecycle,
|
||||
relay-startup and first-owner asymmetry issues; final review reported no
|
||||
findings.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: unavailable
|
||||
timestamp: 2026-08-13T22:59:40Z
|
||||
git_ref: b691422b
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260813T225940Z_b691422b_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue the piker broadcast-consumer audit after the cached feed ownership
|
||||
commit and prepare the next smallest ownership fix.
|
||||
|
||||
## Response summary
|
||||
|
||||
Made every Deribit cached price-feed caller own a lexical child subscription
|
||||
from the retained `LinkedTaskChannel` broadcaster instead of constructing
|
||||
competing roots around one source channel.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/brokers/deribit/api.py` - uniform child subscription ownership for
|
||||
price-feed misses and cache hits.
|
||||
- `tests/test_deribit_broadcast.py` - concurrent fan-out and child lifetime
|
||||
regression.
|
||||
|
||||
## Human edits
|
||||
|
||||
None - generated output follows the requested broadcast-consumer audit.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-13T22:59:40Z
|
||||
git_ref: b691422b
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
The user asked to continue the piker broadcast-consumer audit after committing
|
||||
the cached feed ownership fix. The next isolated item was Deribit's cached
|
||||
price feed, which wrapped one retained `LinkedTaskChannel` in competing
|
||||
broadcaster roots for cache hits.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- piker/brokers/deribit/api.py`
|
||||
|
||||
Changed `maybe_open_price_feed()` so both the first cache owner and every cache
|
||||
hit enter `LinkedTaskChannel.subscribe()` and receive a lexical child from the
|
||||
channel's single retained broadcaster root.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tests/test_deribit_broadcast.py`
|
||||
|
||||
Added a concurrent ownership regression using real Tractor broadcast state.
|
||||
It proves a miss owner and cache-hit caller receive the same quote, closing the
|
||||
hit child leaves the owner live, and both children clean up independently while
|
||||
the channel root remains registered.
|
||||
|
||||
Verification output:
|
||||
|
||||
```text
|
||||
. [100%]
|
||||
1 passed in 0.01s
|
||||
```
|
||||
|
||||
The new test passes through a source-isolated Deribit import. Test Ruff,
|
||||
Python compilation and whitespace checks pass. Full package import and
|
||||
full-file Deribit Ruff remain blocked by pre-existing Qt, msgspec model, and
|
||||
legacy Deribit lint errors unrelated to this patch.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: unavailable
|
||||
timestamp: 2026-08-13T23:07:44Z
|
||||
git_ref: ddac3553
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260813T230744Z_ddac3553_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue the piker broadcast-consumer audit after the Deribit ownership commit
|
||||
and prepare the next smallest ownership fix.
|
||||
|
||||
## Response summary
|
||||
|
||||
Replaced IB's manual raw quote-channel registry with a symbol-keyed cached
|
||||
context and gave every caller a lexical child from one retained
|
||||
`LinkedTaskChannel` broadcaster root.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/brokers/ib/feed.py` - cached raw-channel lifetime and lexical child
|
||||
ownership.
|
||||
- `tests/test_ib_broadcast.py` - concurrent fan-out, cache-key and child
|
||||
lifetime regression.
|
||||
|
||||
## Human edits
|
||||
|
||||
None - generated output follows the requested broadcast-consumer audit.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-13T23:07:44Z
|
||||
git_ref: ddac3553
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
The user asked to continue the piker broadcast-consumer audit after committing
|
||||
the Deribit ownership fix. The next isolated item was IB's actor-local
|
||||
`_quote_streams` cache, which retained a raw channel owned by its first caller
|
||||
and constructed competing broadcaster roots for later callers.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- piker/brokers/ib/feed.py`
|
||||
|
||||
Factored `_open_aio_quote_channel()` as the raw asyncio-channel context and
|
||||
made `open_aio_quote_stream()` cache that context by symbol through
|
||||
`maybe_open_context()`. Every caller now receives a lexical child from the
|
||||
retained `LinkedTaskChannel` broadcaster, while the raw source remains alive
|
||||
until its final cache user exits. Removed the manual `_quote_streams` registry
|
||||
and teardown mutation.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tests/test_ib_broadcast.py`
|
||||
|
||||
Added a concurrent ownership regression using real Tractor broadcast state.
|
||||
It proves miss and hit callers use the same symbol/context factory, receive the
|
||||
same quote through distinct children, close independently, and leave the owner
|
||||
live after the hit exits.
|
||||
|
||||
Verification output:
|
||||
|
||||
```text
|
||||
. [100%]
|
||||
1 passed in 0.01s
|
||||
```
|
||||
|
||||
The new test passes through a source-isolated IB feed import. Test Ruff, E501,
|
||||
Python compilation and whitespace checks pass. Normal package collection
|
||||
remains blocked by the existing Qt environment mismatch.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: unavailable
|
||||
timestamp: 2026-09-07T21:40:00Z
|
||||
git_ref: 9115d24
|
||||
scope: config
|
||||
substantive: true
|
||||
raw_file: 20260907T214000Z_9115d24_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Deploy the provider-neutral `.agents` skill tree for Codex first and
|
||||
then migrate Piker's workflow state without losing archived helpers.
|
||||
|
||||
## Response summary
|
||||
|
||||
Deployed 21 ignored `.agents/skills` links, copied 132 legacy runtime
|
||||
files into `.ai/state`, selected the neutral backend, and preserved the
|
||||
original archive and staged `ruff.toml` change.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `.gitignore` - ignore shared links and generated neutral state.
|
||||
- `.ai/workflow-state.json` - select the neutral backend.
|
||||
- `.ai/commit-msg/style-guide-reference.md` - port commit guidance.
|
||||
- `.ai/run-tests/test-harness-reference.md` - port test guidance.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human prioritized immediate Codex deployment and directed helper
|
||||
archives to migrate as opaque runtime data instead of blocking the
|
||||
operation. No direct source-line edits were made by the human.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-09-07T21:40:00Z
|
||||
git_ref: 9115d24
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
Deploy all shared `ai.skillz` skills through `.agents/skills` for
|
||||
Codex, then migrate Piker's generated workflow state from the legacy
|
||||
Claude layout to provider-neutral `.ai/state` storage.
|
||||
|
||||
Preserve every legacy source and the existing staged `ruff.toml`
|
||||
change. Commit only the neutral backend selection, repository guidance,
|
||||
ignore rules, and this provenance record.
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: unavailable
|
||||
timestamp: 2026-09-08T04:31:34Z
|
||||
git_ref: f148fbcc
|
||||
scope: config
|
||||
substantive: true
|
||||
raw_file: 20260908T043134Z_f148fbcc_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Audit tracked legacy skill files under `.claude/`, then migrate Piker's
|
||||
repository-specific skills into the provider-neutral discovery tree.
|
||||
Keep Claude compatibility and remove stale Claude-local configuration.
|
||||
|
||||
## Response summary
|
||||
|
||||
Moves four Piker-specific skill packages to `.agents/skills/`, retains
|
||||
Claude discovery through relative adapter links, and drops two stale
|
||||
tracked Claude-local files.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `.agents/skills/piker-*/` - provider-neutral Piker skill sources.
|
||||
- `.claude/skills/piker-*` - relative Claude compatibility links.
|
||||
- `.claude/settings.local.json` - remove tracked local permissions.
|
||||
- `.claude/skills/commit-msg/style-guide-reference.md` - remove the
|
||||
superseded legacy copy now represented under `.ai/commit-msg/`.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human identified that repository-specific skills were not migrated
|
||||
by the shared `ai.skillz` deployment, selected `.agents/skills/` as the
|
||||
neutral source, and requested that the migration land in PR 93 before
|
||||
following up on the generic migration tooling.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-09-08T04:31:34Z
|
||||
git_ref: f148fbcc
|
||||
diff_cmd: git diff --cached
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Audit tracked legacy skill files under `.claude/`, then migrate Piker's
|
||||
repository-specific skills into the provider-neutral discovery tree.
|
||||
Keep Claude compatibility and remove stale Claude-local configuration.
|
||||
|
||||
## Generated changes
|
||||
|
||||
> `git diff --cached -- .agents/skills/`
|
||||
|
||||
Moves the four Piker-owned skill packages into the neutral
|
||||
`.agents/skills/` source tree without changing their contents.
|
||||
|
||||
> `git diff --cached -- .claude/skills/ .claude/settings.local.json`
|
||||
|
||||
Adds relative Claude adapter links to the neutral skill sources and
|
||||
removes the tracked local settings file and superseded commit-message
|
||||
style guide.
|
||||
|
||||
## Validation
|
||||
|
||||
- All four Claude adapter links resolve to a `SKILL.md`.
|
||||
- `git diff --cached --check` passes.
|
||||
- No tracked files match the repository's ignore rules.
|
||||
- Shared deployment validation retains one unrelated stale local
|
||||
`commit-plan` hybrid-link error for an `ai.skillz` follow-up.
|
||||
|
|
@ -32,7 +32,14 @@ option.log.disabled = true
|
|||
|
||||
|
||||
[kraken]
|
||||
key_descr = ''
|
||||
# the reference fiat asset as can be set
|
||||
# in an account's web-trading-UI prefs.
|
||||
src_fiat = 'usd'
|
||||
|
||||
# NOTE for account defs, the following
|
||||
# lines must match as follows.
|
||||
accounts.spot = 'spot'
|
||||
key_descr = 'spot'
|
||||
api_key = ''
|
||||
secret = ''
|
||||
# ------ kraken ------
|
||||
|
|
|
|||
|
|
@ -1,8 +1,26 @@
|
|||
[network]
|
||||
pikerd = [
|
||||
'/ipv4/127.0.0.1/tcp/6116', # std localhost daemon-actor tree
|
||||
# '/uds/6116', # TODO std uds socket file
|
||||
# TCP localhost loopback
|
||||
'/ip4/127.0.0.1/tcp/6116',
|
||||
|
||||
# same but UDS
|
||||
'/unix/run/user/1000/piker/pikerd.sock',
|
||||
]
|
||||
chart = [
|
||||
'/ip4/127.0.0.1/tcp/3003',
|
||||
'/unix/run/user/1000/piker/chart.sock',
|
||||
]
|
||||
# the service-actor registry endpoint;
|
||||
# other pikerd trees contact this to discover
|
||||
# actors.
|
||||
# XXX if absent, pikerd binds the registry
|
||||
# on its own tpt_bind_addrs.
|
||||
# regd = ['/ip4/127.0.0.1/tcp/6116']
|
||||
|
||||
# chart = [
|
||||
# '/ip4/127.0.0.1/tcp/3333',
|
||||
# '/unix/run/user/1000/piker/chart@3333.sock',
|
||||
# ]
|
||||
|
||||
|
||||
[ui]
|
||||
|
|
|
|||
|
|
@ -324,10 +324,13 @@ def load_ledger(
|
|||
ldir: Path = (
|
||||
dirpath
|
||||
or
|
||||
config._config_dir / 'accounting' / 'ledgers'
|
||||
config.get_conf_dir() / 'accounting' / 'ledgers'
|
||||
)
|
||||
if not ldir.is_dir():
|
||||
ldir.mkdir()
|
||||
ldir.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
fname = f'trades_{brokername}_{acctid}.toml'
|
||||
fpath: Path = ldir / fname
|
||||
|
|
|
|||
|
|
@ -785,9 +785,16 @@ def load_account(
|
|||
legacy_fn: str = f'pps.{brokername}.{acctid}.toml'
|
||||
fn: str = f'account.{brokername}.{acctid}.toml'
|
||||
|
||||
dirpath: Path = dirpath or (config._config_dir / 'accounting')
|
||||
dirpath: Path = (
|
||||
dirpath
|
||||
or
|
||||
(config.get_conf_dir() / 'accounting')
|
||||
)
|
||||
if not dirpath.is_dir():
|
||||
dirpath.mkdir()
|
||||
dirpath.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
conf, path = config.load(
|
||||
path=dirpath / fn,
|
||||
|
|
|
|||
|
|
@ -143,12 +143,15 @@ def sync(
|
|||
# (what the EMS normally does internall) B)
|
||||
open_brokerd_dialog(
|
||||
brokermod,
|
||||
portal,
|
||||
exec_mode=(
|
||||
'paper'
|
||||
if account == 'paper'
|
||||
else 'live'
|
||||
),
|
||||
# use our own ad-hoc-spawned actor,
|
||||
# do NOT (spawn and) use the
|
||||
# `brokerd.<broker>` service daemon!
|
||||
portal=portal,
|
||||
loglevel=loglevel,
|
||||
) as (
|
||||
brokerd_stream,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ from piker.log import (
|
|||
from ._util import (
|
||||
BrokerError,
|
||||
SymbolNotFound,
|
||||
MarketNotFound as MarketNotFound,
|
||||
NoData,
|
||||
DataUnavailable,
|
||||
DataThrottle,
|
||||
|
|
|
|||
|
|
@ -25,10 +25,8 @@ from contextlib import (
|
|||
)
|
||||
from types import ModuleType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
AsyncContextManager,
|
||||
)
|
||||
import exceptiongroup as eg
|
||||
|
||||
import tractor
|
||||
import trio
|
||||
|
|
@ -40,27 +38,20 @@ from piker.log import (
|
|||
from . import _util
|
||||
from . import get_brokermod
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..data import _FeedsBus
|
||||
|
||||
log = get_logger(name=__name__)
|
||||
|
||||
# `brokerd` enabled modules
|
||||
# TODO: move this def to the `.data` subpkg..
|
||||
# `brokerd`-actor-always-enabled mods.
|
||||
# NOTE: keeping this list as small as possible is part of our caps-sec
|
||||
# model and should be treated with utmost care!
|
||||
_data_mods: str = [
|
||||
'piker.brokers.core',
|
||||
'piker.brokers.data',
|
||||
# model and should be treated with utmost care! In particular NO
|
||||
# `piker.data.*` feed mods should be enabled in this (live,
|
||||
# credentialed) trading actor; all data-feed serving is the
|
||||
# domain of the `datad.<broker>` sibling daemon, see
|
||||
# `piker.data._daemon._datad_service_mods`.
|
||||
_brokerd_service_mods: list[str] = [
|
||||
'piker.brokers._daemon',
|
||||
'piker.data',
|
||||
'piker.data.feed',
|
||||
'piker.data._sampling'
|
||||
]
|
||||
|
||||
|
||||
# TODO: we should rename the daemon to datad prolly once we split up
|
||||
# broker vs. data tasks into separate actors?
|
||||
@tractor.context
|
||||
async def _setup_persistent_brokerd(
|
||||
ctx: tractor.Context,
|
||||
|
|
@ -69,9 +60,15 @@ async def _setup_persistent_brokerd(
|
|||
|
||||
) -> None:
|
||||
'''
|
||||
Allocate a actor-wide service nursery in ``brokerd``
|
||||
such that feeds can be run in the background persistently by
|
||||
the broker backend as needed.
|
||||
Trading-only daemon (lifetime) fixture: console logging
|
||||
setup and a pinned-open context for service mgmt.
|
||||
|
||||
All data-feed-bus state now lives in the (data-feed-only)
|
||||
`datad.<brokername>` sibling daemon, see
|
||||
`piker.data._daemon._setup_persistent_datad()`; this
|
||||
actor hosts only the backend's `open_trade_dialog()`
|
||||
(live order-control) ep-task(s) which manage their own
|
||||
task trees per `tractor.Context`.
|
||||
|
||||
'''
|
||||
# NOTE: we only need to setup logging once (and only) here
|
||||
|
|
@ -87,47 +84,13 @@ async def _setup_persistent_brokerd(
|
|||
)
|
||||
assert log.name == _util.subsys
|
||||
|
||||
# further, set the log level on any broker broker specific
|
||||
# logger instance.
|
||||
|
||||
from piker.data import feed
|
||||
assert not feed._bus
|
||||
|
||||
# allocate a nursery to the bus for spawning background
|
||||
# tasks to service client IPC requests, normally
|
||||
# `tractor.Context` connections to explicitly required
|
||||
# `brokerd` endpoints such as:
|
||||
# - `stream_quotes()`,
|
||||
# - `manage_history()`,
|
||||
# - `allocate_persistent_feed()`,
|
||||
# - `open_symbol_search()`
|
||||
# NOTE: see ep invocation details inside `.data.feed`.
|
||||
try:
|
||||
async with (
|
||||
# tractor.trionics.collapse_eg(),
|
||||
trio.open_nursery() as service_nursery
|
||||
):
|
||||
bus: _FeedsBus = feed.get_feed_bus(
|
||||
brokername,
|
||||
service_nursery,
|
||||
)
|
||||
assert bus is feed._bus
|
||||
|
||||
# unblock caller
|
||||
await ctx.started()
|
||||
|
||||
# we pin this task to keep the feeds manager active until the
|
||||
# we pin this task to keep the daemon active until the
|
||||
# parent actor decides to tear it down
|
||||
await trio.sleep_forever()
|
||||
|
||||
except eg.ExceptionGroup:
|
||||
# TODO: likely some underlying `brokerd` IPC connection
|
||||
# broke so here we handle a respawn and re-connect attempt!
|
||||
# This likely should pair with development of the OCO task
|
||||
# nusery in dev over @ `tractor` B)
|
||||
# https://github.com/goodboy/tractor/pull/363
|
||||
raise
|
||||
|
||||
|
||||
def broker_init(
|
||||
brokername: str,
|
||||
|
|
@ -147,8 +110,10 @@ def broker_init(
|
|||
|
||||
This includes:
|
||||
- load the appropriate <brokername>.py pkg module,
|
||||
- reads any declared `__enable_modules__: listr[str]` which will be
|
||||
passed to `tractor.ActorNursery.start_actor(enabled_modules=<this>)`
|
||||
- reads any declared `_brokerd_mods: list[str]` (falling
|
||||
back to the full `__enable_modules__` set for
|
||||
not-yet-split backends) which will be passed to
|
||||
`tractor.ActorNursery.start_actor(enable_modules=)`
|
||||
at actor start time,
|
||||
- deliver a references to the daemon lifetime fixture, which
|
||||
for now is always the `_setup_persistent_brokerd()` context defined
|
||||
|
|
@ -182,9 +147,15 @@ def broker_init(
|
|||
modpath,
|
||||
]
|
||||
for submodname in getattr(
|
||||
brokermod,
|
||||
'_brokerd_mods',
|
||||
# fallback for (flat, less mature) backends which
|
||||
# don't yet declare a daemon-kind mod split.
|
||||
getattr(
|
||||
brokermod,
|
||||
'__enable_modules__',
|
||||
[],
|
||||
),
|
||||
):
|
||||
subpath: str = f'{modpath}.{submodname}'
|
||||
enabled.append(subpath)
|
||||
|
|
@ -212,6 +183,22 @@ async def spawn_brokerd(
|
|||
f'backend: {brokername!r}'
|
||||
)
|
||||
|
||||
# fail fast on (data-only) backends which don't offer
|
||||
# ANY live order-control eps; the caller should instead
|
||||
# be using paper-mode (and thus never spawning us)!
|
||||
from ..data.validate import get_eps
|
||||
brokerd_eps: dict = get_eps(
|
||||
get_brokermod(brokername),
|
||||
'brokerd',
|
||||
)
|
||||
if not brokerd_eps:
|
||||
raise RuntimeError(
|
||||
f'Backend {brokername!r} offers NO `brokerd` '
|
||||
f'(live order-control) eps!?\n'
|
||||
f'It is likely a datad-only provider, use '
|
||||
f'paper-mode for clearing instead.\n'
|
||||
)
|
||||
|
||||
(
|
||||
brokermode,
|
||||
tractor_kwargs,
|
||||
|
|
@ -233,7 +220,11 @@ async def spawn_brokerd(
|
|||
dname: str = tractor_kwargs.pop('name') # f'brokerd.{brokername}'
|
||||
portal = await Services.actor_n.start_actor(
|
||||
dname,
|
||||
enable_modules=_data_mods + tractor_kwargs.pop('enable_modules'),
|
||||
enable_modules=list(dict.fromkeys(
|
||||
_brokerd_service_mods
|
||||
+
|
||||
tractor_kwargs.pop('enable_modules')
|
||||
)),
|
||||
debug_mode=Services.debug_mode,
|
||||
**tractor_kwargs
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,17 @@ Handy cross-broker utils.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
# from functools import partial
|
||||
from typing import (
|
||||
Type,
|
||||
)
|
||||
|
||||
import json
|
||||
import httpx
|
||||
import logging
|
||||
from msgspec import Struct
|
||||
from tractor._exceptions import (
|
||||
reg_err_types,
|
||||
)
|
||||
|
||||
from piker.log import (
|
||||
colorize_json,
|
||||
|
|
@ -59,6 +66,10 @@ class SymbolNotFound(BrokerError):
|
|||
"Symbol not found by broker search"
|
||||
|
||||
|
||||
class MarketNotFound(SymbolNotFound):
|
||||
"Mkt-pair not found by broker search"
|
||||
|
||||
|
||||
# TODO: these should probably be moved to `.tsp/.data`?
|
||||
class NoData(BrokerError):
|
||||
'''
|
||||
|
|
@ -97,6 +108,19 @@ class DataThrottle(BrokerError):
|
|||
'''
|
||||
# TODO: add in throttle metrics/feedback
|
||||
|
||||
class SchemaMismatch(BrokerError):
|
||||
'''
|
||||
Market `Pair` fields mismatch, likely due to provider API update.
|
||||
|
||||
'''
|
||||
|
||||
# auto-register all `BrokerError` subtypes for
|
||||
# tractor IPC exc-marshalling.
|
||||
reg_err_types([
|
||||
BrokerError,
|
||||
*BrokerError.__subclasses__(),
|
||||
])
|
||||
|
||||
|
||||
def resproc(
|
||||
resp: httpx.Response,
|
||||
|
|
@ -123,3 +147,45 @@ def resproc(
|
|||
log.debug(f"Received json contents:\n{colorize_json(msg)}")
|
||||
|
||||
return msg if return_json else resp
|
||||
|
||||
|
||||
def get_or_raise_on_pair_schema_mismatch(
|
||||
pair_type: Type[Struct],
|
||||
fields_data: dict,
|
||||
provider_name: str,
|
||||
api_url: str|None = None,
|
||||
) -> Struct:
|
||||
'''
|
||||
Boilerplate helper around assset-`Pair` field schema mismatches,
|
||||
normally due to provider API updates.
|
||||
|
||||
'''
|
||||
try:
|
||||
pair: Struct = pair_type(**fields_data)
|
||||
return pair
|
||||
except TypeError as err:
|
||||
|
||||
from tractor.devx.pformat import ppfmt
|
||||
repr_data: str = ppfmt(fields_data)
|
||||
report: str = (
|
||||
f'Field mismatch we need to codify!\n'
|
||||
f'\n'
|
||||
f'{pair_type!r}({repr_data})'
|
||||
f'\n'
|
||||
f'^^^ {err.args[0]!r} ^^^\n'
|
||||
f'\n'
|
||||
f"Don't panic, prolly {provider_name!r} "
|
||||
f"changed their symbology schema..\n"
|
||||
)
|
||||
if (
|
||||
api_url
|
||||
or
|
||||
(api_url := pair_type._api_url)
|
||||
):
|
||||
report += (
|
||||
f'\n'
|
||||
f'Check out their API docs here:\n'
|
||||
f'{api_url}\n'
|
||||
)
|
||||
|
||||
raise SchemaMismatch(report) from err
|
||||
|
|
|
|||
|
|
@ -52,9 +52,25 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
# `brokerd` modules
|
||||
__enable_modules__: list[str] = [
|
||||
# per-daemon-kind (sub)mod groups: declares which of our
|
||||
# submods host the eps run by each daemon-actor kind as
|
||||
# defined by `piker.data.validate._eps`.
|
||||
# NOTE: `get_mkt_info` and `open_symbol_search` both live
|
||||
# in `.feed` for this backend (no `symbols.py`).
|
||||
_brokerd_mods: list[str] = [
|
||||
'api',
|
||||
'feed',
|
||||
'broker',
|
||||
]
|
||||
|
||||
_datad_mods: list[str] = [
|
||||
'api',
|
||||
'feed',
|
||||
]
|
||||
|
||||
|
||||
# tractor RPC enable arg
|
||||
__enable_modules__: list[str] = list(dict.fromkeys(
|
||||
_brokerd_mods
|
||||
+
|
||||
_datad_mods
|
||||
))
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ from piker import config
|
|||
from piker.clearing._messages import (
|
||||
Order,
|
||||
)
|
||||
from piker.brokers._util import (
|
||||
get_or_raise_on_pair_schema_mismatch,
|
||||
)
|
||||
from piker.accounting import (
|
||||
Asset,
|
||||
digits_to_dec,
|
||||
|
|
@ -370,20 +373,12 @@ class Client:
|
|||
item['filters'] = filters
|
||||
|
||||
pair_type: Type = PAIRTYPES[venue]
|
||||
try:
|
||||
pair: Pair = pair_type(**item)
|
||||
except Exception as e:
|
||||
e.add_note(
|
||||
f'\n'
|
||||
f'New or removed field we need to codify!\n'
|
||||
f'pair-type: {pair_type!r}\n'
|
||||
f'\n'
|
||||
f"Don't panic, prolly stupid binance changed their symbology schema again..\n"
|
||||
f'Check out their API docs here:\n'
|
||||
f'\n'
|
||||
f'https://binance-docs.github.io/apidocs/spot/en/#exchange-information\n'
|
||||
pair: Pair = get_or_raise_on_pair_schema_mismatch(
|
||||
pair_type=pair_type,
|
||||
fields_data=item,
|
||||
provider_name='binance',
|
||||
api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information',
|
||||
)
|
||||
raise
|
||||
pair_table[pair.symbol.upper()] = pair
|
||||
|
||||
# update an additional top-level-cross-venue-table
|
||||
|
|
@ -581,8 +576,8 @@ class Client:
|
|||
self,
|
||||
mkt: MktPair,
|
||||
|
||||
start_dt: datetime | None = None,
|
||||
end_dt: datetime | None = None,
|
||||
start_dt: datetime|None = None,
|
||||
end_dt: datetime|None = None,
|
||||
|
||||
as_np: bool = True,
|
||||
|
||||
|
|
@ -609,7 +604,11 @@ class Client:
|
|||
start_time = binance_timestamp(start_dt)
|
||||
end_time = binance_timestamp(end_dt)
|
||||
|
||||
bs_pair: Pair = self._pairs[mkt.bs_fqme.upper()]
|
||||
import tractor
|
||||
with tractor.devx.maybe_open_crash_handler():
|
||||
bs_pair: Pair = self._pairs[
|
||||
mkt.bs_fqme.upper()
|
||||
]
|
||||
|
||||
# https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data
|
||||
bars = await self._api(
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import tractor
|
|||
from piker.brokers import (
|
||||
open_cached_client,
|
||||
NoData,
|
||||
MarketNotFound,
|
||||
)
|
||||
from piker._cacheables import (
|
||||
async_lifo_cache,
|
||||
|
|
@ -203,9 +204,13 @@ async def stream_messages(
|
|||
yield 'trade', piker_quote
|
||||
|
||||
|
||||
def make_sub(pairs: list[str], sub_name: str, uid: int) -> dict[str, str]:
|
||||
def make_sub(
|
||||
pairs: list[str],
|
||||
sub_name: str,
|
||||
uid: int,
|
||||
) -> dict[str, str]:
|
||||
'''
|
||||
Create a request subscription packet dict.
|
||||
Create a request subscription packet `dict`.
|
||||
|
||||
- spot:
|
||||
https://binance-docs.github.io/apidocs/spot/en/#live-subscribing-unsubscribing-to-streams
|
||||
|
|
@ -301,6 +306,10 @@ async def get_mkt_info(
|
|||
|
||||
# uppercase since kraken bs_mktid is always upper
|
||||
if 'binance' not in fqme.lower():
|
||||
log.warning(
|
||||
f'Missing `.<provider>` part in fqme ??\n'
|
||||
f'fqme: {fqme!r}\n'
|
||||
)
|
||||
fqme += '.binance'
|
||||
|
||||
mkt_mode: str = ''
|
||||
|
|
@ -315,6 +324,24 @@ async def get_mkt_info(
|
|||
venue: str = venue.upper()
|
||||
venue_lower: str = venue.lower()
|
||||
|
||||
if not venue:
|
||||
if expiry:
|
||||
expiry = f'.{expiry}'
|
||||
expected: str = (
|
||||
f'{mkt_ep}'
|
||||
f'.<venue>'
|
||||
f'{expiry}'
|
||||
f'.{broker}'
|
||||
)
|
||||
raise MarketNotFound(
|
||||
f'Invalid or missing .<venue> part in fqme?\n'
|
||||
f'\n'
|
||||
f'fqme: {fqme!r}\n'
|
||||
f'expected-form>> {expected}\n'
|
||||
f'\n'
|
||||
f'Maybe you are missing a ".spot." ?\n'
|
||||
)
|
||||
|
||||
# XXX TODO: we should change the usdtm_futes name to just
|
||||
# usdm_futes (dropping the tether part) since it turns out that
|
||||
# there are indeed USD-tokens OTHER THEN tether being used as
|
||||
|
|
@ -332,7 +359,8 @@ async def get_mkt_info(
|
|||
# TODO: handle coinm futes which have a margin asset that
|
||||
# is some crypto token!
|
||||
# https://binance-docs.github.io/apidocs/delivery/en/#exchange-information
|
||||
or 'btc' in venue_lower
|
||||
or
|
||||
'btc' in venue_lower
|
||||
):
|
||||
return None
|
||||
|
||||
|
|
@ -343,16 +371,21 @@ async def get_mkt_info(
|
|||
|
||||
if (
|
||||
venue
|
||||
and 'spot' not in venue_lower
|
||||
and
|
||||
'spot' not in venue_lower
|
||||
|
||||
# XXX: catch all in case user doesn't know which
|
||||
# venue they want (usdtm vs. coinm) and we can choose
|
||||
# a default (via config?) once we support coin-m APIs.
|
||||
or 'perp' in venue_lower
|
||||
or
|
||||
'perp' in venue_lower
|
||||
):
|
||||
if not mkt_mode:
|
||||
mkt_mode: str = f'{venue_lower}_futes'
|
||||
|
||||
# tracing
|
||||
# await tractor.pause()
|
||||
|
||||
async with open_cached_client(
|
||||
'binance',
|
||||
) as client:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ Per market data-type definitions and schemas types.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
ClassVar,
|
||||
Literal,
|
||||
)
|
||||
from decimal import Decimal
|
||||
|
|
@ -203,6 +204,8 @@ class FutesPair(Pair):
|
|||
# NOTE: see `.data._symcache.SymbologyCache.load()` for why
|
||||
ns_path: str = 'piker.brokers.binance:FutesPair'
|
||||
|
||||
_api_url: ClassVar[str] = 'https://binance-docs.github.io/apidocs/spot/en/#exchange-information'
|
||||
|
||||
# NOTE: for compat with spot pairs and `MktPair.src: Asset`
|
||||
# processing..
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from piker.log import (
|
|||
get_logger,
|
||||
)
|
||||
from ..service import (
|
||||
maybe_spawn_brokerd,
|
||||
maybe_spawn_datad,
|
||||
maybe_open_pikerd,
|
||||
)
|
||||
from ..brokers import (
|
||||
|
|
@ -187,7 +187,7 @@ def brokercheck(config, broker):
|
|||
|
||||
'''
|
||||
async def bcheck_main():
|
||||
async with maybe_spawn_brokerd(broker) as portal:
|
||||
async with maybe_spawn_datad(broker) as portal:
|
||||
await portal.run(run_test, broker)
|
||||
await portal.cancel_actor()
|
||||
|
||||
|
|
@ -317,7 +317,7 @@ def record(config, rate, name, dhost, filename):
|
|||
return
|
||||
|
||||
async def main(tries):
|
||||
async with maybe_spawn_brokerd(
|
||||
async with maybe_spawn_datad(
|
||||
tries=tries, loglevel=loglevel
|
||||
) as portal:
|
||||
# run app "main"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import trio
|
|||
|
||||
from piker.log import get_logger
|
||||
from . import get_brokermod
|
||||
from ..service import maybe_spawn_brokerd
|
||||
from ..service import maybe_spawn_datad
|
||||
from . import open_cached_client
|
||||
from ..accounting import MktPair
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ async def symbol_search(
|
|||
# await tractor.devx._debug.maybe_init_greenback()
|
||||
# tractor.pause_from_sync()
|
||||
|
||||
async with maybe_spawn_brokerd(
|
||||
async with maybe_spawn_datad(
|
||||
mod.name,
|
||||
infect_asyncio=getattr(
|
||||
mod,
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ class DataFeed:
|
|||
async def stream_to_file(
|
||||
watchlist_name: str,
|
||||
filename: str,
|
||||
portal: tractor._portal.Portal,
|
||||
portal: tractor.Portal,
|
||||
tickers: List[str],
|
||||
brokermod: ModuleType,
|
||||
rate: int,
|
||||
|
|
|
|||
|
|
@ -47,13 +47,25 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
# tractor RPC enable arg
|
||||
__enable_modules__: list[str] = [
|
||||
# per-daemon-kind (sub)mod groups: declares which of our
|
||||
# submods host the eps run by each daemon-actor kind as
|
||||
# defined by `piker.data.validate._eps`.
|
||||
# NOTE: datad-only backend (no `broker.py` yet)!
|
||||
_brokerd_mods: list[str] = []
|
||||
|
||||
_datad_mods: list[str] = [
|
||||
'api',
|
||||
'feed',
|
||||
# 'broker',
|
||||
]
|
||||
|
||||
|
||||
# tractor RPC enable arg
|
||||
__enable_modules__: list[str] = list(dict.fromkeys(
|
||||
_brokerd_mods
|
||||
+
|
||||
_datad_mods
|
||||
))
|
||||
|
||||
# passed to ``tractor.ActorNursery.start_actor()``
|
||||
_spawn_kwargs = {
|
||||
'infect_asyncio': True,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ from contextlib import (
|
|||
asynccontextmanager as acm,
|
||||
)
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import time
|
||||
from typing import (
|
||||
Any,
|
||||
|
|
@ -37,8 +36,9 @@ from trio_typing import TaskStatus
|
|||
from rapidfuzz import process as fuzzy
|
||||
import numpy as np
|
||||
from tractor.trionics import (
|
||||
BroadcastReceiver,
|
||||
broadcast_receiver,
|
||||
maybe_open_context
|
||||
maybe_open_context,
|
||||
collapse_eg,
|
||||
)
|
||||
from tractor import to_asyncio
|
||||
|
|
@ -524,13 +524,12 @@ async def maybe_open_feed_handler() -> trio.abc.ReceiveStream:
|
|||
|
||||
|
||||
async def aio_price_feed_relay(
|
||||
chan: to_asyncio.LinkedTaskChannel,
|
||||
fh: FeedHandler,
|
||||
instrument: Symbol,
|
||||
from_trio: asyncio.Queue,
|
||||
to_trio: trio.abc.SendChannel,
|
||||
) -> None:
|
||||
async def _trade(data: dict, receipt_timestamp):
|
||||
to_trio.send_nowait(('trade', {
|
||||
chan.send_nowait(('trade', {
|
||||
'symbol': cb_sym_to_deribit_inst(
|
||||
str_to_cb_sym(data.symbol)).lower(),
|
||||
'last': data,
|
||||
|
|
@ -540,7 +539,7 @@ async def aio_price_feed_relay(
|
|||
}))
|
||||
|
||||
async def _l1(data: dict, receipt_timestamp):
|
||||
to_trio.send_nowait(('l1', {
|
||||
chan.send_nowait(('l1', {
|
||||
'symbol': cb_sym_to_deribit_inst(
|
||||
str_to_cb_sym(data.symbol)).lower(),
|
||||
'ticks': [
|
||||
|
|
@ -570,7 +569,7 @@ async def aio_price_feed_relay(
|
|||
install_signal_handlers=False)
|
||||
|
||||
# sync with trio
|
||||
to_trio.send_nowait(None)
|
||||
chan.started_nowait(None)
|
||||
|
||||
await asyncio.sleep(float('inf'))
|
||||
|
||||
|
|
@ -581,11 +580,9 @@ async def open_price_feed(
|
|||
) -> trio.abc.ReceiveStream:
|
||||
async with maybe_open_feed_handler() as fh:
|
||||
async with to_asyncio.open_channel_from(
|
||||
partial(
|
||||
aio_price_feed_relay,
|
||||
fh,
|
||||
instrument
|
||||
)
|
||||
fh=fh,
|
||||
instrument=instrument,
|
||||
) as (chan, first):
|
||||
yield chan
|
||||
|
||||
|
|
@ -593,28 +590,27 @@ async def open_price_feed(
|
|||
@acm
|
||||
async def maybe_open_price_feed(
|
||||
instrument: str
|
||||
) -> trio.abc.ReceiveStream:
|
||||
) -> BroadcastReceiver:
|
||||
|
||||
# TODO: add a predicate to maybe_open_context
|
||||
feed: to_asyncio.LinkedTaskChannel
|
||||
async with maybe_open_context(
|
||||
acm_func=open_price_feed,
|
||||
kwargs={
|
||||
'instrument': instrument
|
||||
},
|
||||
key=f'{instrument}-price',
|
||||
) as (cache_hit, feed):
|
||||
if cache_hit:
|
||||
yield broadcast_receiver(feed, 10)
|
||||
else:
|
||||
yield feed
|
||||
) as (_, feed):
|
||||
bstream: BroadcastReceiver
|
||||
async with feed.subscribe() as bstream:
|
||||
yield bstream
|
||||
|
||||
|
||||
|
||||
async def aio_order_feed_relay(
|
||||
chan: to_asyncio.LinkedTaskChannel,
|
||||
fh: FeedHandler,
|
||||
instrument: Symbol,
|
||||
from_trio: asyncio.Queue,
|
||||
to_trio: trio.abc.SendChannel,
|
||||
) -> None:
|
||||
async def _fill(data: dict, receipt_timestamp):
|
||||
breakpoint()
|
||||
|
|
@ -637,7 +633,7 @@ async def aio_order_feed_relay(
|
|||
install_signal_handlers=False)
|
||||
|
||||
# sync with trio
|
||||
to_trio.send_nowait(None)
|
||||
chan.started_nowait(None)
|
||||
|
||||
await asyncio.sleep(float('inf'))
|
||||
|
||||
|
|
@ -648,11 +644,9 @@ async def open_order_feed(
|
|||
) -> trio.abc.ReceiveStream:
|
||||
async with maybe_open_feed_handler() as fh:
|
||||
async with to_asyncio.open_channel_from(
|
||||
partial(
|
||||
aio_order_feed_relay,
|
||||
fh,
|
||||
instrument
|
||||
)
|
||||
fh=fh,
|
||||
instrument=instrument,
|
||||
) as (chan, first):
|
||||
yield chan
|
||||
|
||||
|
|
|
|||
|
|
@ -65,17 +65,18 @@ _brokerd_mods: list[str] = [
|
|||
]
|
||||
|
||||
_datad_mods: list[str] = [
|
||||
'api',
|
||||
'feed',
|
||||
'symbols',
|
||||
]
|
||||
|
||||
|
||||
# tractor RPC enable arg
|
||||
__enable_modules__: list[str] = (
|
||||
__enable_modules__: list[str] = list(dict.fromkeys(
|
||||
_brokerd_mods
|
||||
+
|
||||
_datad_mods
|
||||
)
|
||||
))
|
||||
|
||||
# passed to ``tractor.ActorNursery.start_actor()``
|
||||
_spawn_kwargs = {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ from .symbols import (
|
|||
)
|
||||
from ...log import get_logger
|
||||
from .venues import (
|
||||
is_expired,
|
||||
is_venue_open,
|
||||
sesh_times,
|
||||
is_venue_closure,
|
||||
|
|
@ -496,7 +497,7 @@ class Client:
|
|||
await self.ib.reqContractDetailsAsync(contract)
|
||||
)[0]
|
||||
# convert to makt-native tz
|
||||
tz: str = details.timeZoneId
|
||||
tz: str = details.timeZoneId or 'EST'
|
||||
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)
|
||||
|
|
@ -508,10 +509,18 @@ class Client:
|
|||
_open_now: bool = is_venue_open(
|
||||
con_deats=details,
|
||||
)
|
||||
_is_expired: bool = is_expired(
|
||||
con_deats=details,
|
||||
)
|
||||
|
||||
# XXX, do gap detections.
|
||||
has_closure_gap: bool = False
|
||||
if (
|
||||
# XXX, expired tracts can't be introspected
|
||||
# for open/closure intervals due to ib's chitty
|
||||
# details seemingly..
|
||||
not _is_expired
|
||||
and
|
||||
last_dt.add(seconds=sample_period_s)
|
||||
<
|
||||
end_dt
|
||||
|
|
@ -958,8 +967,26 @@ class Client:
|
|||
|
||||
else:
|
||||
raise
|
||||
# XXX: ambiguous/unqualifiable contracts are
|
||||
# returned as `None` entries by
|
||||
# `qualifyContractsAsync()` (which also logs an
|
||||
# "Ambiguous contract" warning listing the
|
||||
# possible matches) so filter them BEFORE any
|
||||
# attr access B)
|
||||
contracts: list[Contract] = [
|
||||
tract
|
||||
for tract in contracts
|
||||
if tract is not None
|
||||
]
|
||||
if not contracts:
|
||||
raise ValueError(f"No contract could be found {con}")
|
||||
raise ValueError(
|
||||
f'No (unambiguous) contract could be '
|
||||
f'qualified for {con!r}\n'
|
||||
f'\n'
|
||||
f'If a stonk, you likely need a (more) '
|
||||
f'venue-qualified fqme,\n'
|
||||
f"eg. 'gld.arca.ib' instead of 'gld.ib'\n"
|
||||
)
|
||||
|
||||
# pack all contracts into cache
|
||||
for tract in contracts:
|
||||
|
|
@ -1137,10 +1164,16 @@ class Client:
|
|||
try:
|
||||
con: Contract = self._contracts[symbol]
|
||||
except KeyError:
|
||||
# require that the symbol has been previously cached by
|
||||
# a data feed request - ensure we aren't making orders
|
||||
# against non-known prices.
|
||||
raise RuntimeError("Can not order {symbol}, no live feed?")
|
||||
# require that the mkt's contract has been previously
|
||||
# qualified and cached (see `.symbols.get_mkt_info()`
|
||||
# which is run for any feed-init OR lazily by the
|
||||
# order-request handler in `.broker`) - ensures we
|
||||
# aren't making orders against unknown contracts.
|
||||
raise RuntimeError(
|
||||
f'Can not order {symbol!r}, '
|
||||
f'no qualified contract cached!?\n'
|
||||
f'_contracts: {list(self._contracts)!r}\n'
|
||||
)
|
||||
|
||||
try:
|
||||
trade = self.ib.placeOrder(
|
||||
|
|
@ -1331,6 +1364,21 @@ async def load_aio_clients(
|
|||
ib = None
|
||||
client = None
|
||||
|
||||
# XXX: post (brokerd vs. datad)-split BOTH per-broker
|
||||
# daemons connect to the same API gw/tws endpoint(s); to
|
||||
# avoid `clientId` collisions (and the long conn-timeout
|
||||
# retry cycle they cause) we offset the data-daemon's
|
||||
# default id-range to be disjoint from `brokerd.ib`'s
|
||||
# (which also retries with `client_id + i` increments).
|
||||
if client_id == 6116: # the default from above
|
||||
aname: str = tractor.current_actor().name
|
||||
if 'datad' in aname:
|
||||
client_id += 16
|
||||
# ad-hoc (test/cli) actors get their own range to
|
||||
# avoid clashing with any live daemon-tree's conns.
|
||||
elif 'brokerd' not in aname:
|
||||
client_id += 32
|
||||
|
||||
# attempt to get connection info from config; if no .toml entry
|
||||
# exists, we try to load from a default localhost connection.
|
||||
localhost = '127.0.0.1'
|
||||
|
|
@ -1584,6 +1632,17 @@ class MethodProxy:
|
|||
self.event_table = event_table
|
||||
self._aio_ns = asyncio_ns
|
||||
|
||||
# request-id counter for correlating each method
|
||||
# call to its (eventual) response; necessary since
|
||||
# the (single, shared) chan has NO other ordering
|
||||
# guarantee whenever a caller task is CANCELLED
|
||||
# (eg. by a search-req timeout) after sending its
|
||||
# request but before consuming the response: the
|
||||
# "orphaned" response otherwise gets mis-delivered
|
||||
# to the next caller causing an off-by-one skew of
|
||||
# every result thereafter!
|
||||
self._mids = itertools.count()
|
||||
|
||||
async def _run_method(
|
||||
self,
|
||||
*,
|
||||
|
|
@ -1597,10 +1656,10 @@ class MethodProxy:
|
|||
|
||||
'''
|
||||
chan = self.chan
|
||||
await chan.send((meth, kwargs))
|
||||
mid: int = next(self._mids)
|
||||
await chan.send((meth, kwargs, mid))
|
||||
|
||||
while not chan.closed():
|
||||
# send through method + ``kwargs: dict`` as pair
|
||||
msg = await chan.receive()
|
||||
|
||||
# TODO: implement reconnect functionality like
|
||||
|
|
@ -1610,23 +1669,50 @@ class MethodProxy:
|
|||
# except ConnectionError:
|
||||
# self.reset()
|
||||
|
||||
# print(f'NEXT MSG: {msg}')
|
||||
|
||||
# TODO: py3.10 ``match:`` syntax B)
|
||||
if 'result' in msg:
|
||||
res = msg.get('result')
|
||||
match msg:
|
||||
# OUR method-call response B)
|
||||
case {'mid': resp_mid, 'result': res} if (
|
||||
resp_mid == mid
|
||||
):
|
||||
return res
|
||||
|
||||
elif 'exception' in msg:
|
||||
err = msg.get('exception')
|
||||
case {'mid': resp_mid, 'exception': err} if (
|
||||
resp_mid == mid
|
||||
):
|
||||
raise err
|
||||
|
||||
elif 'error' in msg:
|
||||
etype, emsg = msg
|
||||
# an "orphaned" response to some prior
|
||||
# (cancelled) caller's request; drop it and
|
||||
# keep waiting for ours.
|
||||
case {'mid': resp_mid}:
|
||||
log.warning(
|
||||
f'Dropping stale method-resp,\n'
|
||||
f'mid: {resp_mid} (ours: {mid})\n'
|
||||
f'(a prior caller prolly got '
|
||||
f'cancelled before its resp?)\n'
|
||||
)
|
||||
continue
|
||||
|
||||
# out-of-band (inline) client error: raise
|
||||
# to the current caller as before.
|
||||
case {'exception': err}:
|
||||
raise err
|
||||
|
||||
case ('error', emsg):
|
||||
log.warning(f'IB error relay: {emsg}')
|
||||
continue
|
||||
|
||||
else:
|
||||
# routine (api-farm conn) status events
|
||||
# relayed inline by `Client.inline_errors()`;
|
||||
# not a response to our method call so just
|
||||
# log at info and keep waiting.
|
||||
case ('event', emsg):
|
||||
log.info(
|
||||
f'IB status event relay: {emsg}'
|
||||
)
|
||||
continue
|
||||
|
||||
case _:
|
||||
log.warning(f'UNKNOWN IB MSG: {msg}')
|
||||
|
||||
def status_event(
|
||||
|
|
@ -1680,22 +1766,31 @@ async def open_aio_client_method_relay(
|
|||
log.info('asyncio `Client` method-proxy SHUTDOWN!')
|
||||
break
|
||||
|
||||
case (meth_name, kwargs):
|
||||
meth_name, kwargs = msg
|
||||
case (meth_name, kwargs, mid):
|
||||
meth = getattr(client, meth_name)
|
||||
|
||||
try:
|
||||
resp = await meth(**kwargs)
|
||||
# echo the msg back
|
||||
chan.send_nowait({'result': resp})
|
||||
# echo the msg back tagged with the
|
||||
# req-id for caller-side correlation.
|
||||
chan.send_nowait({
|
||||
'mid': mid,
|
||||
'result': resp,
|
||||
})
|
||||
|
||||
except (
|
||||
RequestError,
|
||||
|
||||
# TODO: relay all errors to trio?
|
||||
# BaseException,
|
||||
) as err:
|
||||
chan.send_nowait({'exception': err})
|
||||
# XXX: relay ALL (non-cancel) errors to the
|
||||
# `trio`-side caller (which re-raises in the
|
||||
# `MethodProxy._run_method()` frame) instead
|
||||
# of crashing this relay task and thus the
|
||||
# whole proxy/dialog; critical post the
|
||||
# (datad|brokerd)-split where eg. contract
|
||||
# qualification failures are expected to be
|
||||
# caught per-request by order/warmup code!
|
||||
except Exception as err:
|
||||
chan.send_nowait({
|
||||
'mid': mid,
|
||||
'exception': err,
|
||||
})
|
||||
|
||||
case {'error': content}:
|
||||
chan.send_nowait({'exception': content})
|
||||
|
|
|
|||
|
|
@ -89,8 +89,9 @@ from .api import (
|
|||
MethodProxy,
|
||||
)
|
||||
from .symbols import (
|
||||
cache_contract,
|
||||
con2fqme,
|
||||
# get_mkt_info,
|
||||
get_mkt_info,
|
||||
)
|
||||
from .ledger import (
|
||||
norm_trade_records,
|
||||
|
|
@ -138,6 +139,7 @@ async def handle_order_requests(
|
|||
ems_order_stream: tractor.MsgStream,
|
||||
accounts_def: dict[str, str],
|
||||
flows: OrderDialogs,
|
||||
proxies: dict[str, MethodProxy],
|
||||
|
||||
) -> None:
|
||||
|
||||
|
|
@ -180,6 +182,58 @@ async def handle_order_requests(
|
|||
if action in {'buy', 'sell'}:
|
||||
# validate
|
||||
order = BrokerdOrder(**request_msg)
|
||||
fqme: str = order.symbol
|
||||
|
||||
# XXX: lazily qualify and cache the contract for
|
||||
# this mkt since, post the (datad|brokerd)-split,
|
||||
# this trading-only actor will NOT have had its
|
||||
# (api) `Client._contracts` pre-warmed by any
|
||||
# in-proc feed setup (which now runs in the
|
||||
# `datad.ib` sibling); we run the SAME symbology
|
||||
# resolution ep the feed-side uses so the cache
|
||||
# key (`MktPair.bs_fqme`) matches what
|
||||
# `Client.submit_limit()` looks up.
|
||||
# NOTE: normally a no-op since
|
||||
# `open_trade_dialog()` eagerly pre-qualifies all
|
||||
# already-open pp/order mkts at startup; this
|
||||
# only fires for a brand-new (to this daemon)
|
||||
# mkt's first order.
|
||||
if fqme not in client._contracts:
|
||||
proxy: MethodProxy = proxies[account]
|
||||
try:
|
||||
(
|
||||
mkt,
|
||||
details,
|
||||
) = await get_mkt_info(
|
||||
fqme,
|
||||
proxy=proxy,
|
||||
)
|
||||
# XXX: explicit write since the lifo-cache
|
||||
# may deliver a hit (body skipped) when
|
||||
# another acct/client already qualified
|
||||
# this fqme.
|
||||
cache_contract(
|
||||
client,
|
||||
mkt,
|
||||
details.contract,
|
||||
)
|
||||
except Exception as err:
|
||||
log.exception(
|
||||
f'Failed to qualify contract for\n'
|
||||
f'fqme: {fqme!r}\n'
|
||||
)
|
||||
await ems_order_stream.send(
|
||||
BrokerdError(
|
||||
oid=oid,
|
||||
symbol=fqme,
|
||||
reason=(
|
||||
f'No contract could be '
|
||||
f'qualified for {fqme!r}:\n'
|
||||
f'{err!r}'
|
||||
),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# XXX: by default 0 tells ``ib_async`` methods that
|
||||
# there is no existing order so ask the client to create
|
||||
|
|
@ -191,15 +245,34 @@ async def handle_order_requests(
|
|||
reqid = int(reqid)
|
||||
|
||||
# call our client api to submit the order
|
||||
# NOTE: guard with order-error relay (vs. crashing
|
||||
# this dialog and thus ALL order ctl for the
|
||||
# backend) so one bad submission can't take down
|
||||
# the daemon's clearing loop.
|
||||
try:
|
||||
reqid = client.submit_limit(
|
||||
oid=order.oid,
|
||||
symbol=order.symbol,
|
||||
symbol=fqme,
|
||||
price=order.price,
|
||||
action=order.action,
|
||||
size=order.size,
|
||||
account=acct_number,
|
||||
reqid=reqid,
|
||||
)
|
||||
except Exception as err:
|
||||
log.exception(
|
||||
f'Order submission failed for\n'
|
||||
f'fqme: {fqme!r}\n'
|
||||
)
|
||||
await ems_order_stream.send(
|
||||
BrokerdError(
|
||||
oid=oid,
|
||||
symbol=fqme,
|
||||
reason=f'Submission error: {err!r}',
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
str_reqid: str = str(reqid)
|
||||
if reqid is None:
|
||||
err_msg = BrokerdError(
|
||||
|
|
@ -231,20 +304,21 @@ async def handle_order_requests(
|
|||
|
||||
async def recv_trade_updates(
|
||||
|
||||
chan: tractor.to_asyncio.LinkedTaskChannel,
|
||||
client: Client,
|
||||
to_trio: trio.abc.SendChannel,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Receive and relay order control and positioning related events
|
||||
from `ib_async`, pack as tuples and push over mem-chan to our
|
||||
trio relay task for processing and relay to EMS.
|
||||
Receive and relay order control and positioning
|
||||
related events from `ib_async`, pack as tuples and
|
||||
push over mem-chan to our trio relay task for
|
||||
processing and relay to EMS.
|
||||
|
||||
'''
|
||||
client.inline_errors(to_trio)
|
||||
client.inline_errors(chan)
|
||||
|
||||
# sync with trio task
|
||||
to_trio.send_nowait(client.ib)
|
||||
chan.started_nowait(client.ib)
|
||||
|
||||
def push_tradesies(
|
||||
eventkit_obj,
|
||||
|
|
@ -282,7 +356,7 @@ async def recv_trade_updates(
|
|||
|
||||
try:
|
||||
# emit event name + relevant ibis internal objects
|
||||
to_trio.send_nowait((event_name, emit))
|
||||
chan.send_nowait((event_name, emit))
|
||||
except trio.BrokenResourceError:
|
||||
log.exception(f'Disconnected from {eventkit_obj} updates')
|
||||
eventkit_obj.disconnect(push_tradesies)
|
||||
|
|
@ -547,10 +621,14 @@ async def open_trade_dialog(
|
|||
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
|
||||
get_console_log(
|
||||
level=loglevel,
|
||||
name=__name__,
|
||||
)
|
||||
# XXX: ONLY adjust the level of this (sub)mod's logger;
|
||||
# attaching a (stderr) handler (via `get_console_log()`)
|
||||
# here would DOUBLE-print every record since the daemon
|
||||
# fixture (`.._daemon._setup_persistent_brokerd()`)
|
||||
# already enables the console handler on the parent
|
||||
# subsys logger which all our records propagate to!
|
||||
if loglevel:
|
||||
log.setLevel(loglevel.upper())
|
||||
|
||||
# task local msg dialog tracking
|
||||
flows = OrderDialogs()
|
||||
|
|
@ -787,6 +865,64 @@ async def open_trade_dialog(
|
|||
for msg in order_msgs:
|
||||
await ems_stream.send(msg)
|
||||
|
||||
# XXX: eagerly pre-qualify (and cache) the
|
||||
# contracts for all already-open pps and
|
||||
# orders so that (live) order submission
|
||||
# NEVER pays a first-request qualification
|
||||
# delay; any brand-new mkt is still lazily
|
||||
# qualified by `handle_order_requests()` on
|
||||
# its first submission. NOTE: since this
|
||||
# runs BEFORE the order handler task is
|
||||
# even started, no order can clear until
|
||||
# the warmup completes (early reqs just
|
||||
# buffer in the ems IPC stream) B)
|
||||
warmup_fqmes: set[str] = {
|
||||
msg.symbol
|
||||
for msg in all_positions
|
||||
}
|
||||
warmup_fqmes.update(
|
||||
msg.req.symbol
|
||||
for msg in order_msgs
|
||||
)
|
||||
unique_clients: set[Client] = set(
|
||||
aioclients.values()
|
||||
)
|
||||
if (
|
||||
warmup_fqmes
|
||||
and
|
||||
proxies
|
||||
):
|
||||
a_proxy: MethodProxy = next(
|
||||
iter(proxies.values())
|
||||
)
|
||||
for fqme in warmup_fqmes:
|
||||
try:
|
||||
(
|
||||
mkt,
|
||||
details,
|
||||
) = await get_mkt_info(
|
||||
fqme,
|
||||
proxy=a_proxy,
|
||||
)
|
||||
except Exception as err:
|
||||
# XXX: non-fatal; an
|
||||
# un-warmed mkt just falls
|
||||
# back to the lazy qualify
|
||||
# in the order handler.
|
||||
log.warning(
|
||||
f'Failed to pre-qualify\n'
|
||||
f'fqme: {fqme!r}\n'
|
||||
f'err: {err!r}\n'
|
||||
)
|
||||
continue
|
||||
|
||||
for _client in unique_clients:
|
||||
cache_contract(
|
||||
_client,
|
||||
mkt,
|
||||
details.contract,
|
||||
)
|
||||
|
||||
for client in set(aioclients.values()):
|
||||
trade_event_stream: LinkedTaskChannel = await tn.start(
|
||||
open_trade_event_stream,
|
||||
|
|
@ -800,6 +936,7 @@ async def open_trade_dialog(
|
|||
ems_stream,
|
||||
accounts_def,
|
||||
flows,
|
||||
proxies,
|
||||
)
|
||||
|
||||
# allocate event relay tasks for each client connection
|
||||
|
|
@ -1162,9 +1299,9 @@ async def deliver_trade_events(
|
|||
case 'execDetailsEvent':
|
||||
# unpack attrs pep-0526 style.
|
||||
trade: Trade
|
||||
con: Contract = trade.contract
|
||||
fill: Fill
|
||||
trade, fill = item
|
||||
con: Contract = trade.contract
|
||||
execu: Execution = fill.execution
|
||||
execid: str = execu.execId
|
||||
report: CommissionReport = fill.commissionReport
|
||||
|
|
@ -1306,7 +1443,15 @@ async def deliver_trade_events(
|
|||
elif isinstance(err, str):
|
||||
code_part, _, reason = err.rpartition(']')
|
||||
if code_part:
|
||||
_, _, code = code_part.partition('[code')
|
||||
for prefix_patt in [
|
||||
'[Errno ',
|
||||
'[code ',
|
||||
]:
|
||||
code_part, _, code = code_part.partition()
|
||||
if code:
|
||||
code = int(code)
|
||||
break
|
||||
|
||||
reqid: str = '<unknown>'
|
||||
|
||||
# "Warning:" msg codes,
|
||||
|
|
@ -1325,6 +1470,8 @@ async def deliver_trade_events(
|
|||
|
||||
# XXX known special (ignore) cases
|
||||
elif code in {
|
||||
# ^TODO, if this is it we should definitely raise
|
||||
# or at least provide for reconnect attempts?
|
||||
200, # uhh.. ni idea
|
||||
|
||||
# hist pacing / connectivity
|
||||
|
|
@ -1335,10 +1482,18 @@ async def deliver_trade_events(
|
|||
# 'No market data during competing live session'
|
||||
1669,
|
||||
}:
|
||||
pcc: str = "Peer closed connection."
|
||||
if pcc in err:
|
||||
# TODO, emit and raise?
|
||||
# [ ] try for reconnect?
|
||||
# raise BrokerdError(
|
||||
await tractor.pause()
|
||||
|
||||
log.error(
|
||||
f'Order-API-error which is non-cancel-causing ?!\n'
|
||||
f'\n'
|
||||
f'{pformat(err)}\n'
|
||||
f'code={code!r}\n'
|
||||
f'err={pformat(err)}\n'
|
||||
)
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -738,10 +738,6 @@ async def get_bars(
|
|||
)
|
||||
|
||||
|
||||
# per-actor cache of inter-eventloop-chans
|
||||
_quote_streams: dict[str, trio.abc.ReceiveStream] = {}
|
||||
|
||||
|
||||
async def _setup_quote_stream(
|
||||
chan: tractor.to_asyncio.LinkedTaskChannel,
|
||||
symbol: str,
|
||||
|
|
@ -770,8 +766,6 @@ async def _setup_quote_stream(
|
|||
and is thus run via `tractor.to_asyncio.open_channel_from()`.
|
||||
|
||||
'''
|
||||
global _quote_streams
|
||||
|
||||
async with load_aio_clients(
|
||||
disconnect_on_exit=False,
|
||||
) as accts2clients:
|
||||
|
|
@ -834,9 +828,6 @@ async def _setup_quote_stream(
|
|||
|
||||
client.ib.cancelMktData(contract)
|
||||
|
||||
# decouple broadcast mem chan
|
||||
_quote_streams.pop(symbol, None)
|
||||
|
||||
def push(
|
||||
t: Ticker,
|
||||
tries_before_raise: int = 6,
|
||||
|
|
@ -953,54 +944,52 @@ async def _setup_quote_stream(
|
|||
|
||||
|
||||
@acm
|
||||
async def open_aio_quote_stream(
|
||||
async def _open_aio_quote_channel(
|
||||
symbol: str,
|
||||
contract: Contract|None = None,
|
||||
|
||||
) -> (
|
||||
trio.abc.Channel| # iface
|
||||
tractor.to_asyncio.LinkedTaskChannel # actually
|
||||
):
|
||||
) -> tractor.to_asyncio.LinkedTaskChannel:
|
||||
'''
|
||||
Open a real-time `Ticker` quote stream from an `asyncio.Task`
|
||||
spawned via `tractor.to_asyncio.open_channel_from()`, deliver the
|
||||
inter-event-loop channel to the `trio.Task` caller and cache it
|
||||
globally for re-use.
|
||||
Open one raw inter-event-loop quote channel for `symbol`.
|
||||
|
||||
'''
|
||||
from tractor.trionics import broadcast_receiver
|
||||
global _quote_streams
|
||||
|
||||
from_aio = _quote_streams.get(symbol)
|
||||
if from_aio:
|
||||
|
||||
# if we already have a cached feed deliver a rx side clone
|
||||
# to consumer
|
||||
async with broadcast_receiver(
|
||||
from_aio,
|
||||
2**6,
|
||||
) as from_aio:
|
||||
yield from_aio
|
||||
return
|
||||
|
||||
from_aio: tractor.to_asyncio.LinkedTaskChannel
|
||||
chan: tractor.to_asyncio.LinkedTaskChannel
|
||||
started_contract: Contract
|
||||
async with tractor.to_asyncio.open_channel_from(
|
||||
_setup_quote_stream,
|
||||
symbol=symbol,
|
||||
contract=contract,
|
||||
|
||||
) as (from_aio, contract):
|
||||
) as (chan, started_contract):
|
||||
assert started_contract
|
||||
yield chan
|
||||
|
||||
assert contract
|
||||
|
||||
# TODO? de-reg on teardown of last consumer task?
|
||||
# -> why aren't we using `.trionics.maybe_open_context()`
|
||||
# here again?? (we are in `open_client_proxies()` tho?)
|
||||
#
|
||||
# cache feed for later consumers
|
||||
_quote_streams[symbol] = from_aio
|
||||
@acm
|
||||
async def open_aio_quote_stream(
|
||||
symbol: str,
|
||||
contract: Contract|None = None,
|
||||
|
||||
yield from_aio
|
||||
) -> tractor.trionics.BroadcastReceiver:
|
||||
'''
|
||||
Open a caller-owned child of the cached quote channel for `symbol`.
|
||||
|
||||
Keep the raw `LinkedTaskChannel` alive until its last cache user
|
||||
exits, and retain exactly one broadcaster root on that channel.
|
||||
|
||||
'''
|
||||
chan: tractor.to_asyncio.LinkedTaskChannel
|
||||
async with tractor.trionics.maybe_open_context(
|
||||
acm_func=_open_aio_quote_channel,
|
||||
kwargs={
|
||||
'symbol': symbol,
|
||||
'contract': contract,
|
||||
},
|
||||
key=symbol,
|
||||
) as (_, chan):
|
||||
bstream: tractor.trionics.BroadcastReceiver
|
||||
async with chan.subscribe() as bstream:
|
||||
yield bstream
|
||||
|
||||
|
||||
# TODO: cython/mypyc/numba this!
|
||||
|
|
|
|||
|
|
@ -501,7 +501,7 @@ async def update_ledger_from_api_trades(
|
|||
for fill in fills:
|
||||
con: Contract = fill.contract
|
||||
conid: str = con.conId
|
||||
pexch: str | None = con.primaryExchange
|
||||
pexch: str|None = con.primaryExchange or con.exchange
|
||||
|
||||
if not pexch:
|
||||
cons = await client.get_con(conid=conid)
|
||||
|
|
|
|||
|
|
@ -639,12 +639,40 @@ async def get_mkt_info(
|
|||
|
||||
# if possible register the bs_mktid to the just-built
|
||||
# mkt so that it can be retreived by order mode tasks later.
|
||||
# TODO NOTE: this is going to be problematic if/when we split
|
||||
# out the datatd vs. brokerd actors since the mktmap lookup
|
||||
# table will now be inaccessible..
|
||||
if proxy is not None:
|
||||
client: Client = proxy._aio_ns
|
||||
client._contracts[mkt.bs_fqme] = con
|
||||
client._cons2mkts[con] = mkt
|
||||
cache_contract(
|
||||
proxy._aio_ns,
|
||||
mkt,
|
||||
con,
|
||||
)
|
||||
|
||||
return mkt, details
|
||||
|
||||
|
||||
def cache_contract(
|
||||
client: Client,
|
||||
mkt: MktPair,
|
||||
con: ibis.Contract,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Register a (qualified) contract + mkt-info pair on the
|
||||
given (api) `Client`'s actor-local lookup tables.
|
||||
|
||||
Cached under BOTH fqme key-forms since consumers vary:
|
||||
- `mkt.bs_fqme` (eg. 'nvda.nasdaq'): read by
|
||||
`Client.submit_limit()` for order requests,
|
||||
- `mkt.fqme` (eg. 'nvda.nasdaq.ib'): read by the
|
||||
fill-time pp-update (symcache-backup-table) path in
|
||||
`.broker.emit_pp_update()`.
|
||||
|
||||
NOTE: post the (datad|brokerd)-actor-split this MUST be
|
||||
run (in the trading actor) for every mkt either eagerly
|
||||
at `.broker.open_trade_dialog()` startup or lazily per
|
||||
order request; there is no in-proc feed setup doing it
|
||||
implicitly anymore!
|
||||
|
||||
'''
|
||||
client._contracts[mkt.bs_fqme] = con
|
||||
client._contracts[mkt.fqme] = con
|
||||
client._cons2mkts[con] = mkt
|
||||
|
|
|
|||
|
|
@ -33,13 +33,21 @@ from typing import (
|
|||
)
|
||||
|
||||
import exchange_calendars as xcals
|
||||
from exchange_calendars.errors import (
|
||||
InvalidCalendarName,
|
||||
)
|
||||
from pendulum import (
|
||||
parse,
|
||||
now,
|
||||
Duration,
|
||||
Interval,
|
||||
Time,
|
||||
)
|
||||
|
||||
from piker.log import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ib_async import (
|
||||
TradingSession,
|
||||
|
|
@ -56,6 +64,22 @@ if TYPE_CHECKING:
|
|||
)
|
||||
|
||||
|
||||
def is_expired(
|
||||
con_deats: ContractDetails,
|
||||
) -> bool:
|
||||
'''
|
||||
Simple predicate whether the provided contract-deats match and
|
||||
already lifetime-terminated instrument.
|
||||
|
||||
'''
|
||||
expiry_str: str = con_deats.realExpirationDate
|
||||
if not expiry_str:
|
||||
return False
|
||||
|
||||
expiry_dt: datetime = parse(expiry_str)
|
||||
return expiry_dt.date() >= now().date()
|
||||
|
||||
|
||||
def has_weekend(
|
||||
period: Interval,
|
||||
) -> bool:
|
||||
|
|
@ -90,13 +114,28 @@ def has_holiday(
|
|||
con.exchange
|
||||
)
|
||||
|
||||
# XXX, ad-hoc handle any IB exchange which are non-std
|
||||
# via lookup table..
|
||||
std_exch: dict = {
|
||||
# XXX, ad-hoc handle any IB exchange which are
|
||||
# non-std via lookup table..
|
||||
std_exch: str = {
|
||||
'ARCA': 'ARCX',
|
||||
}.get(exch, exch)
|
||||
|
||||
cal: ExchangeCalendar = xcals.get_calendar(std_exch)
|
||||
try:
|
||||
cal: ExchangeCalendar = xcals.get_calendar(
|
||||
std_exch
|
||||
)
|
||||
except InvalidCalendarName:
|
||||
# venue has no `exchange_calendars` entry
|
||||
# (eg. IDEALPRO for forex, PAXOS for
|
||||
# crypto) -> not a holiday by default since
|
||||
# weekends are already handled by
|
||||
# `has_weekend()`.
|
||||
log.warning(
|
||||
f'No exchange cal for {std_exch!r},'
|
||||
f' skipping holiday check..\n'
|
||||
)
|
||||
return False
|
||||
|
||||
end: datetime = period.end
|
||||
# _start: datetime = period.start
|
||||
# ?TODO, can rm ya?
|
||||
|
|
@ -170,7 +209,22 @@ def sesh_times(
|
|||
get the (day-agnostic) times for the start/end.
|
||||
|
||||
'''
|
||||
earliest_sesh: Interval = next(iter_sessions(con_deats))
|
||||
# ?TODO, lookup the next front contract instead?
|
||||
if is_expired(con_deats):
|
||||
raise ValueError(
|
||||
f'Contract is already expired!\n'
|
||||
f'Choose an active alt contract instead.\n'
|
||||
f'con_deats: {con_deats!r}\n'
|
||||
)
|
||||
|
||||
maybe_sessions: list[Interval] = list(iter_sessions(con_deats))
|
||||
if not maybe_sessions:
|
||||
raise ValueError(
|
||||
f'Contract has no trading-session info?\n'
|
||||
f'con_deats: {con_deats!r}\n'
|
||||
)
|
||||
|
||||
earliest_sesh: Interval = maybe_sessions[0]
|
||||
return (
|
||||
earliest_sesh.start.time(),
|
||||
earliest_sesh.end.time(),
|
||||
|
|
@ -211,7 +265,13 @@ def is_venue_closure(
|
|||
'''
|
||||
open: Time
|
||||
close: Time
|
||||
open, close = sesh_times(con_deats)
|
||||
maybe_oc: tuple|None = sesh_times(con_deats)
|
||||
if maybe_oc is None:
|
||||
# XXX, should never get here.
|
||||
breakpoint()
|
||||
return False
|
||||
|
||||
open, close = maybe_oc
|
||||
|
||||
# ensure times are in mkt-native timezone
|
||||
tz: str = con_deats.timeZoneId
|
||||
|
|
|
|||
|
|
@ -66,10 +66,24 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
# tractor RPC enable arg
|
||||
__enable_modules__: list[str] = [
|
||||
# per-daemon-kind (sub)mod groups: declares which of our
|
||||
# submods host the eps run by each daemon-actor kind as
|
||||
# defined by `piker.data.validate._eps`.
|
||||
_brokerd_mods: list[str] = [
|
||||
'api',
|
||||
'broker',
|
||||
]
|
||||
|
||||
_datad_mods: list[str] = [
|
||||
'api',
|
||||
'feed',
|
||||
'symbols',
|
||||
]
|
||||
|
||||
|
||||
# tractor RPC enable arg
|
||||
__enable_modules__: list[str] = list(dict.fromkeys(
|
||||
_brokerd_mods
|
||||
+
|
||||
_datad_mods
|
||||
))
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import hashlib
|
|||
import hmac
|
||||
import base64
|
||||
import tractor
|
||||
# from tractor._exceptions import reg_err_types
|
||||
import trio
|
||||
|
||||
from piker import config
|
||||
|
|
@ -52,6 +53,7 @@ from piker.brokers._util import (
|
|||
SymbolNotFound,
|
||||
BrokerError,
|
||||
DataThrottle,
|
||||
get_or_raise_on_pair_schema_mismatch,
|
||||
)
|
||||
from piker.accounting import Transaction
|
||||
from piker.log import get_logger
|
||||
|
|
@ -107,15 +109,37 @@ def get_kraken_signature(
|
|||
return sigdigest.decode()
|
||||
|
||||
|
||||
class InvalidKey(ValueError):
|
||||
'''
|
||||
EAPI:Invalid key
|
||||
This error is returned when the API key used for the call is
|
||||
either expired or disabled, please review the API key in your
|
||||
Settings -> API tab of account management or generate a new one
|
||||
and update your application.
|
||||
# class InvalidKey(ValueError):
|
||||
# '''
|
||||
# EAPI:Invalid key
|
||||
|
||||
'''
|
||||
# This error is returned when the API key used for the call is
|
||||
# either expired or disabled, please review the API key in your
|
||||
# Settings -> API tab of account management or generate a new one
|
||||
# and update your application.
|
||||
|
||||
# '''
|
||||
|
||||
|
||||
# class InvalidSession(RuntimeError):
|
||||
# '''
|
||||
# ESession:Invalid session
|
||||
|
||||
# This error is returned when the ws API key used for an authenticated
|
||||
# sub/endpoint becomes stale, normally after a sufficient network
|
||||
# disconnect/outage.
|
||||
|
||||
# Normally the sub will need to be restarted, likely re-init of the
|
||||
# auth handshake sequence.
|
||||
|
||||
# '''
|
||||
# subscription: dict
|
||||
|
||||
|
||||
# reg_err_types([
|
||||
# InvalidKey,
|
||||
# InvalidSession,
|
||||
# ])
|
||||
|
||||
|
||||
class Client:
|
||||
|
|
@ -143,18 +167,16 @@ class Client:
|
|||
config: dict[str, str],
|
||||
httpx_client: httpx.AsyncClient,
|
||||
|
||||
name: str = '',
|
||||
key_descr: str = '',
|
||||
api_key: str = '',
|
||||
secret: str = ''
|
||||
) -> None:
|
||||
|
||||
self._sesh: httpx.AsyncClient = httpx_client
|
||||
|
||||
self._name = name
|
||||
self._key_descr = key_descr
|
||||
self._api_key = api_key
|
||||
self._secret = secret
|
||||
|
||||
self.conf: dict[str, str] = config
|
||||
self._ws_token: str|None = None
|
||||
|
||||
@property
|
||||
def pairs(self) -> dict[str, Pair]:
|
||||
|
|
@ -239,6 +261,39 @@ class Client:
|
|||
|
||||
return balances
|
||||
|
||||
async def get_ws_token(
|
||||
self,
|
||||
params: dict = {},
|
||||
force_renewal: bool = False,
|
||||
) -> str:
|
||||
'''
|
||||
Get websocket token for authenticated data stream and cache
|
||||
it for reuse.
|
||||
|
||||
Assert a value was actually received before return.
|
||||
|
||||
'''
|
||||
if (
|
||||
not force_renewal
|
||||
and
|
||||
self._ws_token
|
||||
):
|
||||
return self._ws_token
|
||||
|
||||
resp = await self.endpoint(
|
||||
'GetWebSocketsToken',
|
||||
{},
|
||||
)
|
||||
if err := resp.get('error'):
|
||||
raise BrokerError(err)
|
||||
|
||||
# resp token for ws init
|
||||
token: str = resp['result']['token']
|
||||
assert token
|
||||
|
||||
self._ws_token: str = token
|
||||
return token
|
||||
|
||||
async def get_assets(
|
||||
self,
|
||||
reload: bool = False,
|
||||
|
|
@ -502,7 +557,16 @@ class Client:
|
|||
|
||||
# NOTE: always cache in pairs tables for faster lookup
|
||||
with tractor.devx.maybe_open_crash_handler(): # as bxerr:
|
||||
pair = Pair(xname=xkey, **data)
|
||||
# pair = Pair(xname=xkey, **data)
|
||||
pair: Pair = get_or_raise_on_pair_schema_mismatch(
|
||||
pair_type=Pair,
|
||||
fields_data=dict(
|
||||
xname=xkey,
|
||||
**data,
|
||||
),
|
||||
provider_name='kraken',
|
||||
# api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information',
|
||||
)
|
||||
|
||||
# register the above `Pair` structs for all
|
||||
# key-sets/monikers: a set of 4 (frickin) tables
|
||||
|
|
@ -668,7 +732,13 @@ class Client:
|
|||
|
||||
@acm
|
||||
async def get_client() -> Client:
|
||||
'''
|
||||
Load and deliver a `.kraken.api.Client`.
|
||||
|
||||
When defined, inject any config delivered from the user's
|
||||
`brokers.toml` config file.
|
||||
|
||||
'''
|
||||
conf: dict[str, Any] = get_config()
|
||||
async with httpx.AsyncClient(
|
||||
base_url=_url,
|
||||
|
|
@ -679,13 +749,14 @@ async def get_client() -> Client:
|
|||
# connections=4
|
||||
) as trio_client:
|
||||
if conf:
|
||||
api_key_descr: str = conf['key_descr']
|
||||
client = Client(
|
||||
conf,
|
||||
httpx_client=trio_client,
|
||||
|
||||
# TODO: don't break these up and just do internal
|
||||
# conf lookups instead..
|
||||
name=conf['key_descr'],
|
||||
key_descr=api_key_descr,
|
||||
api_key=conf['api_key'],
|
||||
secret=conf['secret']
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,12 +30,15 @@ from typing import (
|
|||
Any,
|
||||
AsyncIterator,
|
||||
Iterable,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from bidict import bidict
|
||||
import trio
|
||||
import tractor
|
||||
from tractor.devx.pformat import ppfmt
|
||||
# from tractor._exceptions import reg_err_types
|
||||
|
||||
from piker.accounting import (
|
||||
Position,
|
||||
|
|
@ -45,6 +48,9 @@ from piker.accounting import (
|
|||
open_trade_ledger,
|
||||
open_account,
|
||||
)
|
||||
from piker.config import (
|
||||
ConfigurationError,
|
||||
)
|
||||
from piker.clearing import(
|
||||
OrderDialogs,
|
||||
)
|
||||
|
|
@ -67,13 +73,12 @@ from piker.log import (
|
|||
get_logger,
|
||||
)
|
||||
from piker.data import open_symcache
|
||||
from .api import (
|
||||
Client,
|
||||
BrokerError,
|
||||
)
|
||||
from .feed import (
|
||||
from piker.data._web_bs import (
|
||||
open_autorecon_ws,
|
||||
NoBsWs,
|
||||
)
|
||||
from . import api
|
||||
from .feed import (
|
||||
stream_messages,
|
||||
)
|
||||
from .ledger import (
|
||||
|
|
@ -94,11 +99,7 @@ MsgUnion = Union[
|
|||
]
|
||||
|
||||
|
||||
class TooFastEdit(Exception):
|
||||
'Edit requests faster then api submissions'
|
||||
|
||||
|
||||
# TODO: make this wrap the `Client` and `ws` instances
|
||||
# TODO: make this wrap the `api.Client` and `ws` instances
|
||||
# and give it methods to submit cancel vs. add vs. edit
|
||||
# requests?
|
||||
class BrokerClient:
|
||||
|
|
@ -126,23 +127,22 @@ class BrokerClient:
|
|||
|
||||
|
||||
async def handle_order_requests(
|
||||
|
||||
ws: NoBsWs,
|
||||
client: Client,
|
||||
client: api.Client,
|
||||
ems_order_stream: tractor.MsgStream,
|
||||
token: str,
|
||||
apiflows: OrderDialogs,
|
||||
ids: bidict[str, int],
|
||||
reqids2txids: dict[int, str],
|
||||
toofastedit: set[int],
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Process new order submission requests from the EMS
|
||||
and deliver acks or errors.
|
||||
`trio.Task` which handles order ctl requests from the EMS and
|
||||
deliver acks or errors back on that IPC dialog.
|
||||
|
||||
'''
|
||||
# XXX: UGH, let's unify this.. with ``msgspec``!!!
|
||||
msg: dict | Order
|
||||
msg: dict|Order
|
||||
async for msg in ems_order_stream:
|
||||
log.info(f'Rx order msg:\n{pformat(msg)}')
|
||||
match msg:
|
||||
|
|
@ -156,8 +156,13 @@ async def handle_order_requests(
|
|||
txid = reqids2txids[reqid]
|
||||
except KeyError:
|
||||
# XXX: not sure if this block ever gets hit now?
|
||||
# SEEMS TO on the race case with the update task?
|
||||
# - update dark order quickly after
|
||||
# triggered-submitted and then we have inavlid
|
||||
# value in `reqids2txids` sent over ws.send()??
|
||||
log.error('TOO FAST CANCEL/EDIT')
|
||||
reqids2txids[reqid] = TooFastEdit(reqid)
|
||||
toofastedit.add(reqid)
|
||||
reqids2txids[reqid] = reqid
|
||||
await ems_order_stream.send(
|
||||
BrokerdError(
|
||||
oid=msg['oid'],
|
||||
|
|
@ -173,7 +178,7 @@ async def handle_order_requests(
|
|||
# https://docs.kraken.com/websockets/#message-cancelOrder
|
||||
await ws.send_msg({
|
||||
'event': 'cancelOrder',
|
||||
'token': token,
|
||||
'token': await client.get_ws_token(),
|
||||
'reqid': reqid,
|
||||
'txid': [txid], # should be txid from submission
|
||||
})
|
||||
|
|
@ -185,7 +190,7 @@ async def handle_order_requests(
|
|||
# validate
|
||||
order = BrokerdOrder(**msg)
|
||||
|
||||
# logic from old `Client.submit_limit()`
|
||||
# logic from old `api.Client.submit_limit()`
|
||||
if order.oid in ids:
|
||||
ep: str = 'editOrder'
|
||||
reqid: int = ids[order.oid] # integer not txid
|
||||
|
|
@ -195,13 +200,15 @@ async def handle_order_requests(
|
|||
|
||||
# XXX: not sure if this block ever gets hit now?
|
||||
log.error('TOO FAST EDIT')
|
||||
reqids2txids[reqid] = TooFastEdit(reqid)
|
||||
reqids2txids[reqid] = reqid
|
||||
toofastedit.add(reqid)
|
||||
await tractor.pause()
|
||||
await ems_order_stream.send(
|
||||
BrokerdError(
|
||||
oid=msg['oid'],
|
||||
symbol=msg['symbol'],
|
||||
reason=(
|
||||
f'TooFastEdit reqid:{reqid}, cancelling..'
|
||||
f'TooFastEdit reqid: {reqid}, cancelling..'
|
||||
),
|
||||
|
||||
)
|
||||
|
|
@ -247,7 +254,7 @@ async def handle_order_requests(
|
|||
# https://docs.kraken.com/websockets/#message-addOrder
|
||||
req = {
|
||||
'event': ep,
|
||||
'token': token,
|
||||
'token': await client.get_ws_token(),
|
||||
|
||||
'reqid': reqid, # remapped-to-int uid from ems
|
||||
# XXX: we set these to the same value since for us
|
||||
|
|
@ -291,13 +298,15 @@ async def handle_order_requests(
|
|||
symbol=msg['symbol'],
|
||||
reason=(
|
||||
'Invalid request msg:\n{msg}'
|
||||
))
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@acm
|
||||
async def subscribe(
|
||||
ws: NoBsWs,
|
||||
client: api.Client,
|
||||
token: str,
|
||||
subs: list[tuple[str, dict]] = [
|
||||
('ownTrades', {
|
||||
|
|
@ -316,12 +325,25 @@ async def subscribe(
|
|||
Setup ws api subscriptions:
|
||||
https://docs.kraken.com/websockets/#message-subscribe
|
||||
|
||||
By default we sign up for trade and order update events.
|
||||
By default we sign up for trade and order (update) events per
|
||||
`subs`.
|
||||
|
||||
'''
|
||||
# more specific logic for this in kraken's sync client:
|
||||
# https://github.com/krakenfx/kraken-wsclient-py/blob/master/kraken_wsclient_py/kraken_wsclient_py.py#L188
|
||||
assert token
|
||||
latest_token: str = await client.get_ws_token()
|
||||
if (
|
||||
token
|
||||
!=
|
||||
latest_token
|
||||
):
|
||||
log.info(
|
||||
f'RE-subscribing to WS connection..\n'
|
||||
f'orig-token: {token!r}\n'
|
||||
f'latest-token: {latest_token!r}\n'
|
||||
)
|
||||
token = latest_token
|
||||
|
||||
subnames: set[str] = set()
|
||||
|
||||
for name, sub_opts in subs:
|
||||
|
|
@ -329,7 +351,8 @@ async def subscribe(
|
|||
'event': 'subscribe',
|
||||
'subscription': {
|
||||
'name': name,
|
||||
'token': token,
|
||||
# 'token': await client.get_ws_token(),
|
||||
'token': latest_token,
|
||||
**sub_opts,
|
||||
}
|
||||
}
|
||||
|
|
@ -344,7 +367,9 @@ async def subscribe(
|
|||
# wait on subscriptionn acks
|
||||
with trio.move_on_after(5):
|
||||
while True:
|
||||
match (msg := await ws.recv_msg()):
|
||||
msg: dict = await ws.recv_msg()
|
||||
fmt_msg: str = ppfmt(msg)
|
||||
match msg:
|
||||
case {
|
||||
'event': 'subscriptionStatus',
|
||||
'status': 'subscribed',
|
||||
|
|
@ -362,10 +387,49 @@ async def subscribe(
|
|||
'event': 'subscriptionStatus',
|
||||
'status': 'error',
|
||||
'errorMessage': errmsg,
|
||||
'subscription': sub_opts,
|
||||
} as msg:
|
||||
raise RuntimeError(
|
||||
f'{errmsg}\n\n'
|
||||
f'{pformat(msg)}'
|
||||
if errmsg:
|
||||
etype_str, _, ev_msg = errmsg.partition(':')
|
||||
etype: Type[Exception] = getattr(
|
||||
api,
|
||||
etype_str,
|
||||
RuntimeError,
|
||||
)
|
||||
|
||||
exc = etype(
|
||||
f'{ev_msg}\n'
|
||||
f'\n'
|
||||
f'{fmt_msg}'
|
||||
)
|
||||
# !TODO, for `InvalidSession` we should
|
||||
# attempt retries to resub and ensure all
|
||||
# sibling (task) `token` holders update
|
||||
# their refs accoridingly!
|
||||
match (etype_str, ev_msg):
|
||||
case (
|
||||
'ESession',
|
||||
'Invalid session',
|
||||
):
|
||||
# attempt ws-token refresh
|
||||
token: str = await client.get_ws_token(
|
||||
force_renewal=True
|
||||
)
|
||||
await tractor.pause()
|
||||
continue
|
||||
|
||||
case _:
|
||||
log.warning(
|
||||
f'Unhandled subscription-status,\n'
|
||||
f'{fmt_msg}'
|
||||
)
|
||||
|
||||
raise exc
|
||||
|
||||
case _:
|
||||
log.warning(
|
||||
f'Unknown ws event rxed?\n'
|
||||
f'{fmt_msg}'
|
||||
)
|
||||
yield
|
||||
|
||||
|
|
@ -461,11 +525,27 @@ async def open_trade_dialog(
|
|||
# (much like the web UI let's you set an "account currency")
|
||||
# such that all positions (nested or flat) will be translated to
|
||||
# this source currency's terms.
|
||||
src_fiat = client.conf['src_fiat']
|
||||
src_fiat = client.conf.get('src_fiat')
|
||||
if not src_fiat:
|
||||
raise ConfigurationError(
|
||||
'No `src_fiat: str` field defined in `brokers.toml`'
|
||||
)
|
||||
|
||||
# auth required block
|
||||
acctid = client._name
|
||||
acc_name = 'kraken.' + acctid
|
||||
conf: dict = client.conf
|
||||
accounts: dict = conf.get('accounts')
|
||||
acctid: str = client._key_descr
|
||||
if not accounts.get(acctid):
|
||||
raise ConfigurationError(
|
||||
f'No API-key found for account-alias defined as {acctid!r} !\n'
|
||||
f'\n'
|
||||
f'Did set a `kraken.accounts.*` entry in your `brokers.toml`?\n'
|
||||
f'It should look something like,\n'
|
||||
f'\n'
|
||||
f'[kraken]\n'
|
||||
f'accounts.{acctid} = {acctid!r}\n'
|
||||
)
|
||||
fqan: str = f'kraken.{acctid}'
|
||||
|
||||
# task local msg dialog tracking
|
||||
apiflows = OrderDialogs()
|
||||
|
|
@ -584,7 +664,10 @@ async def open_trade_dialog(
|
|||
acctid,
|
||||
)
|
||||
# sync with EMS delivering pps and accounts
|
||||
await ctx.started((ppmsgs, [acc_name]))
|
||||
await ctx.started((
|
||||
ppmsgs,
|
||||
[fqan],
|
||||
))
|
||||
|
||||
# TODO: ideally this blocks the this task
|
||||
# as little as possible. we need to either do
|
||||
|
|
@ -592,14 +675,11 @@ async def open_trade_dialog(
|
|||
# async file IO api?
|
||||
acnt.write_config()
|
||||
|
||||
# Get websocket token for authenticated data stream
|
||||
# Assert that a token was actually received.
|
||||
resp = await client.endpoint('GetWebSocketsToken', {})
|
||||
if err := resp.get('error'):
|
||||
raise BrokerError(err)
|
||||
token: str = await client.get_ws_token()
|
||||
|
||||
# resp token for ws init
|
||||
token: str = resp['result']['token']
|
||||
# XXX tracks EMS orders which are updated too quickly
|
||||
# on the emds side with sync-issues on the kraken side.
|
||||
toofastedit: set[int] = set()
|
||||
|
||||
ws: NoBsWs
|
||||
async with (
|
||||
|
|
@ -608,23 +688,24 @@ async def open_trade_dialog(
|
|||
'wss://ws-auth.kraken.com/',
|
||||
fixture=partial(
|
||||
subscribe,
|
||||
client=client,
|
||||
token=token,
|
||||
),
|
||||
) as ws,
|
||||
aclosing(stream_messages(ws)) as stream,
|
||||
trio.open_nursery() as nurse,
|
||||
trio.open_nursery() as tn,
|
||||
):
|
||||
# task for processing inbound requests from ems
|
||||
nurse.start_soon(
|
||||
tn.start_soon(partial(
|
||||
handle_order_requests,
|
||||
ws,
|
||||
client,
|
||||
ems_stream,
|
||||
token,
|
||||
apiflows,
|
||||
ids,
|
||||
reqids2txids,
|
||||
)
|
||||
ws=ws,
|
||||
client=client,
|
||||
ems_order_stream=ems_stream,
|
||||
apiflows=apiflows,
|
||||
ids=ids,
|
||||
reqids2txids=reqids2txids,
|
||||
toofastedit=toofastedit,
|
||||
))
|
||||
|
||||
# enter relay loop
|
||||
await handle_order_updates(
|
||||
|
|
@ -635,22 +716,23 @@ async def open_trade_dialog(
|
|||
apiflows=apiflows,
|
||||
ids=ids,
|
||||
reqids2txids=reqids2txids,
|
||||
toofastedit=toofastedit,
|
||||
acnt=acnt,
|
||||
ledger=ledger,
|
||||
acctid=acctid,
|
||||
acc_name=acc_name,
|
||||
token=token,
|
||||
acc_name=fqan,
|
||||
)
|
||||
|
||||
|
||||
async def handle_order_updates(
|
||||
client: Client, # only for pairs table needed in ledger proc
|
||||
client: api.Client, # only for pairs table needed in ledger proc
|
||||
ws: NoBsWs,
|
||||
ws_stream: AsyncIterator,
|
||||
ems_stream: tractor.MsgStream,
|
||||
apiflows: OrderDialogs,
|
||||
ids: bidict[str, int],
|
||||
reqids2txids: bidict[int, str],
|
||||
toofastedit: set[int],
|
||||
acnt: Account,
|
||||
|
||||
# transaction records which will be updated
|
||||
|
|
@ -659,7 +741,6 @@ async def handle_order_updates(
|
|||
# ledger_trans: dict[str, Transaction],
|
||||
acctid: str,
|
||||
acc_name: str,
|
||||
token: str,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -789,7 +870,7 @@ async def handle_order_updates(
|
|||
for order_msg in order_msgs:
|
||||
log.info(
|
||||
f'`openOrders` msg update_{seq}:\n'
|
||||
f'{pformat(order_msg)}'
|
||||
f'{ppfmt(order_msg)}'
|
||||
)
|
||||
txid, update_msg = list(order_msg.items())[0]
|
||||
|
||||
|
|
@ -959,10 +1040,8 @@ async def handle_order_updates(
|
|||
# <-> ems dialog.
|
||||
if (
|
||||
status == 'open'
|
||||
and isinstance(
|
||||
reqids2txids.get(reqid),
|
||||
TooFastEdit
|
||||
)
|
||||
and
|
||||
reqid in toofastedit
|
||||
):
|
||||
# TODO: don't even allow this case
|
||||
# by not moving the client side line
|
||||
|
|
@ -977,7 +1056,8 @@ async def handle_order_updates(
|
|||
# https://docs.kraken.com/websockets/#message-cancelOrder
|
||||
await ws.send_msg({
|
||||
'event': 'cancelOrder',
|
||||
'token': token,
|
||||
# 'token': token,
|
||||
'token': await client.get_ws_token(),
|
||||
'reqid': reqid or 0,
|
||||
'txid': [txid],
|
||||
})
|
||||
|
|
@ -1123,7 +1203,8 @@ async def handle_order_updates(
|
|||
txid
|
||||
|
||||
# we throttle too-fast-requests on the ems side
|
||||
and not isinstance(txid, TooFastEdit)
|
||||
and
|
||||
reqid in toofastedit
|
||||
):
|
||||
# client was editting too quickly
|
||||
# so we instead cancel this order
|
||||
|
|
@ -1131,7 +1212,8 @@ async def handle_order_updates(
|
|||
f'Cancelling {reqid}@{txid} due to:\n {event}')
|
||||
await ws.send_msg({
|
||||
'event': 'cancelOrder',
|
||||
'token': token,
|
||||
# 'token': token,
|
||||
'token': await client.get_ws_token(),
|
||||
'reqid': reqid or 0,
|
||||
'txid': [txid],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ Symbology defs and search.
|
|||
|
||||
'''
|
||||
from decimal import Decimal
|
||||
from typing import (
|
||||
ClassVar,
|
||||
)
|
||||
|
||||
import tractor
|
||||
|
||||
|
|
@ -86,9 +89,14 @@ class Pair(Struct):
|
|||
short_position_limit: float = 0
|
||||
long_position_limit: float = float('inf')
|
||||
|
||||
# TODO, add API note when this was added!
|
||||
execution_venue: str|None = None
|
||||
|
||||
# TODO: should we make this a literal NamespacePath ref?
|
||||
ns_path: str = 'piker.brokers.kraken:Pair'
|
||||
|
||||
_api_url: ClassVar[str] = 'https://docs.kraken.com/api/docs/rest-api/get-tradable-asset-pairs'
|
||||
|
||||
@property
|
||||
def bs_mktid(self) -> str:
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ _time_frames = {
|
|||
class QuestradeError(Exception):
|
||||
"Non-200 OK response code"
|
||||
|
||||
from tractor._exceptions import reg_err_types
|
||||
reg_err_types([QuestradeError])
|
||||
|
||||
|
||||
class ContractsKey(NamedTuple):
|
||||
symbol: str
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ from collections import (
|
|||
from contextlib import asynccontextmanager as acm
|
||||
from decimal import Decimal
|
||||
from math import isnan
|
||||
from pprint import pformat
|
||||
from time import time_ns
|
||||
from types import ModuleType
|
||||
from typing import (
|
||||
|
|
@ -43,6 +42,7 @@ import trio
|
|||
from trio_typing import TaskStatus
|
||||
import tractor
|
||||
from tractor import trionics
|
||||
from tractor.devx.pformat import ppfmt
|
||||
|
||||
from ._util import (
|
||||
log, # sub-sys logger
|
||||
|
|
@ -159,11 +159,11 @@ class DarkBook(Struct):
|
|||
_DEFAULT_SIZE: float = 1.0
|
||||
|
||||
|
||||
async def clear_dark_triggers(
|
||||
async def _clear_dark_triggers(
|
||||
|
||||
router: Router,
|
||||
brokerd_orders_stream: tractor.MsgStream,
|
||||
quote_stream: tractor.MsgStream,
|
||||
quote_stream: trionics.AsyncReceiver,
|
||||
broker: str,
|
||||
fqme: str,
|
||||
|
||||
|
|
@ -182,7 +182,6 @@ async def clear_dark_triggers(
|
|||
# - port to the new ringbuf stuff in `tractor.ipc`!
|
||||
# - numba all this!
|
||||
# - this stream may eventually contain multiple symbols
|
||||
quote_stream._raise_on_lag = False
|
||||
async for quotes in quote_stream:
|
||||
# start = time.time()
|
||||
for sym, quote in quotes.items():
|
||||
|
|
@ -314,6 +313,32 @@ async def clear_dark_triggers(
|
|||
# print(f'execs scan took: {time.time() - start}')
|
||||
|
||||
|
||||
async def clear_dark_triggers(
|
||||
router: Router,
|
||||
brokerd_orders_stream: tractor.MsgStream,
|
||||
quote_stream: tractor.MsgStream,
|
||||
broker: str,
|
||||
fqme: str,
|
||||
book: DarkBook,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Run dark clearing on a dedicated non-strict quote subscription.
|
||||
|
||||
'''
|
||||
async with quote_stream.subscribe(
|
||||
raise_on_lag=False,
|
||||
) as quotes_stream:
|
||||
await _clear_dark_triggers(
|
||||
router,
|
||||
brokerd_orders_stream,
|
||||
quotes_stream,
|
||||
broker,
|
||||
fqme,
|
||||
book,
|
||||
)
|
||||
|
||||
|
||||
class TradesRelay(Struct):
|
||||
|
||||
# for now we keep only a single connection open with
|
||||
|
|
@ -335,9 +360,14 @@ class TradesRelay(Struct):
|
|||
@acm
|
||||
async def open_brokerd_dialog(
|
||||
brokermod: ModuleType,
|
||||
portal: tractor.Portal,
|
||||
exec_mode: str,
|
||||
fqme: str|None = None,
|
||||
|
||||
# XXX: explicit (already spawned) trading-actor override,
|
||||
# currently only used by the `piker ledger` cli which
|
||||
# boots its own ad-hoc `brokerd`-like actor; normally we
|
||||
# (lazily) spawn/find the `brokerd.<broker>` daemon here.
|
||||
portal: tractor.Portal|None = None,
|
||||
loglevel: str|None = None,
|
||||
|
||||
) -> tuple[
|
||||
|
|
@ -351,6 +381,10 @@ async def open_brokerd_dialog(
|
|||
paper engine instance depending on live trading support for the
|
||||
broker backend, configuration, or client code usage.
|
||||
|
||||
NOTE: this is now the ONE place where a (live, credentialed)
|
||||
`brokerd.<broker>` daemon-actor gets (lazily) booted; pure
|
||||
data/paper sessions should never spawn one!
|
||||
|
||||
'''
|
||||
get_console_log(
|
||||
level=loglevel,
|
||||
|
|
@ -416,16 +450,29 @@ async def open_brokerd_dialog(
|
|||
)
|
||||
exec_mode: str = 'paper'
|
||||
|
||||
if (
|
||||
trades_endpoint is not None
|
||||
or
|
||||
exec_mode != 'paper'
|
||||
):
|
||||
# open live brokerd trades endpoint
|
||||
open_trades_endpoint = portal.open_context(
|
||||
trades_endpoint,
|
||||
loglevel=loglevel,
|
||||
@acm
|
||||
async def acquire_live_portal():
|
||||
'''
|
||||
Deliver a portal to the (live, credentialed) trading
|
||||
actor hosting the backend's `open_trade_dialog()` ep:
|
||||
either the caller-provided override or the (maybe
|
||||
lazily spawned) `brokerd.<broker>` service daemon.
|
||||
|
||||
'''
|
||||
if portal is not None:
|
||||
yield portal
|
||||
return
|
||||
|
||||
# XXX: the ONE (normal) place a `brokerd.<broker>`
|
||||
# daemon-actor gets booted in the runtime B)
|
||||
from piker.brokers._daemon import (
|
||||
maybe_spawn_brokerd,
|
||||
)
|
||||
async with maybe_spawn_brokerd(
|
||||
brokermod.name,
|
||||
loglevel=loglevel,
|
||||
) as live_portal:
|
||||
yield live_portal
|
||||
|
||||
@acm
|
||||
async def maybe_open_paper_ep():
|
||||
|
|
@ -437,7 +484,14 @@ async def open_brokerd_dialog(
|
|||
return
|
||||
|
||||
# open trades-dialog endpoint with backend broker
|
||||
async with open_trades_endpoint as msg:
|
||||
async with (
|
||||
acquire_live_portal() as live_portal,
|
||||
|
||||
live_portal.open_context(
|
||||
trades_endpoint,
|
||||
loglevel=loglevel,
|
||||
) as msg,
|
||||
):
|
||||
ctx, first = msg
|
||||
|
||||
# runtime indication that the backend can't support live
|
||||
|
|
@ -490,7 +544,7 @@ async def open_brokerd_dialog(
|
|||
msg = BrokerdPosition(**msg)
|
||||
log.info(
|
||||
f'loading pp for {brokermod.__name__}:\n'
|
||||
f'{pformat(msg.to_dict())}',
|
||||
f'{ppfmt(msg.to_dict())}',
|
||||
)
|
||||
|
||||
# TODO: state any mismatch here?
|
||||
|
|
@ -581,7 +635,6 @@ class Router(Struct):
|
|||
async def maybe_open_brokerd_dialog(
|
||||
self,
|
||||
brokermod: ModuleType,
|
||||
portal: tractor.Portal,
|
||||
exec_mode: str,
|
||||
fqme: str,
|
||||
loglevel: str,
|
||||
|
|
@ -606,7 +659,6 @@ class Router(Struct):
|
|||
|
||||
async with open_brokerd_dialog(
|
||||
brokermod=brokermod,
|
||||
portal=portal,
|
||||
exec_mode=exec_mode,
|
||||
fqme=fqme,
|
||||
loglevel=loglevel,
|
||||
|
|
@ -668,7 +720,6 @@ class Router(Struct):
|
|||
brokername, _, _, _ = unpack_fqme(fqme)
|
||||
brokermod = feed.mods[brokername]
|
||||
broker = brokermod.name
|
||||
portal = feed.portals[brokermod]
|
||||
|
||||
# XXX: this should be initial price quote from target provider
|
||||
flume = feed.flumes[fqme]
|
||||
|
|
@ -682,7 +733,6 @@ class Router(Struct):
|
|||
|
||||
async with self.maybe_open_brokerd_dialog(
|
||||
brokermod=brokermod,
|
||||
portal=portal,
|
||||
exec_mode=exec_mode,
|
||||
fqme=fqme,
|
||||
loglevel=loglevel,
|
||||
|
|
@ -840,7 +890,7 @@ async def translate_and_relay_brokerd_events(
|
|||
|
||||
brokerd_msg: dict[str, Any]
|
||||
async for brokerd_msg in brokerd_trades_stream:
|
||||
fmsg = pformat(brokerd_msg)
|
||||
fmsg = ppfmt(brokerd_msg)
|
||||
log.info(
|
||||
f'Rx brokerd trade msg:\n'
|
||||
f'{fmsg}'
|
||||
|
|
@ -1039,6 +1089,7 @@ async def translate_and_relay_brokerd_events(
|
|||
)
|
||||
|
||||
status_msg.reqid = reqid # THIS LINE IS CRITICAL!
|
||||
if not status_msg.brokerd_msg:
|
||||
status_msg.brokerd_msg = msg
|
||||
status_msg.src = msg.broker_details['name']
|
||||
|
||||
|
|
@ -1072,7 +1123,7 @@ async def translate_and_relay_brokerd_events(
|
|||
else: # open
|
||||
# relayed from backend but probably not handled so
|
||||
# just log it
|
||||
log.info(f'{broker} opened order {msg}')
|
||||
log.info(f'{broker!r} opened order {msg}')
|
||||
|
||||
# BrokerdFill
|
||||
case {
|
||||
|
|
@ -1185,7 +1236,7 @@ async def translate_and_relay_brokerd_events(
|
|||
}:
|
||||
msg = (
|
||||
f'Unhandled broker status for dialog {reqid}:\n'
|
||||
f'{pformat(brokerd_msg)}'
|
||||
f'{ppfmt(brokerd_msg)}'
|
||||
)
|
||||
if (
|
||||
oid := book._ems2brokerd_ids.inverse.get(reqid)
|
||||
|
|
@ -1194,7 +1245,7 @@ async def translate_and_relay_brokerd_events(
|
|||
# clearable limits..
|
||||
if status_msg := book._active.get(oid):
|
||||
msg += (
|
||||
f'last status msg: {pformat(status_msg)}\n\n'
|
||||
f'last status msg: {ppfmt(status_msg)}\n\n'
|
||||
f'this msg:{fmsg}\n'
|
||||
)
|
||||
|
||||
|
|
@ -1233,7 +1284,7 @@ async def process_client_order_cmds(
|
|||
async for cmd in client_order_stream:
|
||||
log.info(
|
||||
f'Received order cmd:\n'
|
||||
f'{pformat(cmd)}\n'
|
||||
f'{ppfmt(cmd)}\n'
|
||||
)
|
||||
|
||||
# CAWT DAMN we need struct support!
|
||||
|
|
@ -1398,8 +1449,8 @@ async def process_client_order_cmds(
|
|||
# handle relaying the ems side responses back to
|
||||
# the client/cmd sender from this request
|
||||
log.info(
|
||||
f'Sending live order to {broker}:\n'
|
||||
f'{pformat(msg)}'
|
||||
f'Sending live order to {broker!r}:\n'
|
||||
f'{ppfmt(msg)}'
|
||||
)
|
||||
|
||||
await brokerd_order_stream.send(msg)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from types import ModuleType
|
|||
import click
|
||||
import trio
|
||||
import tractor
|
||||
from tractor._multiaddr import parse_maddr
|
||||
from tractor.discovery import parse_endpoints
|
||||
|
||||
from ..log import (
|
||||
get_console_log,
|
||||
|
|
@ -46,48 +46,25 @@ log = get_logger('piker.cli')
|
|||
|
||||
|
||||
def load_trans_eps(
|
||||
network: dict | None = None,
|
||||
maddrs: list[tuple] | None = None,
|
||||
network: dict|None = None,
|
||||
maddrs: list[str]|None = None,
|
||||
|
||||
) -> dict[str, dict[str, dict]]:
|
||||
) -> dict[str, list]:
|
||||
'''
|
||||
Load transport endpoints from a `[network]` config
|
||||
table or CLI-provided multiaddr strings, delegating
|
||||
to `tractor.discovery.parse_endpoints()`.
|
||||
|
||||
# transport-oriented endpoint multi-addresses
|
||||
eps: dict[
|
||||
str, # service name, eg. `pikerd`, `emsd`..
|
||||
|
||||
# libp2p style multi-addresses parsed into prot layers
|
||||
list[dict[str, str | int]]
|
||||
] = {}
|
||||
|
||||
if (
|
||||
network
|
||||
and
|
||||
not maddrs
|
||||
):
|
||||
# load network section and (attempt to) connect all endpoints
|
||||
# which are reachable B)
|
||||
for key, maddrs in network.items():
|
||||
match key:
|
||||
|
||||
# TODO: resolve table across multiple discov
|
||||
# prots Bo
|
||||
case 'resolv':
|
||||
pass
|
||||
|
||||
case 'pikerd':
|
||||
dname: str = key
|
||||
for maddr in maddrs:
|
||||
layers: dict = parse_maddr(maddr)
|
||||
eps.setdefault(
|
||||
dname,
|
||||
[],
|
||||
).append(layers)
|
||||
'''
|
||||
if network and not maddrs:
|
||||
return parse_endpoints(network)
|
||||
|
||||
elif maddrs:
|
||||
# presume user is manually specifying the root actor ep.
|
||||
eps['pikerd'] = [parse_maddr(maddr)]
|
||||
return parse_endpoints(
|
||||
{'pikerd': list(maddrs)}
|
||||
)
|
||||
|
||||
return eps
|
||||
return {}
|
||||
|
||||
|
||||
@click.command()
|
||||
|
|
@ -108,13 +85,13 @@ def load_trans_eps(
|
|||
help='Enable tractor debug mode',
|
||||
)
|
||||
@click.option(
|
||||
'--maddr',
|
||||
'--maddrs',
|
||||
'-m',
|
||||
default=None,
|
||||
help='Multiaddrs to bind or contact',
|
||||
)
|
||||
def pikerd(
|
||||
maddr: list[str] | None,
|
||||
maddrs: list[str]|None,
|
||||
loglevel: str,
|
||||
tl: bool,
|
||||
pdb: bool,
|
||||
|
|
@ -145,7 +122,8 @@ def pikerd(
|
|||
))
|
||||
|
||||
# service-actor registry endpoint socket-address set
|
||||
regaddrs: list[tuple[str, int]] = []
|
||||
regaddrs: list = []
|
||||
tpt_bind_addrs: list|None = None
|
||||
|
||||
conf, _ = config.load(
|
||||
conf_name='conf',
|
||||
|
|
@ -153,7 +131,8 @@ def pikerd(
|
|||
network: dict = conf.get('network')
|
||||
if (
|
||||
network is None
|
||||
and not maddr
|
||||
and
|
||||
not maddrs
|
||||
):
|
||||
regaddrs = [(
|
||||
_default_registry_host,
|
||||
|
|
@ -161,15 +140,23 @@ def pikerd(
|
|||
)]
|
||||
|
||||
else:
|
||||
eps: dict = load_trans_eps(
|
||||
from tractor.devx import maybe_open_crash_handler
|
||||
with maybe_open_crash_handler(pdb=pdb):
|
||||
eps: dict[str, list] = load_trans_eps(
|
||||
network,
|
||||
maddr,
|
||||
maddrs,
|
||||
)
|
||||
|
||||
# pikerd bind addresses (may differ from
|
||||
# registry if regd is separate)
|
||||
tpt_bind_addrs = eps.get('pikerd')
|
||||
|
||||
# registry: dedicated `regd` key, or fall
|
||||
# back to pikerd addrs (pikerd IS the registry)
|
||||
regaddrs = eps.get(
|
||||
'regd',
|
||||
tpt_bind_addrs,
|
||||
)
|
||||
for layers in eps['pikerd']:
|
||||
regaddrs.append((
|
||||
layers['ipv4']['addr'],
|
||||
layers['tcp']['port'],
|
||||
))
|
||||
|
||||
from .. import service
|
||||
|
||||
|
|
@ -178,6 +165,7 @@ def pikerd(
|
|||
async with (
|
||||
service.open_pikerd(
|
||||
registry_addrs=regaddrs,
|
||||
tpt_bind_addrs=tpt_bind_addrs,
|
||||
loglevel=loglevel,
|
||||
debug_mode=pdb,
|
||||
# enable_transports=['uds'],
|
||||
|
|
@ -262,7 +250,6 @@ def cli(
|
|||
|
||||
# TODO: load endpoints from `conf::[network].pikerd`
|
||||
# - pikerd vs. regd, separate registry daemon?
|
||||
# - expose datad vs. brokerd?
|
||||
# - bind emsd with certain perms on public iface?
|
||||
regaddrs: list[tuple[str, int]] = regaddr or [(
|
||||
_default_registry_host,
|
||||
|
|
@ -345,7 +332,7 @@ def services(
|
|||
if not ports:
|
||||
ports: list[int] = [_default_registry_port]
|
||||
|
||||
addr = tractor._addr.wrap_address(
|
||||
addr = tractor.discovery._addr.wrap_address(
|
||||
addr=(host, ports[0])
|
||||
)
|
||||
|
||||
|
|
|
|||
120
piker/config.py
120
piker/config.py
|
|
@ -36,6 +36,8 @@ except ModuleNotFoundError:
|
|||
import tomli as tomllib
|
||||
|
||||
|
||||
from tractor._exceptions import reg_err_types
|
||||
from tractor.devx.pformat import ppfmt
|
||||
from .log import get_logger
|
||||
|
||||
log = get_logger('broker-config')
|
||||
|
|
@ -172,6 +174,12 @@ class ConfigurationError(Exception):
|
|||
class NoSignature(ConfigurationError):
|
||||
'No credentials setup for broker backend!'
|
||||
|
||||
# auto-register for tractor IPC exc-marshalling.
|
||||
reg_err_types([
|
||||
ConfigurationError,
|
||||
*ConfigurationError.__subclasses__(),
|
||||
])
|
||||
|
||||
|
||||
def _override_config_dir(
|
||||
path: str
|
||||
|
|
@ -180,6 +188,48 @@ def _override_config_dir(
|
|||
_config_dir = path
|
||||
|
||||
|
||||
def _maybe_use_test_dir() -> None:
|
||||
'''
|
||||
When running under the `pytest` harness, override
|
||||
the config dir to the per-test-tmp dir "passed down"
|
||||
the actor tree via `tractor`'s runtime-vars
|
||||
inheritance mechanism.
|
||||
|
||||
See the `tractor_runtime_overrides` usage in our
|
||||
`tests.conftest._open_test_pikerd()` as well as
|
||||
`.service._actor_runtime.open_piker_runtime()` for
|
||||
the root-actor's pre-loading of the var state.
|
||||
|
||||
NOTE: this must be checked lazily at config-path
|
||||
access time (NOT import time) since sub-actors only
|
||||
receive runtime-vars once their `tractor` runtime
|
||||
has fully booted.
|
||||
|
||||
'''
|
||||
global _config_dir
|
||||
import tractor
|
||||
actor = tractor.current_actor(
|
||||
err_on_no_runtime=False,
|
||||
)
|
||||
if actor is None:
|
||||
return
|
||||
|
||||
rvs: dict = tractor.runtime._state._runtime_vars
|
||||
pvars: dict|None = rvs.get('piker_vars')
|
||||
if (
|
||||
pvars
|
||||
and
|
||||
(testdir := pvars.get('piker_test_dir'))
|
||||
):
|
||||
testdirpath = Path(testdir)
|
||||
assert testdirpath.exists(), (
|
||||
f'piker test harness might be borked!?\n'
|
||||
f'testdirpath: {testdirpath!r}\n'
|
||||
)
|
||||
if _config_dir != testdirpath:
|
||||
_override_config_dir(testdirpath)
|
||||
|
||||
|
||||
def _conf_fn_w_ext(
|
||||
name: str,
|
||||
) -> str:
|
||||
|
|
@ -193,6 +243,7 @@ def get_conf_dir() -> Path:
|
|||
on the local filesystem.
|
||||
|
||||
'''
|
||||
_maybe_use_test_dir()
|
||||
return _config_dir
|
||||
|
||||
|
||||
|
|
@ -218,7 +269,7 @@ def get_conf_path(
|
|||
assert str(conf_name) in _conf_names
|
||||
|
||||
fn = _conf_fn_w_ext(conf_name)
|
||||
return _config_dir / Path(fn)
|
||||
return get_conf_dir() / Path(fn)
|
||||
|
||||
|
||||
def repodir() -> Path:
|
||||
|
|
@ -263,8 +314,9 @@ def load(
|
|||
|
||||
'''
|
||||
# create the $HOME/.config/piker dir if dne
|
||||
if not _config_dir.is_dir():
|
||||
_config_dir.mkdir(
|
||||
conf_dir: Path = get_conf_dir()
|
||||
if not conf_dir.is_dir():
|
||||
conf_dir.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
|
@ -350,30 +402,56 @@ def write(
|
|||
|
||||
|
||||
def load_accounts(
|
||||
providers: list[str] | None = None
|
||||
|
||||
) -> bidict[str, str | None]:
|
||||
providers: list[str]|None = None
|
||||
) -> bidict[str, str|None]:
|
||||
|
||||
conf, path = load(
|
||||
conf_name='brokers',
|
||||
)
|
||||
accounts = bidict()
|
||||
for provider_name, section in conf.items():
|
||||
accounts_section = section.get('accounts')
|
||||
if (
|
||||
providers is None or
|
||||
providers and provider_name in providers
|
||||
):
|
||||
accounts = bidict({
|
||||
# XXX, default paper-engine entry; this MUST be set.
|
||||
'paper': None,
|
||||
})
|
||||
msg: str = (
|
||||
'Loading account(s) from `brokers.toml`,\n'
|
||||
)
|
||||
for (
|
||||
provider_name,
|
||||
section,
|
||||
) in conf.items():
|
||||
accounts_section: dict[str, str] = section.get('accounts')
|
||||
if accounts_section is None:
|
||||
log.warning(f'No accounts named for {provider_name}?')
|
||||
msg += f'No accounts declared for {provider_name!r}?\n'
|
||||
continue
|
||||
|
||||
# msg += f'Loaded accounts for {provider_name!r}?\n'
|
||||
if (
|
||||
providers is None
|
||||
or (
|
||||
providers
|
||||
and
|
||||
provider_name in providers
|
||||
)
|
||||
):
|
||||
for (
|
||||
label,
|
||||
value,
|
||||
) in accounts_section.items():
|
||||
account_alias: str = f'{provider_name}.{label}'
|
||||
accounts[account_alias] = value
|
||||
msg += f'{account_alias} = {value!r}\n'
|
||||
|
||||
else:
|
||||
for label, value in accounts_section.items():
|
||||
accounts[
|
||||
f'{provider_name}.{label}'
|
||||
] = value
|
||||
|
||||
# our default paper engine entry
|
||||
accounts['paper'] = None
|
||||
log.debug(
|
||||
f'NOT loading account(s) for entry in `brokers.toml`,\n'
|
||||
f'The account provider was not requested for loading.\n'
|
||||
f'requested-providers: {providers!r}\n'
|
||||
f'this-provider: {provider_name!r}\n'
|
||||
f'\n'
|
||||
f'{ppfmt(accounts_section)}\n'
|
||||
)
|
||||
# ?TODO? mk this bp work?
|
||||
# breakpoint()
|
||||
|
||||
log.info(msg)
|
||||
return accounts
|
||||
|
|
|
|||
|
|
@ -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/>.
|
||||
|
||||
'''
|
||||
Data-daemon-actor "endpoint-hooks": the service task entry
|
||||
points for `datad.<brokername>`, the per-provider real-time
|
||||
and historical market-data feed daemon.
|
||||
|
||||
The (data vs. broker)d-split mirrors the ep-groupings in
|
||||
`piker.data.validate._eps`: this daemon hosts all `'datad'`
|
||||
eps (live quotes, history loading, symbology search) while
|
||||
its `brokerd.<brokername>` sibling hosts only the live
|
||||
order-control (and thus credentialed) `'brokerd'` eps.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from contextlib import (
|
||||
asynccontextmanager as acm,
|
||||
)
|
||||
from types import ModuleType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
AsyncContextManager,
|
||||
)
|
||||
|
||||
import tractor
|
||||
import trio
|
||||
|
||||
from piker.log import (
|
||||
get_logger,
|
||||
get_console_log,
|
||||
)
|
||||
from . import _util
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .feed import _FeedsBus
|
||||
|
||||
log = get_logger(name=__name__)
|
||||
|
||||
# `datad`-actor-always-enabled mods: the data-side successor
|
||||
# to the old `piker.brokers._daemon._data_mods` set.
|
||||
# NOTE: keeping this list as small as possible is part of
|
||||
# our caps-sec model and should be treated with utmost care!
|
||||
_datad_service_mods: list[str] = [
|
||||
'piker.brokers.core',
|
||||
'piker.brokers.data',
|
||||
'piker.data',
|
||||
'piker.data.feed',
|
||||
'piker.data._sampling',
|
||||
'piker.data._daemon',
|
||||
]
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def _setup_persistent_datad(
|
||||
ctx: tractor.Context,
|
||||
brokername: str,
|
||||
loglevel: str|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Allocate an actor-wide service nursery in this
|
||||
`datad.<brokername>` actor such that data-feed tasks
|
||||
(shm writer-samplers, history backfillers, symbology
|
||||
loaders) can be run in the background persistently by
|
||||
the provider backend as needed.
|
||||
|
||||
'''
|
||||
# NOTE: we only need to setup logging once (and only)
|
||||
# here since all hosted daemon tasks will reference
|
||||
# this same log instance's (actor local) state and thus
|
||||
# don't require any further (level) configuration on
|
||||
# their own B)
|
||||
actor: tractor.Actor = tractor.current_actor()
|
||||
tll: str = actor.loglevel
|
||||
log = get_console_log(
|
||||
level=loglevel or tll,
|
||||
name=f'{_util.subsys}.{brokername}',
|
||||
with_tractor_log=bool(tll),
|
||||
)
|
||||
assert log.name == _util.subsys
|
||||
|
||||
# XXX: ALSO enable console logging for the provider
|
||||
# backend's mod subtree (eg. `piker.brokers.ib.*`)
|
||||
# since (pre the datad|brokerd-split) that was done
|
||||
# by `brokerd`'s fixture; without this all backend
|
||||
# records here fall through to the stdlib's bare
|
||||
# (non-colorized) `logging.lastResort` handler!
|
||||
get_console_log(
|
||||
level=loglevel or tll,
|
||||
name=f'piker.brokers.{brokername}',
|
||||
with_tractor_log=bool(tll),
|
||||
)
|
||||
|
||||
from piker.data import feed
|
||||
assert not feed._bus
|
||||
|
||||
# allocate a nursery to the bus for spawning background
|
||||
# tasks which service client IPC requests, normally
|
||||
# `tractor.Context` connections to explicitly required
|
||||
# `datad` endpoints such as:
|
||||
# - `stream_quotes()`,
|
||||
# - `manage_history()`,
|
||||
# - `allocate_persistent_feed()`,
|
||||
# - `open_symbol_search()`
|
||||
# NOTE: see ep invocation details inside `.data.feed`.
|
||||
async with (
|
||||
trio.open_nursery() as service_nursery
|
||||
):
|
||||
bus: _FeedsBus = feed.get_feed_bus(
|
||||
brokername,
|
||||
service_nursery,
|
||||
)
|
||||
assert bus is feed._bus
|
||||
|
||||
# unblock caller
|
||||
await ctx.started()
|
||||
|
||||
# we pin this task to keep the feeds manager active
|
||||
# until the parent actor decides to tear it down
|
||||
await trio.sleep_forever()
|
||||
|
||||
|
||||
def datad_init(
|
||||
brokername: str,
|
||||
loglevel: str|None = None,
|
||||
|
||||
**start_actor_kwargs,
|
||||
|
||||
) -> tuple[
|
||||
ModuleType,
|
||||
dict,
|
||||
AsyncContextManager,
|
||||
]:
|
||||
'''
|
||||
Given an input broker name, load all named arguments
|
||||
which can be passed for daemon endpoint + context spawn
|
||||
as required in every `datad.<brokername>` (actor)
|
||||
service.
|
||||
|
||||
This includes:
|
||||
- load the appropriate <brokername>.py pkg module,
|
||||
- reads any declared `_datad_mods: list[str]` (falling
|
||||
back to the full `__enable_modules__` set for
|
||||
not-yet-split backends) which will be passed to
|
||||
`tractor.ActorNursery.start_actor(enable_modules=)`
|
||||
at actor start time,
|
||||
- deliver a reference to the daemon lifetime fixture,
|
||||
which for now is always the
|
||||
`_setup_persistent_datad()` context defined above.
|
||||
|
||||
'''
|
||||
from piker.brokers import get_brokermod
|
||||
from .validate import get_eps
|
||||
brokermod = get_brokermod(brokername)
|
||||
modpath: str = brokermod.__name__
|
||||
|
||||
# warn (but don't bail) when the backend is missing
|
||||
# some/all of the `datad` ep contract defined by
|
||||
# `piker.data.validate._eps`.
|
||||
datad_eps: dict = get_eps(brokermod, 'datad')
|
||||
if not datad_eps:
|
||||
log.warning(
|
||||
f'Backend {brokername!r} offers NO `datad` '
|
||||
f'(data-feed) eps!?\n'
|
||||
f'Most feed/chart functionality will be '
|
||||
f'broken for this provider..\n'
|
||||
)
|
||||
|
||||
start_actor_kwargs['name'] = f'datad.{brokername}'
|
||||
|
||||
# XXX CRITICAL: include any backend-declared spawn
|
||||
# kwargs, eg. `{'infect_asyncio': True}` required by
|
||||
# `ib`'s embedded `asyncio`-mode `ib_async` usage!
|
||||
start_actor_kwargs.update(
|
||||
getattr(
|
||||
brokermod,
|
||||
'_spawn_kwargs',
|
||||
{},
|
||||
)
|
||||
)
|
||||
|
||||
# lookup actor-enabled modules declared by the backend
|
||||
# offering the `datad` endpoint(s).
|
||||
enabled: list[str]
|
||||
enabled = start_actor_kwargs['enable_modules'] = [
|
||||
__name__, # so that eps from THIS mod can be invoked
|
||||
modpath,
|
||||
]
|
||||
for submodname in getattr(
|
||||
brokermod,
|
||||
'_datad_mods',
|
||||
# fallback for (flat, less mature) backends which
|
||||
# don't yet declare a daemon-kind mod split.
|
||||
getattr(
|
||||
brokermod,
|
||||
'__enable_modules__',
|
||||
[],
|
||||
),
|
||||
):
|
||||
subpath: str = f'{modpath}.{submodname}'
|
||||
enabled.append(subpath)
|
||||
|
||||
return (
|
||||
brokermod,
|
||||
start_actor_kwargs, # to `ActorNursery.start_actor()`
|
||||
|
||||
# XXX see impl above; contains all (actor global)
|
||||
# setup/teardown expected in all `datad` actor
|
||||
# instances.
|
||||
_setup_persistent_datad,
|
||||
)
|
||||
|
||||
|
||||
async def spawn_datad(
|
||||
brokername: str,
|
||||
loglevel: str|None = None,
|
||||
|
||||
**tractor_kwargs,
|
||||
|
||||
) -> bool:
|
||||
|
||||
log.info(
|
||||
f'Spawning data-daemon,\n'
|
||||
f'backend: {brokername!r}'
|
||||
)
|
||||
|
||||
(
|
||||
brokermod,
|
||||
tractor_kwargs,
|
||||
daemon_fixture_ep,
|
||||
) = datad_init(
|
||||
brokername,
|
||||
loglevel,
|
||||
**tractor_kwargs,
|
||||
)
|
||||
|
||||
# ask `pikerd` to spawn a new sub-actor and manage it
|
||||
# under its actor nursery
|
||||
from piker.service import Services
|
||||
|
||||
dname: str = tractor_kwargs.pop('name') # f'datad.{brokername}'
|
||||
enable_mods: list[str] = list(dict.fromkeys(
|
||||
_datad_service_mods
|
||||
+
|
||||
tractor_kwargs.pop('enable_modules')
|
||||
))
|
||||
portal = await Services.actor_n.start_actor(
|
||||
dname,
|
||||
enable_modules=enable_mods,
|
||||
debug_mode=Services.debug_mode,
|
||||
**tractor_kwargs,
|
||||
)
|
||||
|
||||
# NOTE: the service mngr expects an already spawned
|
||||
# actor + its portal ref in order to do non-blocking
|
||||
# setup of the `datad` service nursery.
|
||||
await Services.start_service_task(
|
||||
dname,
|
||||
portal,
|
||||
|
||||
# signature of target root-task endpoint
|
||||
daemon_fixture_ep,
|
||||
brokername=brokername,
|
||||
loglevel=loglevel,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@acm
|
||||
async def maybe_spawn_datad(
|
||||
brokername: str,
|
||||
loglevel: str|None = None,
|
||||
|
||||
**pikerd_kwargs,
|
||||
|
||||
) -> tractor.Portal:
|
||||
'''
|
||||
Helper to spawn a datad service *from* a client who
|
||||
wishes to use the sub-actor-daemon but is fine with
|
||||
re-using any existing and contactable `datad`.
|
||||
|
||||
Mas o menos, acts as a cached-actor-getter factory.
|
||||
|
||||
'''
|
||||
from piker.service import maybe_spawn_daemon
|
||||
|
||||
async with maybe_spawn_daemon(
|
||||
service_name=f'datad.{brokername}',
|
||||
service_task_target=spawn_datad,
|
||||
spawn_args={
|
||||
'brokername': brokername,
|
||||
},
|
||||
loglevel=loglevel,
|
||||
|
||||
**pikerd_kwargs,
|
||||
|
||||
) as portal:
|
||||
yield portal
|
||||
|
|
@ -168,7 +168,7 @@ async def _reconnect_forever(
|
|||
nobsws: NoBsWs,
|
||||
reset_after: int, # msg recv timeout before reset attempt
|
||||
|
||||
fixture: AsyncContextManager | None = None,
|
||||
fixture: AsyncContextManager|None = None,
|
||||
task_status: TaskStatus = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
) -> None:
|
||||
|
|
@ -185,7 +185,7 @@ async def _reconnect_forever(
|
|||
async def proxy_msgs(
|
||||
ws: WebSocketConnection,
|
||||
rent_cs: trio.CancelScope, # parent cancel scope
|
||||
):
|
||||
) -> None:
|
||||
'''
|
||||
Receive (under `timeout` deadline) all msgs from from underlying
|
||||
websocket and relay them to (calling) parent task via ``trio``
|
||||
|
|
@ -206,7 +206,7 @@ async def _reconnect_forever(
|
|||
except nobsws.recon_errors:
|
||||
log.exception(
|
||||
f'{src_mod}\n'
|
||||
f'{url} connection bail with:'
|
||||
f'{url!r} connection failed\n'
|
||||
)
|
||||
with trio.CancelScope(shield=True):
|
||||
await trio.sleep(0.5)
|
||||
|
|
@ -269,7 +269,7 @@ async def _reconnect_forever(
|
|||
nobsws._ws = ws
|
||||
log.info(
|
||||
f'{src_mod}\n'
|
||||
f'Connection success: {url}'
|
||||
f'Connection success: {url!r}'
|
||||
)
|
||||
|
||||
# begin relay loop to forward msgs
|
||||
|
|
@ -341,7 +341,7 @@ async def _reconnect_forever(
|
|||
async def open_autorecon_ws(
|
||||
url: str,
|
||||
|
||||
fixture: AsyncContextManager | None = None,
|
||||
fixture: AsyncContextManager|None = None,
|
||||
|
||||
# time in sec between msgs received before
|
||||
# we presume connection might need a reset.
|
||||
|
|
@ -361,7 +361,7 @@ async def open_autorecon_ws(
|
|||
and restarts the full http(s) handshake on catches of certain
|
||||
connetivity errors, or some user defined recv timeout.
|
||||
|
||||
You can provide a ``fixture`` async-context-manager which will be
|
||||
You can provide a `fixture` async-context-manager which will be
|
||||
entered/exitted around each connection reset; eg. for
|
||||
(re)requesting subscriptions without requiring streaming setup
|
||||
code to rerun.
|
||||
|
|
@ -402,7 +402,8 @@ async def open_autorecon_ws(
|
|||
except NoBsWs.recon_errors as con_err:
|
||||
log.warning(
|
||||
f'Entire ws-channel disconnect due to,\n'
|
||||
f'con_err: {con_err!r}\n'
|
||||
f'\n'
|
||||
f'{con_err!r}\n'
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -424,7 +425,7 @@ class JSONRPCResult(Struct):
|
|||
async def open_jsonrpc_session(
|
||||
url: str,
|
||||
start_id: int = 0,
|
||||
response_type: type = JSONRPCResult,
|
||||
response_type: Type[Struct] = JSONRPCResult,
|
||||
msg_recv_timeout: float = float('inf'),
|
||||
# ^NOTE, since only `deribit` is using this jsonrpc stuff atm
|
||||
# and options mkts are generally "slow moving"..
|
||||
|
|
@ -435,7 +436,10 @@ async def open_jsonrpc_session(
|
|||
# broken and never restored with wtv init sequence is required to
|
||||
# re-establish a working req-resp session.
|
||||
|
||||
) -> Callable[[str, dict], dict]:
|
||||
) -> Callable[
|
||||
[str, dict],
|
||||
dict,
|
||||
]:
|
||||
'''
|
||||
Init a json-RPC-over-websocket connection to the provided `url`.
|
||||
|
||||
|
|
@ -531,14 +535,18 @@ async def open_jsonrpc_session(
|
|||
'id': mid,
|
||||
} if not rpc_results.get(mid):
|
||||
log.warning(
|
||||
f'Unexpected ws msg: {json.dumps(msg, indent=4)}'
|
||||
f'Unexpected ws msg?\n'
|
||||
f'{json.dumps(msg, indent=4)}'
|
||||
)
|
||||
|
||||
case {
|
||||
'method': _,
|
||||
'params': _,
|
||||
}:
|
||||
log.debug(f'Recieved\n{msg}')
|
||||
log.debug(
|
||||
f'Recieved\n'
|
||||
f'{msg!r}'
|
||||
)
|
||||
|
||||
case {
|
||||
'error': error
|
||||
|
|
@ -554,12 +562,15 @@ async def open_jsonrpc_session(
|
|||
result['event'].set()
|
||||
log.error(
|
||||
f'JSONRPC request failed\n'
|
||||
f'req: {req_msg}\n'
|
||||
f'resp: {error}\n'
|
||||
f'req: {req_msg!r}\n'
|
||||
f'resp: {error!r}\n'
|
||||
)
|
||||
|
||||
case _:
|
||||
log.warning(f'Unhandled JSON-RPC msg!?\n{msg}')
|
||||
log.warning(
|
||||
f'Unhandled JSON-RPC msg!?\n'
|
||||
f'{msg!r}'
|
||||
)
|
||||
|
||||
tn.start_soon(recv_task)
|
||||
yield json_rpc
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
'''
|
||||
Data feed apis and infra.
|
||||
|
||||
This module is enabled for ``brokerd`` daemons and includes mostly
|
||||
This module is enabled for ``datad`` daemons and includes mostly
|
||||
endpoints and middleware to support our real-time, provider agnostic,
|
||||
live market quotes layer. Historical data loading and processing is also
|
||||
initiated in parts of the feed bus startup but business logic and
|
||||
|
|
@ -30,6 +30,7 @@ from collections import (
|
|||
defaultdict,
|
||||
abc,
|
||||
)
|
||||
from copy import copy
|
||||
from contextlib import asynccontextmanager as acm
|
||||
from functools import partial
|
||||
import time
|
||||
|
|
@ -54,8 +55,11 @@ from piker.accounting import (
|
|||
)
|
||||
from piker.types import Struct
|
||||
from piker.brokers import get_brokermod
|
||||
from piker.service import (
|
||||
maybe_spawn_brokerd,
|
||||
# NOTE: must be a "relative-direct" import (NOT via
|
||||
# `piker.service`) to avoid a partial-init cycle when this
|
||||
# mod is loaded as part of `piker.service.__init__`.
|
||||
from ._daemon import (
|
||||
maybe_spawn_datad,
|
||||
)
|
||||
from piker.calc import humanize
|
||||
from ._util import (
|
||||
|
|
@ -77,7 +81,7 @@ from ._sampling import (
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from .flows import Flume
|
||||
from tractor._addr import Address
|
||||
from tractor.discovery._addr import Address
|
||||
from tractor.msg.types import Aid
|
||||
|
||||
|
||||
|
|
@ -110,7 +114,7 @@ class _FeedsBus(Struct):
|
|||
'''
|
||||
Data feeds broadcaster and persistence management.
|
||||
|
||||
This is a brokerd side api used to manager persistent real-time
|
||||
This is a datad side api used to manager persistent real-time
|
||||
streams that can be allocated and left alive indefinitely. A bus is
|
||||
associated one-to-one with a particular broker backend where the
|
||||
"bus" refers so a multi-symbol bus where quotes are interleaved in
|
||||
|
|
@ -249,7 +253,7 @@ async def allocate_persistent_feed(
|
|||
'''
|
||||
Create and maintain a "feed bus" which allocates tasks for real-time
|
||||
streaming and optional historical data storage per broker/data provider
|
||||
backend; this normally task runs *in* a `brokerd` actor.
|
||||
backend; this normally task runs *in* a `datad` actor.
|
||||
|
||||
If none exists, this allocates a ``_FeedsBus`` which manages the
|
||||
lifetimes of streaming tasks created for each requested symbol.
|
||||
|
|
@ -318,8 +322,8 @@ async def allocate_persistent_feed(
|
|||
# at max capacity.
|
||||
# - the same ideas ^ but when a local core is maxxed out (like how
|
||||
# binance does often with hft XD
|
||||
# - if a brokerd is non-local then we can't just allocate a mem
|
||||
# channel here and have the brokerd write it, we instead need
|
||||
# - if a datad is non-local then we can't just allocate a mem
|
||||
# channel here and have the datad write it, we instead need
|
||||
# a small streaming machine around the remote feed which can then
|
||||
# do the normal work of sampling and writing shm buffers
|
||||
# (depending on if we want sampling done on the far end or not?)
|
||||
|
|
@ -499,7 +503,7 @@ async def open_feed_bus(
|
|||
# (after we also group them in a nice `/dev/shm/piker/` subdir).
|
||||
# ensure we are who we think we are
|
||||
servicename = tractor.current_actor().name
|
||||
assert 'brokerd' in servicename
|
||||
assert 'datad' in servicename
|
||||
assert brokername in servicename
|
||||
|
||||
bus: _FeedsBus = get_feed_bus(brokername)
|
||||
|
|
@ -509,12 +513,12 @@ async def open_feed_bus(
|
|||
for symbol in symbols:
|
||||
|
||||
# if no cached feed for this symbol has been created for this
|
||||
# brokerd yet, start persistent stream and shm writer task in
|
||||
# datad yet, start persistent stream and shm writer task in
|
||||
# service nursery
|
||||
flume = bus.feeds.get(symbol)
|
||||
if flume is None:
|
||||
# allocate a new actor-local stream bus which
|
||||
# will persist for this `brokerd`'s service lifetime.
|
||||
# will persist for this `datad`'s service lifetime.
|
||||
async with bus.task_lock:
|
||||
await bus.nursery.start(
|
||||
partial(
|
||||
|
|
@ -674,6 +678,36 @@ async def open_feed_bus(
|
|||
bus.remove_subs(bs_fqme, subs)
|
||||
|
||||
|
||||
async def relay_to_common_memchan(
|
||||
stream: (
|
||||
tractor.MsgStream
|
||||
|trionics.BroadcastReceiver
|
||||
),
|
||||
tx: trio.MemorySendChannel[dict[str, Any]],
|
||||
task_status: TaskStatus[
|
||||
trionics.BroadcastReceiver,
|
||||
] = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Relay one provider subscription into a common memory channel.
|
||||
|
||||
Deliver the live child receiver through `task_status` so the
|
||||
caller can retain and inspect each provider relay's cursor.
|
||||
|
||||
'''
|
||||
bstream: trionics.BroadcastReceiver
|
||||
async with (
|
||||
stream.subscribe() as bstream,
|
||||
tx,
|
||||
):
|
||||
task_status.started(bstream)
|
||||
|
||||
msg: dict[str, Any]
|
||||
async for msg in bstream:
|
||||
await tx.send(msg)
|
||||
|
||||
|
||||
class Feed(Struct):
|
||||
'''
|
||||
A per-provider API for client-side consumption from real-time data
|
||||
|
|
@ -721,31 +755,53 @@ class Feed(Struct):
|
|||
mods = {name: self.mods[name] for name in brokers}
|
||||
|
||||
if len(mods) == 1:
|
||||
# just pass the brokerd stream directly if only one provider
|
||||
# just pass the datad stream directly if only one provider
|
||||
# was detected.
|
||||
stream = self.streams[list(brokers)[0]]
|
||||
stream: (
|
||||
tractor.MsgStream
|
||||
|trionics.BroadcastReceiver
|
||||
) = self.streams[list(brokers)[0]]
|
||||
bstream: trionics.BroadcastReceiver
|
||||
async with stream.subscribe() as bstream:
|
||||
yield bstream
|
||||
return
|
||||
|
||||
# start multiplexing task tree
|
||||
tx: trio.MemorySendChannel[dict[str, Any]]
|
||||
rx: trio.MemoryReceiveChannel[dict[str, Any]]
|
||||
tx, rx = trio.open_memory_channel(616)
|
||||
|
||||
async def relay_to_common_memchan(stream: tractor.MsgStream):
|
||||
async with tx:
|
||||
async for msg in stream:
|
||||
await tx.send(msg)
|
||||
|
||||
async with (
|
||||
tractor.trionics.collapse_eg(),
|
||||
trio.open_nursery() as nurse
|
||||
):
|
||||
# spawn a relay task for each stream so that they all
|
||||
# multiplex to a common channel.
|
||||
for brokername in mods:
|
||||
stream = self.streams[brokername]
|
||||
nurse.start_soon(relay_to_common_memchan, stream)
|
||||
broker2bstreams: dict[
|
||||
str,
|
||||
trionics.BroadcastReceiver,
|
||||
] = {}
|
||||
broker: str
|
||||
for broker in mods:
|
||||
stream: (
|
||||
tractor.MsgStream
|
||||
|trionics.BroadcastReceiver
|
||||
) = self.streams[broker]
|
||||
|
||||
relay_tx: trio.MemorySendChannel[
|
||||
dict[str, Any]
|
||||
] = tx.clone()
|
||||
|
||||
bstream: trionics.BroadcastReceiver = await nurse.start(
|
||||
relay_to_common_memchan,
|
||||
stream,
|
||||
relay_tx,
|
||||
)
|
||||
broker2bstreams[broker] = bstream
|
||||
|
||||
# Each relay now owns one `tx` clone. Closing the original
|
||||
# lets `rx` reach EOC only after every provider relay exits.
|
||||
await tx.aclose()
|
||||
try:
|
||||
yield rx
|
||||
finally:
|
||||
|
|
@ -754,16 +810,29 @@ class Feed(Struct):
|
|||
_max_sample_rate: int = 1
|
||||
|
||||
async def pause(self) -> None:
|
||||
'''
|
||||
Pause the shared remote provider context for this cached feed.
|
||||
|
||||
This controls provider publication for every local cache user;
|
||||
it does not pause only this caller's receive cursor.
|
||||
|
||||
'''
|
||||
stream: tractor.MsgStream|trionics.BroadcastReceiver
|
||||
for stream in set(self.streams.values()):
|
||||
await stream.send('pause')
|
||||
|
||||
async def resume(self) -> None:
|
||||
'''
|
||||
Resume the shared remote provider context for this cached feed.
|
||||
|
||||
'''
|
||||
stream: tractor.MsgStream|trionics.BroadcastReceiver
|
||||
for stream in set(self.streams.values()):
|
||||
await stream.send('resume')
|
||||
|
||||
|
||||
@acm
|
||||
async def install_brokerd_search(
|
||||
async def install_datad_search(
|
||||
|
||||
portal: tractor.Portal,
|
||||
brokermod: ModuleType,
|
||||
|
|
@ -812,12 +881,22 @@ async def maybe_open_feed(
|
|||
ReceiveChannel[dict[str, Any]],
|
||||
):
|
||||
'''
|
||||
Maybe open a data to a ``brokerd`` daemon only if there is no
|
||||
Maybe open a data feed to a ``datad`` daemon only if there is no
|
||||
local one for the broker-symbol pair, if one is cached use it wrapped
|
||||
in a tractor broadcast receiver.
|
||||
|
||||
'''
|
||||
fqme = fqmes[0]
|
||||
cache_key: tuple[
|
||||
tuple[str, ...],
|
||||
float|None,
|
||||
bool,
|
||||
bool,
|
||||
] = (
|
||||
tuple(sorted(fqmes)),
|
||||
kwargs.get('tick_throttle'),
|
||||
kwargs.get('allow_overruns', True),
|
||||
kwargs.get('start_stream', True),
|
||||
)
|
||||
|
||||
async with trionics.maybe_open_context(
|
||||
acm_func=open_feed,
|
||||
|
|
@ -830,27 +909,73 @@ async def maybe_open_feed(
|
|||
'allow_overruns': kwargs.get('allow_overruns', True),
|
||||
'start_stream': kwargs.get('start_stream', True),
|
||||
},
|
||||
key=fqme,
|
||||
key=cache_key,
|
||||
|
||||
) as (cache_hit, feed):
|
||||
feed: Feed
|
||||
|
||||
if cache_hit:
|
||||
log.info(f'Using cached feed for {fqme}')
|
||||
# add a new broadcast subscription for the quote stream
|
||||
# if this feed is likely already in use
|
||||
if (
|
||||
cache_hit
|
||||
and
|
||||
log.at_least_level('info')
|
||||
):
|
||||
log.info(
|
||||
f'Using cached feed for key:\n'
|
||||
f'{cache_key!r}'
|
||||
)
|
||||
|
||||
# The cached `Feed` exclusively owns provider root streams.
|
||||
# Every caller, including the first cache owner, gets a local
|
||||
# child cursor and copied descriptors for its lexical lifetime.
|
||||
broker_names: tuple[str, ...] = tuple(feed.streams)
|
||||
bstreams: tuple[
|
||||
trionics.BroadcastReceiver,
|
||||
...,
|
||||
]
|
||||
async with trionics.gather_contexts(
|
||||
mngrs=[stream.subscribe() for stream in feed.streams.values()]
|
||||
mngrs=[
|
||||
feed.streams[broker].subscribe()
|
||||
for broker in broker_names
|
||||
]
|
||||
) as bstreams:
|
||||
for bstream, flume in zip(bstreams, feed.flumes.values()):
|
||||
broker2bstreams: dict[
|
||||
str,
|
||||
trionics.BroadcastReceiver,
|
||||
] = dict(zip(
|
||||
broker_names,
|
||||
bstreams,
|
||||
strict=True,
|
||||
))
|
||||
flumes: dict[str, Flume] = {}
|
||||
fqme: str
|
||||
flume: Flume
|
||||
for fqme, flume in feed.flumes.items():
|
||||
broker: str = flume.mkt.broker
|
||||
|
||||
bstream: trionics.BroadcastReceiver = (
|
||||
broker2bstreams[broker]
|
||||
)
|
||||
|
||||
# XXX: TODO: horrible hackery that needs fixing..
|
||||
# i guess we have to create context proxies?
|
||||
bstream._ctx = flume.stream._ctx
|
||||
flume.stream = bstream
|
||||
root_stream: tractor.MsgStream = feed.streams[broker]
|
||||
|
||||
yield feed
|
||||
else:
|
||||
yield feed
|
||||
ctx: tractor.Context = root_stream.ctx
|
||||
bstream._ctx = ctx
|
||||
|
||||
local_flume: Flume = copy(flume)
|
||||
local_flume.stream = bstream
|
||||
flumes[fqme] = local_flume
|
||||
|
||||
# `feed` is the cache-owned resource and must retain only
|
||||
# provider root streams. A shallow `Feed` copy gives this
|
||||
# caller its own descriptor mappings while intentionally
|
||||
# sharing immutable market metadata, SHM handles and
|
||||
# provider controls for the cache resource's lifetime.
|
||||
local_feed: Feed = copy(feed)
|
||||
local_feed.flumes = flumes
|
||||
local_feed.streams = broker2bstreams
|
||||
yield local_feed
|
||||
|
||||
|
||||
@acm
|
||||
|
|
@ -885,14 +1010,14 @@ async def open_feed(
|
|||
providers.setdefault(mod, []).append(bs_fqme)
|
||||
feed.mods[mod.name] = mod
|
||||
|
||||
# one actor per brokerd for now
|
||||
brokerd_ctxs = []
|
||||
# one actor per datad for now
|
||||
datad_ctxs = []
|
||||
for brokermod, bfqmes in providers.items():
|
||||
|
||||
# if no `brokerd` for this backend exists yet we spawn
|
||||
# if no `datad` for this backend exists yet we spawn
|
||||
# a daemon actor for it.
|
||||
brokerd_ctxs.append(
|
||||
maybe_spawn_brokerd(
|
||||
datad_ctxs.append(
|
||||
maybe_spawn_datad(
|
||||
brokermod.name,
|
||||
loglevel=loglevel
|
||||
)
|
||||
|
|
@ -900,7 +1025,7 @@ async def open_feed(
|
|||
|
||||
portals: tuple[tractor.Portal]
|
||||
async with trionics.gather_contexts(
|
||||
brokerd_ctxs,
|
||||
datad_ctxs,
|
||||
) as portals:
|
||||
|
||||
bus_ctxs: list[AsyncContextManager] = []
|
||||
|
|
@ -937,9 +1062,9 @@ async def open_feed(
|
|||
tick_throttle=tick_throttle,
|
||||
|
||||
# XXX: super important to avoid
|
||||
# the brokerd from some other
|
||||
# the datad from some other
|
||||
# backend overruning the task here
|
||||
# bc some other brokerd took longer
|
||||
# bc some other datad took longer
|
||||
# to startup before we hit the `.open_stream()`
|
||||
# loop below XD .. really we should try to do each
|
||||
# of these stream open sequences sequentially per
|
||||
|
|
@ -973,9 +1098,6 @@ async def open_feed(
|
|||
# assert flume.mkt.fqme == fqme
|
||||
feed.flumes[fqme] = flume
|
||||
|
||||
# TODO: do we need this?
|
||||
flume.feed = feed
|
||||
|
||||
# attach and cache shm handles
|
||||
rt_shm = flume.rt_shm
|
||||
assert rt_shm
|
||||
|
|
@ -1011,7 +1133,7 @@ async def open_feed(
|
|||
assert stream
|
||||
feed.streams[brokermod.name] = stream
|
||||
|
||||
# apply `brokerd`-common stream to each flume
|
||||
# apply `datad`-common stream to each flume
|
||||
# tracking a live market feed from that provider.
|
||||
for fqme, flume in feed.flumes.items():
|
||||
if brokermod.name == flume.mkt.broker:
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@ real-time data processing data-structures.
|
|||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import tractor
|
||||
import pendulum
|
||||
|
|
@ -38,9 +35,6 @@ from tractor.ipc._shm import (
|
|||
)
|
||||
from piker.accounting import MktPair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from piker.data.feed import Feed
|
||||
|
||||
|
||||
class Flume(Struct):
|
||||
'''
|
||||
|
|
@ -80,10 +74,6 @@ class Flume(Struct):
|
|||
izero_rt: int = 0
|
||||
throttle_rate: int | None = None
|
||||
|
||||
# TODO: do we need this really if we can pull the `Portal` from
|
||||
# ``tractor``'s internals?
|
||||
feed: Feed|None = None
|
||||
|
||||
@property
|
||||
def rt_shm(self) -> ShmArray:
|
||||
|
||||
|
|
@ -156,7 +146,6 @@ class Flume(Struct):
|
|||
# will get instead some kind of msg-compat version
|
||||
# that it can load.
|
||||
msg.pop('stream')
|
||||
msg.pop('feed')
|
||||
msg.pop('_rt_shm')
|
||||
msg.pop('_hist_shm')
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from typing import (
|
|||
)
|
||||
|
||||
from msgspec import field
|
||||
from tractor._exceptions import reg_err_types
|
||||
|
||||
from piker.types import Struct
|
||||
from piker.accounting import (
|
||||
|
|
@ -43,6 +44,8 @@ class FeedInitializationError(ValueError):
|
|||
|
||||
'''
|
||||
|
||||
reg_err_types([FeedInitializationError])
|
||||
|
||||
|
||||
class FeedInit(Struct, frozen=True):
|
||||
'''
|
||||
|
|
@ -88,6 +91,24 @@ _eps: dict[str, list[str]] = {
|
|||
}
|
||||
|
||||
|
||||
def get_eps(
|
||||
mod: ModuleType,
|
||||
kind: str, # 'middleware' | 'datad' | 'brokerd'
|
||||
|
||||
) -> dict[str, Callable]:
|
||||
'''
|
||||
Return the daemon-kind's ep funcs defined by the backend
|
||||
`mod`, keyed by ep name; any eps from `_eps[kind]` not
|
||||
implemented by the backend are excluded.
|
||||
|
||||
'''
|
||||
return {
|
||||
name: ep
|
||||
for name in _eps[kind]
|
||||
if (ep := getattr(mod, name, None))
|
||||
}
|
||||
|
||||
|
||||
def validate_backend(
|
||||
mod: ModuleType,
|
||||
syms: list[str],
|
||||
|
|
|
|||
|
|
@ -56,3 +56,7 @@ from ..brokers._daemon import (
|
|||
spawn_brokerd as spawn_brokerd,
|
||||
maybe_spawn_brokerd as maybe_spawn_brokerd,
|
||||
)
|
||||
from ..data._daemon import (
|
||||
spawn_datad as spawn_datad,
|
||||
maybe_spawn_datad as maybe_spawn_datad,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ def get_runtime_vars() -> dict[str, Any]:
|
|||
async def open_piker_runtime(
|
||||
name: str,
|
||||
registry_addrs: list[tuple[str, int]] = [],
|
||||
tpt_bind_addrs: list|None = None,
|
||||
|
||||
enable_modules: list[str] = [],
|
||||
loglevel: str|None = None,
|
||||
|
|
@ -91,7 +92,7 @@ async def open_piker_runtime(
|
|||
try:
|
||||
actor = tractor.current_actor()
|
||||
except tractor._exceptions.NoRuntime:
|
||||
tractor._state._runtime_vars[
|
||||
tractor.runtime._state._runtime_vars[
|
||||
'piker_vars'
|
||||
] = tractor_runtime_overrides
|
||||
|
||||
|
|
@ -112,6 +113,7 @@ async def open_piker_runtime(
|
|||
|
||||
# passed through to `open_root_actor`
|
||||
registry_addrs=registry_addrs,
|
||||
tpt_bind_addrs=tpt_bind_addrs,
|
||||
name=name,
|
||||
start_method=start_method,
|
||||
loglevel=loglevel,
|
||||
|
|
@ -155,6 +157,7 @@ _root_modules: list[str] = [
|
|||
__name__,
|
||||
'piker.service._daemon',
|
||||
'piker.brokers._daemon',
|
||||
'piker.data._daemon',
|
||||
|
||||
'piker.clearing._ems',
|
||||
'piker.clearing._client',
|
||||
|
|
@ -166,6 +169,7 @@ _root_modules: list[str] = [
|
|||
@acm
|
||||
async def open_pikerd(
|
||||
registry_addrs: list[tuple[str, int]],
|
||||
tpt_bind_addrs: list|None = None,
|
||||
loglevel: str|None = None,
|
||||
|
||||
# XXX: you should pretty much never want debug mode
|
||||
|
|
@ -198,6 +202,7 @@ async def open_pikerd(
|
|||
loglevel=loglevel,
|
||||
debug_mode=debug_mode,
|
||||
registry_addrs=registry_addrs,
|
||||
tpt_bind_addrs=tpt_bind_addrs,
|
||||
|
||||
**kwargs,
|
||||
|
||||
|
|
@ -210,7 +215,16 @@ async def open_pikerd(
|
|||
trio.open_nursery() as service_tn,
|
||||
):
|
||||
for addr in reg_addrs:
|
||||
if addr not in root_actor.accept_addrs:
|
||||
# normalize to a wrapped `tractor` addr-type;
|
||||
# entries may be raw `tuple`s when passed in
|
||||
# from (test) client code.
|
||||
wladdr = tractor.discovery._addr.wrap_address(
|
||||
addr,
|
||||
)
|
||||
uaddr: tuple = wladdr.unwrap()
|
||||
if (
|
||||
uaddr not in root_actor.accept_addrs
|
||||
):
|
||||
raise RuntimeError(
|
||||
f'`pikerd` failed to bind on {addr}!\n'
|
||||
'Maybe you have another daemon already running?'
|
||||
|
|
@ -264,7 +278,7 @@ async def maybe_open_pikerd(
|
|||
**kwargs,
|
||||
|
||||
) -> (
|
||||
tractor._portal.Portal
|
||||
tractor.Portal
|
||||
|ClassVar[Services]
|
||||
):
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from requests.exceptions import (
|
|||
ReadTimeout,
|
||||
)
|
||||
|
||||
from tractor._exceptions import reg_err_types
|
||||
from piker.log import (
|
||||
get_console_log,
|
||||
get_logger,
|
||||
|
|
@ -66,6 +67,11 @@ class DockerNotStarted(Exception):
|
|||
class ApplicationLogError(Exception):
|
||||
'App in container reported an error in logs'
|
||||
|
||||
reg_err_types([
|
||||
DockerNotStarted,
|
||||
ApplicationLogError,
|
||||
])
|
||||
|
||||
|
||||
@acm
|
||||
async def open_docker(
|
||||
|
|
|
|||
|
|
@ -79,10 +79,17 @@ async def maybe_spawn_daemon(
|
|||
lock = Services.locks[service_name]
|
||||
await lock.acquire()
|
||||
|
||||
if not pikerd_kwargs:
|
||||
# XXX NOTE, pin to apprope `tractor` branch!
|
||||
rtvs: dict = tractor.get_runtime_vars()
|
||||
registry_addrs: list[tuple] = list(
|
||||
map(tuple, rtvs['_registry_addrs'])
|
||||
)
|
||||
|
||||
try:
|
||||
async with find_service(
|
||||
service_name,
|
||||
registry_addrs=[('127.0.0.1', 6116)],
|
||||
registry_addrs=registry_addrs,
|
||||
) as portal:
|
||||
if portal is not None:
|
||||
lock.release()
|
||||
|
|
@ -99,6 +106,7 @@ async def maybe_spawn_daemon(
|
|||
# process tree
|
||||
async with maybe_open_pikerd(
|
||||
loglevel=loglevel,
|
||||
registry_addrs=registry_addrs,
|
||||
**pikerd_kwargs,
|
||||
|
||||
) as pikerd_portal:
|
||||
|
|
@ -142,7 +150,65 @@ async def maybe_spawn_daemon(
|
|||
async with tractor.wait_for_actor(service_name) as portal:
|
||||
lock.release()
|
||||
yield portal
|
||||
await portal.cancel_actor()
|
||||
# --- ---- ---
|
||||
# XXX NOTE XXX
|
||||
# --- ---- ---
|
||||
# DO NOT PUT A `portal.cancel_actor()` here (as was prior)!
|
||||
#
|
||||
# Doing so will cause an "out-of-band" ctxc
|
||||
# (`tractor.ContextCancelled`) to be raised inside the
|
||||
# `ServiceMngr.open_context_in_task()`'s call to
|
||||
# `ctx.wait_for_result()` AND the internal self-ctxc
|
||||
# "graceful capture" WILL NOT CATCH IT!
|
||||
#
|
||||
# This can cause certain types of operations to raise
|
||||
# that ctxc BEFORE THEY `return`, resulting in
|
||||
# a "false-negative" ctxc being raised when really
|
||||
# nothing actually failed, other then our semantic
|
||||
# "failure" to suppress an expected, graceful,
|
||||
# self-cancel scenario..
|
||||
#
|
||||
# bUt wHy duZ It WorK lIKe dis..
|
||||
# ------------------------------
|
||||
# from the perspective of the `tractor.Context` this
|
||||
# cancel request was conducted "out of band" since
|
||||
# `Context.cancel()` was never called and thus the
|
||||
# `._cancel_called: bool` was never set. Despite the
|
||||
# remote `.canceller` being set to `pikerd` (i.e. the
|
||||
# same `Actor.uid` of the raising service-mngr task) the
|
||||
# service-task's ctx itself was never marked as having
|
||||
# requested cancellation and thus still raises the ctxc
|
||||
# bc it was unaware of any such request.
|
||||
#
|
||||
# How to make grokin these cases easier tho?
|
||||
# ------------------------------------------
|
||||
# Because `Portal.cancel_actor()` was called it requests
|
||||
# "full-`Actor`-runtime-cancellation" of it's peer
|
||||
# process which IS NOT THE SAME as a single inter-actor
|
||||
# RPC task cancelling its local context with a remote
|
||||
# peer `Task` in that same peer process.
|
||||
#
|
||||
# ?TODO? It might be better if we do one (or all) of the
|
||||
# following:
|
||||
#
|
||||
# -[ ] at least set a special message for the
|
||||
# `ContextCancelled` when raised locally by the
|
||||
# unaware ctx task such that we check for the
|
||||
# `.canceller` being *our `Actor`* and in the case
|
||||
# where `Context._cancel_called == False` we specially
|
||||
# note that this is likely an "out-of-band"
|
||||
# runtime-cancel request triggered by some call to
|
||||
# `Portal.cancel_actor()`, possibly even reporting the
|
||||
# exact LOC of that caller by tracking it inside our
|
||||
# portal-type?
|
||||
# -[ ] possibly add another field `ContextCancelled` like
|
||||
# maybe a,
|
||||
# `.request_type: Literal['os', 'proc', 'actor',
|
||||
# 'ctx']` type thing which would allow immediately
|
||||
# being able to tell what kind of cancellation caused
|
||||
# the unexpected ctxc?
|
||||
# -[ ] REMOVE THIS COMMENT, once we've settled on how to
|
||||
# better augment `tractor` to be more explicit on this!
|
||||
|
||||
except BaseException as _err:
|
||||
err = _err
|
||||
|
|
@ -152,11 +218,13 @@ async def maybe_spawn_daemon(
|
|||
lock.statistics().owner is current_task()
|
||||
):
|
||||
log.exception(
|
||||
f'Releasing stale lock after crash..?'
|
||||
f'Releasing stale lock after crash..?\n'
|
||||
f'\n'
|
||||
f'{err!r}\n'
|
||||
)
|
||||
lock.release()
|
||||
raise err
|
||||
|
||||
raise
|
||||
|
||||
|
||||
async def spawn_emsd(
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ log = get_logger(name=__name__)
|
|||
# new actors and supervises them to completion?
|
||||
class Services:
|
||||
|
||||
actor_n: tractor._supervise.ActorNursery
|
||||
actor_n: tractor.ActorNursery
|
||||
service_n: trio.Nursery
|
||||
debug_mode: bool # tractor sub-actor debug mode flag
|
||||
service_tasks: dict[
|
||||
|
|
|
|||
|
|
@ -225,10 +225,13 @@ async def check_for_service(
|
|||
|
||||
'''
|
||||
async with (
|
||||
open_registry(ensure_exists=False) as reg_addr,
|
||||
open_registry(
|
||||
addrs=Registry.addrs,
|
||||
ensure_exists=False,
|
||||
) as reg_addrs,
|
||||
tractor.query_actor(
|
||||
service_name,
|
||||
arbiter_sockaddr=reg_addr,
|
||||
) as sockaddr,
|
||||
regaddr=reg_addrs[0],
|
||||
) as (sockaddr, _),
|
||||
):
|
||||
return sockaddr
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ from msgspec.msgpack import (
|
|||
# import pyqtgraph as pg
|
||||
import numpy as np
|
||||
import tractor
|
||||
from tractor._exceptions import reg_err_types
|
||||
from trio_websocket import open_websocket_url
|
||||
from anyio_marketstore import ( # noqa
|
||||
open_marketstore_client,
|
||||
|
|
@ -382,6 +383,8 @@ def quote_to_marketstore_structarray(
|
|||
class MarketStoreError(Exception):
|
||||
"Generic marketstore client error"
|
||||
|
||||
reg_err_types([MarketStoreError])
|
||||
|
||||
|
||||
# def err_on_resp(response: dict) -> None:
|
||||
# """Raise any errors found in responses from client request.
|
||||
|
|
|
|||
|
|
@ -42,15 +42,17 @@ from typing import (
|
|||
)
|
||||
|
||||
import numpy as np
|
||||
from tractor._exceptions import reg_err_types
|
||||
|
||||
from .. import config
|
||||
from ..service import (
|
||||
check_for_service,
|
||||
)
|
||||
from ..log import (
|
||||
from piker import config
|
||||
from piker.log import (
|
||||
get_logger,
|
||||
get_console_log,
|
||||
)
|
||||
from piker.service import (
|
||||
check_for_service,
|
||||
)
|
||||
|
||||
subsys: str = 'piker.storage'
|
||||
|
||||
log = get_logger(subsys)
|
||||
|
|
@ -151,6 +153,12 @@ class StorageConnectionError(ConnectionError):
|
|||
|
||||
'''
|
||||
|
||||
reg_err_types([
|
||||
TimeseriesNotFound,
|
||||
StorageConnectionError,
|
||||
])
|
||||
|
||||
|
||||
def get_storagemod(
|
||||
name: str,
|
||||
|
||||
|
|
|
|||
|
|
@ -292,6 +292,11 @@ def ldshm(
|
|||
f'Something is wrong with time period for {shm}:\n{times}'
|
||||
)
|
||||
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(
|
||||
frame=shm.array,
|
||||
|
|
@ -301,7 +306,7 @@ def ldshm(
|
|||
# TODO: call null-seg fixer somehow?
|
||||
if null_segs:
|
||||
|
||||
if tractor._state.is_debug_mode():
|
||||
if tractor.runtime._state.is_debug_mode():
|
||||
await tractor.pause()
|
||||
# async with (
|
||||
# trio.open_nursery() as tn,
|
||||
|
|
|
|||
|
|
@ -276,14 +276,41 @@ def get_null_segs(
|
|||
absi_zdiff: np.ndarray = np.diff(absi_zeros)
|
||||
|
||||
if zero_t.size < 2:
|
||||
try:
|
||||
breakpoint()
|
||||
except RuntimeError:
|
||||
# XXX, if greenback not active from
|
||||
# piker store ldshm cmd..
|
||||
log.exception(
|
||||
"Can't debug single-sample null!\n"
|
||||
idx: int = zero_t['index'][0]
|
||||
idx_before: int = idx - 1
|
||||
idx_after: int = idx + 1
|
||||
index = frame['index']
|
||||
before_cond = idx_before <= index
|
||||
after_cond = index <= idx_after
|
||||
bars: np.ndarray = frame[
|
||||
before_cond
|
||||
&
|
||||
after_cond
|
||||
]
|
||||
time: np.ndarray = bars['time']
|
||||
from pendulum import (
|
||||
from_timestamp,
|
||||
Interval,
|
||||
)
|
||||
gap: Interval = (
|
||||
from_timestamp(time[-1])
|
||||
-
|
||||
from_timestamp(time[0])
|
||||
)
|
||||
log.warning(
|
||||
f'Single OHLCV-bar null-segment detected??\n'
|
||||
f'gap -> {gap}\n'
|
||||
)
|
||||
|
||||
# ^^XXX, if you want to debug the above bar-gap^^
|
||||
# try:
|
||||
# breakpoint()
|
||||
# except RuntimeError:
|
||||
# # XXX, if greenback not active from
|
||||
# # piker store ldshm cmd..
|
||||
# log.exception(
|
||||
# "Can't debug single-sample null!\n"
|
||||
# )
|
||||
|
||||
return None
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue