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.
Files changed (91) hide show
  1. nyrobrain-0.1.0/.gitignore +41 -0
  2. nyrobrain-0.1.0/.python-version +1 -0
  3. nyrobrain-0.1.0/PKG-INFO +100 -0
  4. nyrobrain-0.1.0/README.md +78 -0
  5. nyrobrain-0.1.0/justfile +5 -0
  6. nyrobrain-0.1.0/pyproject.toml +109 -0
  7. nyrobrain-0.1.0/src/nyrobrain/__init__.py +7 -0
  8. nyrobrain-0.1.0/src/nyrobrain/execution/CONTEXT.md +109 -0
  9. nyrobrain-0.1.0/src/nyrobrain/execution/__init__.py +78 -0
  10. nyrobrain-0.1.0/src/nyrobrain/execution/aegis.py +92 -0
  11. nyrobrain-0.1.0/src/nyrobrain/execution/backtest.py +184 -0
  12. nyrobrain-0.1.0/src/nyrobrain/execution/candles.py +218 -0
  13. nyrobrain-0.1.0/src/nyrobrain/execution/credentials.py +203 -0
  14. nyrobrain-0.1.0/src/nyrobrain/execution/data.py +95 -0
  15. nyrobrain-0.1.0/src/nyrobrain/execution/driver.py +219 -0
  16. nyrobrain-0.1.0/src/nyrobrain/execution/emitter.py +210 -0
  17. nyrobrain-0.1.0/src/nyrobrain/execution/examples/README.md +173 -0
  18. nyrobrain-0.1.0/src/nyrobrain/execution/examples/__init__.py +17 -0
  19. nyrobrain-0.1.0/src/nyrobrain/execution/examples/oms.py +804 -0
  20. nyrobrain-0.1.0/src/nyrobrain/execution/examples/over_delivery_guard.py +83 -0
  21. nyrobrain-0.1.0/src/nyrobrain/execution/examples/sizing_a_target.py +70 -0
  22. nyrobrain-0.1.0/src/nyrobrain/execution/examples/your_own_placement.py +117 -0
  23. nyrobrain-0.1.0/src/nyrobrain/execution/executor.py +837 -0
  24. nyrobrain-0.1.0/src/nyrobrain/execution/freshness.py +108 -0
  25. nyrobrain-0.1.0/src/nyrobrain/execution/ingest.py +171 -0
  26. nyrobrain-0.1.0/src/nyrobrain/execution/intake.py +194 -0
  27. nyrobrain-0.1.0/src/nyrobrain/execution/ladder.py +256 -0
  28. nyrobrain-0.1.0/src/nyrobrain/execution/metrics.py +463 -0
  29. nyrobrain-0.1.0/src/nyrobrain/execution/pending.py +96 -0
  30. nyrobrain-0.1.0/src/nyrobrain/execution/placement.py +177 -0
  31. nyrobrain-0.1.0/src/nyrobrain/execution/prime.py +232 -0
  32. nyrobrain-0.1.0/src/nyrobrain/execution/ratelimit.py +97 -0
  33. nyrobrain-0.1.0/src/nyrobrain/execution/reconciliation.py +213 -0
  34. nyrobrain-0.1.0/src/nyrobrain/execution/report.py +649 -0
  35. nyrobrain-0.1.0/src/nyrobrain/execution/rules.py +72 -0
  36. nyrobrain-0.1.0/src/nyrobrain/execution/runner.py +328 -0
  37. nyrobrain-0.1.0/src/nyrobrain/execution/sandbox.py +109 -0
  38. nyrobrain-0.1.0/src/nyrobrain/execution/signal.py +236 -0
  39. nyrobrain-0.1.0/src/nyrobrain/execution/soak.py +761 -0
  40. nyrobrain-0.1.0/src/nyrobrain/execution/store.py +95 -0
  41. nyrobrain-0.1.0/src/nyrobrain/execution/target.py +169 -0
  42. nyrobrain-0.1.0/src/nyrobrain/execution/testing.py +273 -0
  43. nyrobrain-0.1.0/src/nyrobrain/execution/venue.py +335 -0
  44. nyrobrain-0.1.0/src/nyrobrain/py.typed +0 -0
  45. nyrobrain-0.1.0/tests/conftest.py +36 -0
  46. nyrobrain-0.1.0/tests/fixtures/aegis/capture_incumbent.py +128 -0
  47. nyrobrain-0.1.0/tests/fixtures/aegis/incumbent_payloads.json +32 -0
  48. nyrobrain-0.1.0/tests/fixtures/candles/interval_1m_symbol_BTCUSDT_2024-03-01.parquet +0 -0
  49. nyrobrain-0.1.0/tests/fixtures/signals.jsonl +4 -0
  50. nyrobrain-0.1.0/tests/instruments.py +51 -0
  51. nyrobrain-0.1.0/tests/pricepaths.py +122 -0
  52. nyrobrain-0.1.0/tests/test_aegis.py +218 -0
  53. nyrobrain-0.1.0/tests/test_backtest.py +343 -0
  54. nyrobrain-0.1.0/tests/test_candles.py +173 -0
  55. nyrobrain-0.1.0/tests/test_data.py +156 -0
  56. nyrobrain-0.1.0/tests/test_driver.py +306 -0
  57. nyrobrain-0.1.0/tests/test_emitter.py +58 -0
  58. nyrobrain-0.1.0/tests/test_examples.py +52 -0
  59. nyrobrain-0.1.0/tests/test_executor_faults.py +721 -0
  60. nyrobrain-0.1.0/tests/test_executor_rules.py +100 -0
  61. nyrobrain-0.1.0/tests/test_executor_smoke.py +406 -0
  62. nyrobrain-0.1.0/tests/test_freshness.py +140 -0
  63. nyrobrain-0.1.0/tests/test_fuzz_engine.py +510 -0
  64. nyrobrain-0.1.0/tests/test_ingest.py +134 -0
  65. nyrobrain-0.1.0/tests/test_intake.py +221 -0
  66. nyrobrain-0.1.0/tests/test_invariants_stateful.py +193 -0
  67. nyrobrain-0.1.0/tests/test_ladder.py +454 -0
  68. nyrobrain-0.1.0/tests/test_live_guard.py +406 -0
  69. nyrobrain-0.1.0/tests/test_metric_round.py +219 -0
  70. nyrobrain-0.1.0/tests/test_metrics.py +371 -0
  71. nyrobrain-0.1.0/tests/test_oms.py +520 -0
  72. nyrobrain-0.1.0/tests/test_pending.py +161 -0
  73. nyrobrain-0.1.0/tests/test_placement.py +295 -0
  74. nyrobrain-0.1.0/tests/test_prime.py +221 -0
  75. nyrobrain-0.1.0/tests/test_ratelimit.py +261 -0
  76. nyrobrain-0.1.0/tests/test_reconciliation.py +226 -0
  77. nyrobrain-0.1.0/tests/test_registration.py +128 -0
  78. nyrobrain-0.1.0/tests/test_report.py +665 -0
  79. nyrobrain-0.1.0/tests/test_restart.py +343 -0
  80. nyrobrain-0.1.0/tests/test_runner.py +159 -0
  81. nyrobrain-0.1.0/tests/test_sandbox.py +128 -0
  82. nyrobrain-0.1.0/tests/test_signal.py +334 -0
  83. nyrobrain-0.1.0/tests/test_signed_leaves.py +148 -0
  84. nyrobrain-0.1.0/tests/test_smoke.py +84 -0
  85. nyrobrain-0.1.0/tests/test_soak.py +534 -0
  86. nyrobrain-0.1.0/tests/test_stale_feeds.py +260 -0
  87. nyrobrain-0.1.0/tests/test_store.py +137 -0
  88. nyrobrain-0.1.0/tests/test_target.py +365 -0
  89. nyrobrain-0.1.0/tests/test_testing.py +250 -0
  90. nyrobrain-0.1.0/tests/test_venue.py +336 -0
  91. 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
@@ -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.
@@ -0,0 +1,5 @@
1
+ build:
2
+ uv run hatch build
3
+
4
+ publish:
5
+ uv publish
@@ -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
+ )