nyrobrain 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nyrobrain-0.1.0/.gitignore +41 -0
- nyrobrain-0.1.0/.python-version +1 -0
- nyrobrain-0.1.0/PKG-INFO +100 -0
- nyrobrain-0.1.0/README.md +78 -0
- nyrobrain-0.1.0/justfile +5 -0
- nyrobrain-0.1.0/pyproject.toml +109 -0
- nyrobrain-0.1.0/src/nyrobrain/__init__.py +7 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/CONTEXT.md +109 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/__init__.py +78 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/aegis.py +92 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/backtest.py +184 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/candles.py +218 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/credentials.py +203 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/data.py +95 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/driver.py +219 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/emitter.py +210 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/examples/README.md +173 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/examples/__init__.py +17 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/examples/oms.py +804 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/examples/over_delivery_guard.py +83 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/examples/sizing_a_target.py +70 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/examples/your_own_placement.py +117 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/executor.py +837 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/freshness.py +108 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/ingest.py +171 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/intake.py +194 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/ladder.py +256 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/metrics.py +463 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/pending.py +96 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/placement.py +177 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/prime.py +232 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/ratelimit.py +97 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/reconciliation.py +213 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/report.py +649 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/rules.py +72 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/runner.py +328 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/sandbox.py +109 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/signal.py +236 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/soak.py +761 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/store.py +95 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/target.py +169 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/testing.py +273 -0
- nyrobrain-0.1.0/src/nyrobrain/execution/venue.py +335 -0
- nyrobrain-0.1.0/src/nyrobrain/py.typed +0 -0
- nyrobrain-0.1.0/tests/conftest.py +36 -0
- nyrobrain-0.1.0/tests/fixtures/aegis/capture_incumbent.py +128 -0
- nyrobrain-0.1.0/tests/fixtures/aegis/incumbent_payloads.json +32 -0
- nyrobrain-0.1.0/tests/fixtures/candles/interval_1m_symbol_BTCUSDT_2024-03-01.parquet +0 -0
- nyrobrain-0.1.0/tests/fixtures/signals.jsonl +4 -0
- nyrobrain-0.1.0/tests/instruments.py +51 -0
- nyrobrain-0.1.0/tests/pricepaths.py +122 -0
- nyrobrain-0.1.0/tests/test_aegis.py +218 -0
- nyrobrain-0.1.0/tests/test_backtest.py +343 -0
- nyrobrain-0.1.0/tests/test_candles.py +173 -0
- nyrobrain-0.1.0/tests/test_data.py +156 -0
- nyrobrain-0.1.0/tests/test_driver.py +306 -0
- nyrobrain-0.1.0/tests/test_emitter.py +58 -0
- nyrobrain-0.1.0/tests/test_examples.py +52 -0
- nyrobrain-0.1.0/tests/test_executor_faults.py +721 -0
- nyrobrain-0.1.0/tests/test_executor_rules.py +100 -0
- nyrobrain-0.1.0/tests/test_executor_smoke.py +406 -0
- nyrobrain-0.1.0/tests/test_freshness.py +140 -0
- nyrobrain-0.1.0/tests/test_fuzz_engine.py +510 -0
- nyrobrain-0.1.0/tests/test_ingest.py +134 -0
- nyrobrain-0.1.0/tests/test_intake.py +221 -0
- nyrobrain-0.1.0/tests/test_invariants_stateful.py +193 -0
- nyrobrain-0.1.0/tests/test_ladder.py +454 -0
- nyrobrain-0.1.0/tests/test_live_guard.py +406 -0
- nyrobrain-0.1.0/tests/test_metric_round.py +219 -0
- nyrobrain-0.1.0/tests/test_metrics.py +371 -0
- nyrobrain-0.1.0/tests/test_oms.py +520 -0
- nyrobrain-0.1.0/tests/test_pending.py +161 -0
- nyrobrain-0.1.0/tests/test_placement.py +295 -0
- nyrobrain-0.1.0/tests/test_prime.py +221 -0
- nyrobrain-0.1.0/tests/test_ratelimit.py +261 -0
- nyrobrain-0.1.0/tests/test_reconciliation.py +226 -0
- nyrobrain-0.1.0/tests/test_registration.py +128 -0
- nyrobrain-0.1.0/tests/test_report.py +665 -0
- nyrobrain-0.1.0/tests/test_restart.py +343 -0
- nyrobrain-0.1.0/tests/test_runner.py +159 -0
- nyrobrain-0.1.0/tests/test_sandbox.py +128 -0
- nyrobrain-0.1.0/tests/test_signal.py +334 -0
- nyrobrain-0.1.0/tests/test_signed_leaves.py +148 -0
- nyrobrain-0.1.0/tests/test_smoke.py +84 -0
- nyrobrain-0.1.0/tests/test_soak.py +534 -0
- nyrobrain-0.1.0/tests/test_stale_feeds.py +260 -0
- nyrobrain-0.1.0/tests/test_store.py +137 -0
- nyrobrain-0.1.0/tests/test_target.py +365 -0
- nyrobrain-0.1.0/tests/test_testing.py +250 -0
- nyrobrain-0.1.0/tests/test_venue.py +336 -0
- nyrobrain-0.1.0/uv.lock +744 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.env*
|
|
3
|
+
.worktrees
|
|
4
|
+
.DS_Store
|
|
5
|
+
.claude/
|
|
6
|
+
graphify-out/
|
|
7
|
+
|
|
8
|
+
# Generated KB artifacts — never commit (mcp writes these under a
|
|
9
|
+
# workspace's .nyrobrain/kb/ at runtime; the template copies are stray).
|
|
10
|
+
template/kb.sqlite*
|
|
11
|
+
template/kb-graph.json
|
|
12
|
+
template/.nyrobrain/
|
|
13
|
+
|
|
14
|
+
# Cleaned template staged into the desktop app bundle at build time
|
|
15
|
+
# (apps/desktop/scripts/stage-template.sh).
|
|
16
|
+
apps/desktop/src-tauri/resources/template/
|
|
17
|
+
.worktrees/
|
|
18
|
+
|
|
19
|
+
# Test coverage output (bun --coverage-dir, junit, cobertura). Generated by
|
|
20
|
+
# `bun run test:coverage` and uploaded as CI artifacts; never committed.
|
|
21
|
+
coverage/
|
|
22
|
+
|
|
23
|
+
# Python (py/ — the nyrobrain library). `coverage/` above already covers
|
|
24
|
+
# py/coverage/. These are uv / pytest / ruff / mypy local caches.
|
|
25
|
+
.venv/
|
|
26
|
+
__pycache__/
|
|
27
|
+
*.py[cod]
|
|
28
|
+
.pytest_cache/
|
|
29
|
+
.ruff_cache/
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
.uv-cache/
|
|
32
|
+
.coverage
|
|
33
|
+
.coverage.*
|
|
34
|
+
.hypothesis/
|
|
35
|
+
|
|
36
|
+
# Run output from the replay, paper, live and soak drivers (`--out`). These are
|
|
37
|
+
# measurements of one moment on one machine — a summary, a parquet series and a
|
|
38
|
+
# soak CSV — and the findings that matter are written up in the map's tickets
|
|
39
|
+
# instead. A three-hour soak's series is also large enough to be worth keeping
|
|
40
|
+
# out of the history.
|
|
41
|
+
py/runs/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
nyrobrain-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nyrobrain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Nyrobrain libraries. nyrobrain.execution translates portfolio signals into exchange orders.
|
|
5
|
+
Author-email: Marcus Lee <marcuslee@balaenaquant.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Classifier: Programming Language :: Python
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Python: <3.15,>=3.13
|
|
13
|
+
Provides-Extra: aegis
|
|
14
|
+
Requires-Dist: bq-nats-client>=0.1.6; extra == 'aegis'
|
|
15
|
+
Provides-Extra: execution
|
|
16
|
+
Requires-Dist: nautilus-trader==2.0.0rc4; extra == 'execution'
|
|
17
|
+
Requires-Dist: pandas>=2.2; extra == 'execution'
|
|
18
|
+
Requires-Dist: pyarrow>=17; extra == 'execution'
|
|
19
|
+
Provides-Extra: prime
|
|
20
|
+
Requires-Dist: httpx>=0.27; extra == 'prime'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# nyrobrain
|
|
24
|
+
|
|
25
|
+
Nyrobrain Python libraries.
|
|
26
|
+
|
|
27
|
+
- `nyrobrain.execution` — translates portfolio signals (a target weight per
|
|
28
|
+
symbol) into orders at the exchange, on top of `nautilus_trader`.
|
|
29
|
+
|
|
30
|
+
## What it is for
|
|
31
|
+
|
|
32
|
+
A portfolio says *hold 25% of the book in BTC*. Something has to turn that into
|
|
33
|
+
orders, against whatever is already held and whatever is already working, and
|
|
34
|
+
keep doing it as the price moves and the target changes — without ever buying
|
|
35
|
+
past the target. That last clause is the whole library. The failure it exists to
|
|
36
|
+
prevent is an OMS that re-sends a delta it has already sent and walks a position
|
|
37
|
+
away from its target.
|
|
38
|
+
|
|
39
|
+
## You write the driver
|
|
40
|
+
|
|
41
|
+
The library does not ship one. It ships the executor, the guards, the report and
|
|
42
|
+
the pieces that are dangerous to get wrong; assembling them into a running
|
|
43
|
+
system is your code, because that is where your choices live.
|
|
44
|
+
|
|
45
|
+
Start from [`examples/oms.py`](src/nyrobrain/execution/examples/oms.py)
|
|
46
|
+
— one script, three modes, copy it and change it:
|
|
47
|
+
|
|
48
|
+
python oms.py --mode backtest --signals signals.jsonl --candles ~/candles
|
|
49
|
+
python oms.py --mode paper --minutes 10
|
|
50
|
+
python oms.py --mode live --minutes 10 --environment demo
|
|
51
|
+
|
|
52
|
+
The executor, the guards and the report are identical in all three. That is the
|
|
53
|
+
point: a difference in behaviour between two modes is the venue, not the
|
|
54
|
+
harness.
|
|
55
|
+
|
|
56
|
+
## Two surfaces
|
|
57
|
+
|
|
58
|
+
**Decisions** — `nyrobrain.execution`. Sizing, the delta, the guards, the
|
|
59
|
+
placement protocol. Imports no nautilus and installs without it, so you can
|
|
60
|
+
reason about a target without an engine in the process. A placement strategy you
|
|
61
|
+
write never imports nautilus either.
|
|
62
|
+
|
|
63
|
+
**Assembly** — `nyrobrain.execution.driver`, `.backtest`, `.credentials`,
|
|
64
|
+
`.venue`, `.runner`, `.report`. Nautilus all the way through, because building a
|
|
65
|
+
node cannot be anything else.
|
|
66
|
+
|
|
67
|
+
## Examples
|
|
68
|
+
|
|
69
|
+
Runnable, shipped in the package, and executed by the test suite so they cannot
|
|
70
|
+
rot. Read them in order:
|
|
71
|
+
|
|
72
|
+
python -m nyrobrain.execution.examples.sizing_a_target # weight -> target -> delta
|
|
73
|
+
python -m nyrobrain.execution.examples.over_delivery_guard # the invariant, and what it refuses
|
|
74
|
+
python -m nyrobrain.execution.examples.your_own_placement # a placement strategy, no venue needed
|
|
75
|
+
|
|
76
|
+
Then [`examples/README.md`](src/nyrobrain/execution/examples/README.md) for the
|
|
77
|
+
driver itself.
|
|
78
|
+
|
|
79
|
+
## Local development
|
|
80
|
+
|
|
81
|
+
uv sync --all-extras
|
|
82
|
+
uv run pytest
|
|
83
|
+
|
|
84
|
+
`soak` is ours rather than yours — a long-run diagnostic for validating the
|
|
85
|
+
executor over hours, not part of the driver story:
|
|
86
|
+
|
|
87
|
+
uv run python -m nyrobrain.execution.soak --hours 3 --environment demo
|
|
88
|
+
|
|
89
|
+
## Design maps
|
|
90
|
+
|
|
91
|
+
Built with the wayfinder method: numbered decision tickets under a map, worked
|
|
92
|
+
one at a time. Each records not just what was decided but what was measured to
|
|
93
|
+
decide it.
|
|
94
|
+
|
|
95
|
+
- [`.scratch/nyrobrain-execution/map.md`](../.scratch/nyrobrain-execution/map.md)
|
|
96
|
+
— the arithmetic, the guards and the placement seam.
|
|
97
|
+
- [`.scratch/nyrobrain-execution-runners/map.md`](../.scratch/nyrobrain-execution-runners/map.md)
|
|
98
|
+
— driving it from a `BacktestEngine` and a sandbox.
|
|
99
|
+
- [`.scratch/nyrobrain-execution-live/map.md`](../.scratch/nyrobrain-execution-live/map.md)
|
|
100
|
+
— against a real venue on Bybit demo, and the nautilus 2 migration.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# nyrobrain
|
|
2
|
+
|
|
3
|
+
Nyrobrain Python libraries.
|
|
4
|
+
|
|
5
|
+
- `nyrobrain.execution` — translates portfolio signals (a target weight per
|
|
6
|
+
symbol) into orders at the exchange, on top of `nautilus_trader`.
|
|
7
|
+
|
|
8
|
+
## What it is for
|
|
9
|
+
|
|
10
|
+
A portfolio says *hold 25% of the book in BTC*. Something has to turn that into
|
|
11
|
+
orders, against whatever is already held and whatever is already working, and
|
|
12
|
+
keep doing it as the price moves and the target changes — without ever buying
|
|
13
|
+
past the target. That last clause is the whole library. The failure it exists to
|
|
14
|
+
prevent is an OMS that re-sends a delta it has already sent and walks a position
|
|
15
|
+
away from its target.
|
|
16
|
+
|
|
17
|
+
## You write the driver
|
|
18
|
+
|
|
19
|
+
The library does not ship one. It ships the executor, the guards, the report and
|
|
20
|
+
the pieces that are dangerous to get wrong; assembling them into a running
|
|
21
|
+
system is your code, because that is where your choices live.
|
|
22
|
+
|
|
23
|
+
Start from [`examples/oms.py`](src/nyrobrain/execution/examples/oms.py)
|
|
24
|
+
— one script, three modes, copy it and change it:
|
|
25
|
+
|
|
26
|
+
python oms.py --mode backtest --signals signals.jsonl --candles ~/candles
|
|
27
|
+
python oms.py --mode paper --minutes 10
|
|
28
|
+
python oms.py --mode live --minutes 10 --environment demo
|
|
29
|
+
|
|
30
|
+
The executor, the guards and the report are identical in all three. That is the
|
|
31
|
+
point: a difference in behaviour between two modes is the venue, not the
|
|
32
|
+
harness.
|
|
33
|
+
|
|
34
|
+
## Two surfaces
|
|
35
|
+
|
|
36
|
+
**Decisions** — `nyrobrain.execution`. Sizing, the delta, the guards, the
|
|
37
|
+
placement protocol. Imports no nautilus and installs without it, so you can
|
|
38
|
+
reason about a target without an engine in the process. A placement strategy you
|
|
39
|
+
write never imports nautilus either.
|
|
40
|
+
|
|
41
|
+
**Assembly** — `nyrobrain.execution.driver`, `.backtest`, `.credentials`,
|
|
42
|
+
`.venue`, `.runner`, `.report`. Nautilus all the way through, because building a
|
|
43
|
+
node cannot be anything else.
|
|
44
|
+
|
|
45
|
+
## Examples
|
|
46
|
+
|
|
47
|
+
Runnable, shipped in the package, and executed by the test suite so they cannot
|
|
48
|
+
rot. Read them in order:
|
|
49
|
+
|
|
50
|
+
python -m nyrobrain.execution.examples.sizing_a_target # weight -> target -> delta
|
|
51
|
+
python -m nyrobrain.execution.examples.over_delivery_guard # the invariant, and what it refuses
|
|
52
|
+
python -m nyrobrain.execution.examples.your_own_placement # a placement strategy, no venue needed
|
|
53
|
+
|
|
54
|
+
Then [`examples/README.md`](src/nyrobrain/execution/examples/README.md) for the
|
|
55
|
+
driver itself.
|
|
56
|
+
|
|
57
|
+
## Local development
|
|
58
|
+
|
|
59
|
+
uv sync --all-extras
|
|
60
|
+
uv run pytest
|
|
61
|
+
|
|
62
|
+
`soak` is ours rather than yours — a long-run diagnostic for validating the
|
|
63
|
+
executor over hours, not part of the driver story:
|
|
64
|
+
|
|
65
|
+
uv run python -m nyrobrain.execution.soak --hours 3 --environment demo
|
|
66
|
+
|
|
67
|
+
## Design maps
|
|
68
|
+
|
|
69
|
+
Built with the wayfinder method: numbered decision tickets under a map, worked
|
|
70
|
+
one at a time. Each records not just what was decided but what was measured to
|
|
71
|
+
decide it.
|
|
72
|
+
|
|
73
|
+
- [`.scratch/nyrobrain-execution/map.md`](../.scratch/nyrobrain-execution/map.md)
|
|
74
|
+
— the arithmetic, the guards and the placement seam.
|
|
75
|
+
- [`.scratch/nyrobrain-execution-runners/map.md`](../.scratch/nyrobrain-execution-runners/map.md)
|
|
76
|
+
— driving it from a `BacktestEngine` and a sandbox.
|
|
77
|
+
- [`.scratch/nyrobrain-execution-live/map.md`](../.scratch/nyrobrain-execution-live/map.md)
|
|
78
|
+
— against a real venue on Bybit demo, and the nautilus 2 migration.
|
nyrobrain-0.1.0/justfile
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nyrobrain"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Nyrobrain libraries. nyrobrain.execution translates portfolio signals into exchange orders."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "Proprietary" }
|
|
7
|
+
authors = [{ name = "Marcus Lee", email = "marcuslee@balaenaquant.com" }]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Programming Language :: Python",
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
12
|
+
"Programming Language :: Python :: 3.13",
|
|
13
|
+
"Programming Language :: Python :: 3.14",
|
|
14
|
+
]
|
|
15
|
+
# Floor is 3.13 to match adrs, which migrates in later as a sibling subpackage;
|
|
16
|
+
# ceiling is nautilus_trader's (`>=3.12,<3.15`). Supporting 3.12 would buy
|
|
17
|
+
# nothing and would have to be given up the moment adrs lands.
|
|
18
|
+
requires-python = ">=3.13,<3.15"
|
|
19
|
+
# The base install is deliberately dependency-free: importing `nyrobrain` must
|
|
20
|
+
# never drag in an engine or a transport. Everything real lives behind an extra.
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
execution = ["nautilus_trader==2.0.0rc4", "pandas>=2.2", "pyarrow>=17"]
|
|
25
|
+
# The HTTP client the Prime signal source polls with. Behind an extra so the
|
|
26
|
+
# decision surface, and every test of it, needs no transport installed.
|
|
27
|
+
prime = ["httpx>=0.27"]
|
|
28
|
+
# The gRPC client the Aegis sink publishes through. Not a NATS protocol client:
|
|
29
|
+
# publishers reach JetStream via a sidecar authenticated by API key and hold no
|
|
30
|
+
# NATS credentials, which is also why portfolio targets deliberately live in a
|
|
31
|
+
# different account. Behind an extra for the same reason `prime` is — the
|
|
32
|
+
# decision surface builds the payloads and needs no transport to be tested.
|
|
33
|
+
aegis = ["bq-nats-client>=0.1.6"]
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.3",
|
|
38
|
+
"pytest-cov>=6.0",
|
|
39
|
+
"hypothesis>=6.120",
|
|
40
|
+
"mypy>=1.13",
|
|
41
|
+
"ruff>=0.8",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["hatchling"]
|
|
46
|
+
build-backend = "hatchling.build"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/nyrobrain"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
# Coverage and report flags are NOT in addopts: the local run stays fast so it
|
|
54
|
+
# actually gets run. CI adds them explicitly.
|
|
55
|
+
addopts = "-q --strict-markers --strict-config"
|
|
56
|
+
filterwarnings = [
|
|
57
|
+
# A warning raised by this library is a defect, and much cheaper to adopt
|
|
58
|
+
# now than to retrofit.
|
|
59
|
+
"error",
|
|
60
|
+
# Except when it is not ours. nautilus's BacktestEngine calls
|
|
61
|
+
# `Timestamp.utcnow` internally, which pandas 4 deprecates. We cannot fix it
|
|
62
|
+
# and will not skip running the engine over it; narrow it to the one message
|
|
63
|
+
# so everything else still fails.
|
|
64
|
+
"ignore:Timestamp.utcnow is deprecated:",
|
|
65
|
+
# Same rule, second instance: constructing a `TradingNode` calls
|
|
66
|
+
# `asyncio.get_event_loop_policy`, which Python 3.14 deprecates. Theirs to
|
|
67
|
+
# fix, and not a reason to stop building nodes in tests.
|
|
68
|
+
"ignore:'asyncio.get_event_loop_policy' is deprecated:",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.coverage.run]
|
|
72
|
+
source = ["nyrobrain"]
|
|
73
|
+
branch = true
|
|
74
|
+
|
|
75
|
+
[tool.ruff]
|
|
76
|
+
line-length = 88
|
|
77
|
+
src = ["src", "tests"]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint]
|
|
80
|
+
select = [
|
|
81
|
+
"E", # pycodestyle
|
|
82
|
+
"F", # pyflakes
|
|
83
|
+
"I", # isort
|
|
84
|
+
"UP", # pyupgrade
|
|
85
|
+
"B", # bugbear
|
|
86
|
+
"SIM", # simplify
|
|
87
|
+
"RUF",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.mypy]
|
|
91
|
+
python_version = "3.13"
|
|
92
|
+
packages = ["nyrobrain"]
|
|
93
|
+
strict = true
|
|
94
|
+
warn_unreachable = true
|
|
95
|
+
# Scoped to third parties rather than global. A global
|
|
96
|
+
# `ignore_missing_imports` also silences a *first-party* module that does not
|
|
97
|
+
# exist: a stale `from nyrobrain.execution.backtest import ...` type-checked
|
|
98
|
+
# clean while failing at import, which is precisely the failure the examples
|
|
99
|
+
# are type-checked to prevent.
|
|
100
|
+
# Every nautilus base class is Cython, so mypy sees it as `Any` and this check
|
|
101
|
+
# can never pass for any nautilus subclass - Data here, Strategy shortly. It
|
|
102
|
+
# carries no signal for us, and the alternative is a growing list of per-module
|
|
103
|
+
# ignores. Everything else stays strict.
|
|
104
|
+
disallow_subclassing_any = false
|
|
105
|
+
|
|
106
|
+
[[tool.mypy.overrides]]
|
|
107
|
+
# Nautilus ships partial stubs; pandas/pyarrow/hypothesis ship none we rely on.
|
|
108
|
+
module = ["nautilus_trader.*", "pandas.*", "pyarrow.*", "hypothesis.*"]
|
|
109
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Nyrobrain Python libraries.
|
|
2
|
+
|
|
3
|
+
This package is a namespace and deliberately exports nothing. Siblings are
|
|
4
|
+
expected alongside `nyrobrain.execution` — adrs migrates in later under the same
|
|
5
|
+
brand — so importing `nyrobrain` must never pull an engine, a transport or a
|
|
6
|
+
research stack into the process. Import the subpackage you want.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# nyrobrain.execution
|
|
2
|
+
|
|
3
|
+
The shared language of the execution library. A glossary, not a spec: it fixes what each word means so that the library, the portfolio that feeds it, and the dashboards that watch it all use it the same way.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
### The instruction
|
|
8
|
+
|
|
9
|
+
**Signal**:
|
|
10
|
+
One portfolio's complete target state at one instant: a target weight for every asset it holds. The whole truth, never a change set — so applying the same signal twice leaves the portfolio where applying it once did.
|
|
11
|
+
_Avoid_: Order, instruction, update, delta, rebalance
|
|
12
|
+
|
|
13
|
+
**Target weight**:
|
|
14
|
+
The share of a portfolio's notional budget an asset should occupy, signed for direction. Positive is long, negative is short. It says what to hold, never what to trade.
|
|
15
|
+
_Avoid_: Allocation, size, position size, exposure
|
|
16
|
+
|
|
17
|
+
**Gross exposure**:
|
|
18
|
+
The sum of a signal's target weights ignoring their signs — the total notional a portfolio wants deployed. A signal is invalid above 1. Distinct from net exposure, which cancels longs against shorts and therefore bounds direction rather than leverage.
|
|
19
|
+
_Avoid_: Total exposure, leverage, sum of weights
|
|
20
|
+
|
|
21
|
+
**Flat**:
|
|
22
|
+
Holding nothing in an asset. A target weight of zero and an asset's absence from a signal both mean flat; a correct publisher states the zero, and an absence is a defect worth alerting on.
|
|
23
|
+
_Avoid_: Closed, exited, zeroed, neutral
|
|
24
|
+
|
|
25
|
+
**Target**:
|
|
26
|
+
The position an asset should hold, in base units, implied by its target weight and the portfolio's budget, leverage and the current price. What we want; never what we do about it.
|
|
27
|
+
_Avoid_: Desired, goal, wanted position, ideal
|
|
28
|
+
|
|
29
|
+
**Actual**:
|
|
30
|
+
The position held at the venue. Read, never remembered — a figure the library keeps for itself between ticks is a figure that can go stale, and stale actuals are what made an OMS buy without end.
|
|
31
|
+
_Avoid_: Current, held, exchange position, real position
|
|
32
|
+
|
|
33
|
+
**Working**:
|
|
34
|
+
Every order of ours that could still fill, counted toward the position we are already committed to. Includes orders sent but not yet acknowledged: a quantity in flight has been committed even though nothing has filled and the venue has not replied.
|
|
35
|
+
_Avoid_: Open orders, pending, resting, outstanding, in-flight
|
|
36
|
+
|
|
37
|
+
**Delta**:
|
|
38
|
+
What remains to be traded for an asset: its target less its actual less its working. Derived afresh each tick, never carried forward, so an under-traded tick corrects itself and a repeated tick does nothing.
|
|
39
|
+
_Avoid_: Diff, gap, remainder, adjustment, order size
|
|
40
|
+
|
|
41
|
+
**Placement**:
|
|
42
|
+
Deciding how to work a delta into the book — how many orders, at what prices, amended or cancelled when. Owned by the user, who may substitute their own. It is told the quantity to trade and never the position, so that position arithmetic cannot re-enter through it.
|
|
43
|
+
_Avoid_: Execution, order strategy, routing, slicing
|
|
44
|
+
|
|
45
|
+
**Driver**:
|
|
46
|
+
The script that assembles an engine or a node, attaches the executor, runs it for a while and writes the report. Owned by the user, like placement — the library ships examples of one, never one of its own. What distinguishes it from placement is scope rather than ownership: placement decides how a single delta reaches the book, a driver decides which venue, which mode, which symbols and for how long.
|
|
47
|
+
_Avoid_: Runner, entrypoint, harness, main
|
|
48
|
+
|
|
49
|
+
**Committed**:
|
|
50
|
+
The quantity we already stand behind for an asset — what our live orders would add to the position if every one of them filled. Placement is judged on how much it changes this, never on the size of the orders it returns: repricing an order changes its price and commits nothing, which is what makes it legal when there is nothing left to trade.
|
|
51
|
+
_Avoid_: Exposure, pending, allocated, reserved
|
|
52
|
+
|
|
53
|
+
**Over-delivery**:
|
|
54
|
+
Placement returning orders that would trade further than the delta, or in the opposite direction. Always a defect: under-trading is bounded by doing nothing and corrects next tick, while over-trading has no ceiling and compounds.
|
|
55
|
+
_Avoid_: Overshoot, overfill, excess
|
|
56
|
+
|
|
57
|
+
**Base asset**:
|
|
58
|
+
What a signal names — the underlying an alpha has a view on, such as `BTC`. Independent of any venue.
|
|
59
|
+
_Avoid_: Ticker, symbol, instrument, coin
|
|
60
|
+
|
|
61
|
+
**Symbol**:
|
|
62
|
+
What a venue trades, such as `BTCUSDT`. One base asset maps to a different symbol on each venue; the mapping is configuration, never part of a signal.
|
|
63
|
+
_Avoid_: Pair, instrument, market, asset
|
|
64
|
+
|
|
65
|
+
**Sandbox**:
|
|
66
|
+
Paper trading against live market data with simulated fills. Needs no exchange credentials, and is therefore not a venue — no order it produces exists anywhere, and its results are a simulation whose realism is a setting rather than a fact.
|
|
67
|
+
_Avoid_: Paper, demo, testnet, simulation, dry run
|
|
68
|
+
|
|
69
|
+
### Provenance and ordering
|
|
70
|
+
|
|
71
|
+
**Publisher**:
|
|
72
|
+
The process that computes a portfolio's target state and emits signals. It owns the portfolio's alphas and their aggregation; the library is only ever its consumer.
|
|
73
|
+
_Avoid_: Producer, sender, portfolio process, upstream
|
|
74
|
+
|
|
75
|
+
**Epoch**:
|
|
76
|
+
A publisher process's identity — fixed for its lifetime and different after every restart. It exists so a restarted publisher, whose sequence has returned to zero, still outranks the stream it replaced.
|
|
77
|
+
_Avoid_: Run id, generation, boot id, version
|
|
78
|
+
|
|
79
|
+
**Sequence**:
|
|
80
|
+
A signal's position within one epoch, increasing with each signal that publisher emits. Meaningless across epochs on its own.
|
|
81
|
+
_Avoid_: Index, counter, offset, id
|
|
82
|
+
|
|
83
|
+
**Supersede**:
|
|
84
|
+
What a signal does to the last one applied when it is genuinely newer, judged on epoch and sequence together. A signal that does not supersede is inert rather than erroneous: redelivery is expected traffic.
|
|
85
|
+
_Avoid_: Override, replace, win, update
|
|
86
|
+
|
|
87
|
+
### Time and health
|
|
88
|
+
|
|
89
|
+
**Expected gap**:
|
|
90
|
+
How long a publisher says it will be before it speaks again, taken from its own aggregation cadence. Not the interval of the alphas it aggregates — those are usually far longer, and a deadline built on them would never fire.
|
|
91
|
+
_Avoid_: Interval, period, frequency, cadence, alpha interval
|
|
92
|
+
|
|
93
|
+
**Missed deadline**:
|
|
94
|
+
The expected gap elapsing, plus grace, without a signal arriving. It describes the health of the feed, never the validity of a signal: one that arrives afterwards is still applied, and applying it is what clears the condition.
|
|
95
|
+
_Avoid_: Timeout, stale signal, late signal, expired
|
|
96
|
+
|
|
97
|
+
**Escalated**:
|
|
98
|
+
The state a portfolio enters after several consecutive missed deadlines — its publisher is presumed gone rather than slow. Escalation stops new orders and cancels resting ones; it never exits positions, because a crash-looping process would then exit and rebuy in a loop, paying the spread each cycle.
|
|
99
|
+
_Avoid_: Halted, stopped, paused, killed, panicked
|
|
100
|
+
|
|
101
|
+
**Actionable**:
|
|
102
|
+
Young enough to trade on. A signal can be the newest ever seen and still too old to act on, in which case it is not traded — a signal that stale means the publisher was broken, and guessing is worse than stopping.
|
|
103
|
+
_Avoid_: Fresh, valid, current, live
|
|
104
|
+
|
|
105
|
+
### Rejection
|
|
106
|
+
|
|
107
|
+
**Invalid**:
|
|
108
|
+
Untrustworthy, and therefore not applied at all. Rejection is always whole-signal: applying the sound part of a target state would leave a position the strategy never asked for and cannot reason about.
|
|
109
|
+
_Avoid_: Malformed, bad, partial, unparseable
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Order execution: portfolio signal in, exchange orders out.
|
|
2
|
+
|
|
3
|
+
The library's job is target-position reconciliation. A signal names a target
|
|
4
|
+
weight per symbol; this package decides what orders that implies against the
|
|
5
|
+
position actually held at the venue, and places them.
|
|
6
|
+
|
|
7
|
+
Nothing nautilus-shaped crosses this package's public boundary. Callers work in
|
|
8
|
+
plain types — symbols as `str`, quantities and weights as `Decimal` — so the
|
|
9
|
+
engine underneath stays an implementation detail and hooks stay testable without
|
|
10
|
+
constructing framework objects.
|
|
11
|
+
|
|
12
|
+
Public so far: the input type, and the sizing and guard arithmetic the executor
|
|
13
|
+
is built on. The executor itself — the nautilus Strategy that drives these on a
|
|
14
|
+
timer — arrives with the hook-surface and testing-surface tickets.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from nyrobrain.execution.intake import Fetch, SignalSource
|
|
18
|
+
from nyrobrain.execution.ladder import LadderConfig, TouchLadder
|
|
19
|
+
from nyrobrain.execution.metrics import AegisMetrics, Fanout, Metric, MetricSink
|
|
20
|
+
from nyrobrain.execution.placement import (
|
|
21
|
+
Action,
|
|
22
|
+
Amend,
|
|
23
|
+
Cancel,
|
|
24
|
+
Context,
|
|
25
|
+
Place,
|
|
26
|
+
Placement,
|
|
27
|
+
Resting,
|
|
28
|
+
UnknownOrder,
|
|
29
|
+
committed_change,
|
|
30
|
+
validate,
|
|
31
|
+
)
|
|
32
|
+
from nyrobrain.execution.signal import (
|
|
33
|
+
MAX_GROSS_EXPOSURE,
|
|
34
|
+
SCHEMA_VERSION,
|
|
35
|
+
InvalidSignal,
|
|
36
|
+
Signal,
|
|
37
|
+
parse_signal,
|
|
38
|
+
)
|
|
39
|
+
from nyrobrain.execution.target import (
|
|
40
|
+
OverDelivery,
|
|
41
|
+
check_intent,
|
|
42
|
+
round_price,
|
|
43
|
+
round_quantity,
|
|
44
|
+
target_quantity,
|
|
45
|
+
trade_delta,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"MAX_GROSS_EXPOSURE",
|
|
50
|
+
"SCHEMA_VERSION",
|
|
51
|
+
"Action",
|
|
52
|
+
"AegisMetrics",
|
|
53
|
+
"Amend",
|
|
54
|
+
"Cancel",
|
|
55
|
+
"Context",
|
|
56
|
+
"Fanout",
|
|
57
|
+
"Fetch",
|
|
58
|
+
"InvalidSignal",
|
|
59
|
+
"LadderConfig",
|
|
60
|
+
"Metric",
|
|
61
|
+
"MetricSink",
|
|
62
|
+
"OverDelivery",
|
|
63
|
+
"Place",
|
|
64
|
+
"Placement",
|
|
65
|
+
"Resting",
|
|
66
|
+
"Signal",
|
|
67
|
+
"SignalSource",
|
|
68
|
+
"TouchLadder",
|
|
69
|
+
"UnknownOrder",
|
|
70
|
+
"check_intent",
|
|
71
|
+
"committed_change",
|
|
72
|
+
"parse_signal",
|
|
73
|
+
"round_price",
|
|
74
|
+
"round_quantity",
|
|
75
|
+
"target_quantity",
|
|
76
|
+
"trade_delta",
|
|
77
|
+
"validate",
|
|
78
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Publishing metrics to Aegis over the JetStream sidecar.
|
|
2
|
+
|
|
3
|
+
Requires the ``aegis`` extra, which carries the gRPC client. The payloads — and
|
|
4
|
+
every decision about what they contain — live in `nyrobrain.execution.metrics`,
|
|
5
|
+
which needs nothing installed at all. A second transport replaces this file and
|
|
6
|
+
inherits the rest.
|
|
7
|
+
|
|
8
|
+
**This is not a NATS client.** `bq-nats-client` depends only on `grpcio` and
|
|
9
|
+
`protobuf`: publishers reach JetStream through a sidecar authenticated by an API
|
|
10
|
+
key and hold no NATS credentials. That is convenient here, and it is also why
|
|
11
|
+
portfolio targets deliberately live in a different account — the sidecar applies
|
|
12
|
+
no per-subject authorization, so anything publishable through it is publishable
|
|
13
|
+
by any holder of a key.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from typing import Protocol
|
|
20
|
+
|
|
21
|
+
from nyrobrain.execution.metrics import Metric
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JetStreamClient(Protocol):
|
|
25
|
+
"""The part of `nats_client.NATSClient` this uses.
|
|
26
|
+
|
|
27
|
+
Stated as a protocol so a test can drive the real sink without a sidecar,
|
|
28
|
+
and so the dependency is visible at a glance rather than implied by an
|
|
29
|
+
import.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
async def jetstream(self) -> None: ...
|
|
33
|
+
|
|
34
|
+
async def js_publish(
|
|
35
|
+
self,
|
|
36
|
+
subject: str,
|
|
37
|
+
payload: bytes = b"",
|
|
38
|
+
headers: dict[str, str] | None = None,
|
|
39
|
+
) -> object: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class JetStreamSink:
|
|
43
|
+
"""A `MetricSink` that publishes through the sidecar.
|
|
44
|
+
|
|
45
|
+
Deliberately thin, and deliberately not defensive: a publish that fails
|
|
46
|
+
raises. The emitter counts the failure and the report shows it, so a sink
|
|
47
|
+
that swallowed exceptions would turn a dead metrics pipeline into one that
|
|
48
|
+
looks healthy — the exact failure this map exists to prevent, and the one
|
|
49
|
+
nothing downstream detects.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, *, open_client: Callable[[], JetStreamClient]) -> None:
|
|
53
|
+
#: A factory, not a client, and that is the whole point. `NATSClient`
|
|
54
|
+
#: binds its gRPC channel to an event loop when it is **constructed**,
|
|
55
|
+
#: and a driver builds its publisher before the node has a loop — so a
|
|
56
|
+
#: ready-made client binds to a loop that is closed by the time the
|
|
57
|
+
#: first heartbeat fires, and every publish dies with "attached to a
|
|
58
|
+
#: different loop". Deferring construction to the first publish means
|
|
59
|
+
#: the channel is built in whichever loop is actually going to use it.
|
|
60
|
+
#:
|
|
61
|
+
#: Measured the hard way: a live run reported `published 0, failures 3`
|
|
62
|
+
#: and nothing else until the round started recording the reason.
|
|
63
|
+
self._open_client = open_client
|
|
64
|
+
self._client: JetStreamClient | None = None
|
|
65
|
+
|
|
66
|
+
async def connect(self) -> None:
|
|
67
|
+
"""Build the client and configure JetStream, once.
|
|
68
|
+
|
|
69
|
+
Call this only from the loop that will publish — or not at all, since
|
|
70
|
+
`publish` does it. `js_publish` refuses with "call jetstream() first"
|
|
71
|
+
until it has run, and doing it per publish would add a round trip to
|
|
72
|
+
every row.
|
|
73
|
+
"""
|
|
74
|
+
if self._client is not None:
|
|
75
|
+
return
|
|
76
|
+
client = self._open_client()
|
|
77
|
+
await client.jetstream()
|
|
78
|
+
self._client = client
|
|
79
|
+
|
|
80
|
+
async def publish(self, metric: Metric) -> None:
|
|
81
|
+
# Lazily, so the channel is built in whichever loop is publishing.
|
|
82
|
+
await self.connect()
|
|
83
|
+
assert self._client is not None
|
|
84
|
+
# `msg_id` becomes `Nats-Msg-Id` here and nowhere else. The client does
|
|
85
|
+
# `headers.setdefault("Nats-Msg-Id", uuid4())`, so omitting it would
|
|
86
|
+
# publish successfully while silently losing every cross-publish
|
|
87
|
+
# collapse — retries would still be idempotent, and nothing else would.
|
|
88
|
+
await self._client.js_publish(
|
|
89
|
+
metric.subject,
|
|
90
|
+
metric.payload,
|
|
91
|
+
headers={"Nats-Msg-Id": metric.msg_id},
|
|
92
|
+
)
|