@senpi/runtime 3.0.9-dev.20260710204324 → 3.0.9-dev.feat-strategy-compos.20260716233548
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.
- package/dist/cli/composer-commands.d.ts +37 -0
- package/dist/cli/composer-commands.d.ts.map +1 -0
- package/dist/cli/composer-commands.js +396 -0
- package/dist/cli/composer-commands.js.map +1 -0
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +2 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/cli/validate-runtime-config.mjs +190 -0
- package/dist/composer/AUTHORING.md +435 -0
- package/dist/composer/__init__.py +29 -0
- package/dist/composer/authoring/__init__.py +59 -0
- package/dist/composer/authoring/answers.py +297 -0
- package/dist/composer/authoring/archetypes/__init__.py +26 -0
- package/dist/composer/authoring/archetypes/_common.py +410 -0
- package/dist/composer/authoring/archetypes/breakout.py +134 -0
- package/dist/composer/authoring/archetypes/classifier.py +109 -0
- package/dist/composer/authoring/archetypes/skeleton.py +76 -0
- package/dist/composer/authoring/archetypes/thesis_fund.py +103 -0
- package/dist/composer/authoring/archetypes/trend_momentum.py +132 -0
- package/dist/composer/authoring/builder.py +191 -0
- package/dist/composer/authoring/emit.py +264 -0
- package/dist/composer/authoring/gaps.py +115 -0
- package/dist/composer/authoring/generate.py +131 -0
- package/dist/composer/authoring/presets.py +168 -0
- package/dist/composer/authoring/runtime_emit.py +266 -0
- package/dist/composer/authoring/strategy_manifest.py +94 -0
- package/dist/composer/cli.py +487 -0
- package/dist/composer/compiler/__init__.py +22 -0
- package/dist/composer/compiler/compile.py +739 -0
- package/dist/composer/examples/breakout_swing.graph.yaml +121 -0
- package/dist/composer/examples/conviction_scorer.graph.yaml +216 -0
- package/dist/composer/examples/macro_thesis_basket.graph.yaml +247 -0
- package/dist/composer/examples/pre_listing_classifier.graph.yaml +204 -0
- package/dist/composer/examples/trend_momentum_majors.graph.yaml +133 -0
- package/dist/composer/generated/README.md +22 -0
- package/dist/composer/generated/catalog.md +118 -0
- package/dist/composer/generated/describe.md +1168 -0
- package/dist/composer/generated/graph.schema.json +1946 -0
- package/dist/composer/primitives/__init__.py +320 -0
- package/dist/composer/primitives/_candles.py +76 -0
- package/dist/composer/primitives/adapter.py +363 -0
- package/dist/composer/primitives/emitters.py +142 -0
- package/dist/composer/primitives/filters.py +71 -0
- package/dist/composer/primitives/indicators.py +326 -0
- package/dist/composer/primitives/rankers.py +40 -0
- package/dist/composer/primitives/scorers.py +100 -0
- package/dist/composer/primitives/sources.py +979 -0
- package/dist/composer/primitives/stateful.py +254 -0
- package/dist/composer/pure_fn/__init__.py +34 -0
- package/dist/composer/pure_fn/checks.py +39 -0
- package/dist/composer/pure_fn/models.py +177 -0
- package/dist/composer/pure_fn/purity.py +311 -0
- package/dist/composer/pure_fn/runtests.py +155 -0
- package/dist/composer/registry/__init__.py +40 -0
- package/dist/composer/registry/generate.py +425 -0
- package/dist/composer/registry/mcp-tools-snapshot.json +193 -0
- package/dist/composer/registry/mcp_closure.py +311 -0
- package/dist/composer/registry/models.py +417 -0
- package/dist/composer/registry/nodes.py +1297 -0
- package/dist/composer/requirements.txt +21 -0
- package/dist/composer/scaffold_path.py +118 -0
- package/dist/composer/templates/__init__.py +13 -0
- package/dist/composer/templates/scan.py.j2 +34 -0
- package/dist/composer/validator/__init__.py +29 -0
- package/dist/composer/validator/desugar.py +133 -0
- package/dist/composer/validator/errors.py +227 -0
- package/dist/composer/validator/graph_checks.py +443 -0
- package/dist/composer/validator/loader.py +110 -0
- package/dist/composer/validator/schema_checks.py +147 -0
- package/dist/composer/validator/validate.py +86 -0
- package/dist/composer/vendoring.py +103 -0
- package/dist/composer/verbs/__init__.py +49 -0
- package/dist/composer/verbs/_box.py +308 -0
- package/dist/composer/verbs/check.py +254 -0
- package/dist/composer/verbs/close.py +494 -0
- package/dist/composer/verbs/deploy.py +279 -0
- package/dist/composer/verbs/docs.py +30 -0
- package/dist/composer/verbs/fund.py +498 -0
- package/dist/composer/verbs/install.py +586 -0
- package/dist/composer/verbs/runtime_validate.py +422 -0
- package/dist/composer/verbs/smoke.py +134 -0
- package/dist/composer/verbs/status.py +383 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -9
- package/dist/index.js.map +1 -1
- package/dist/runtime/load-runtime-config.d.ts +17 -0
- package/dist/runtime/load-runtime-config.d.ts.map +1 -1
- package/dist/runtime/load-runtime-config.js +2 -2
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime-api/config.d.ts +2 -2
- package/dist/runtime-api/config.js +4 -4
- package/dist/runtime-api/config.js.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.d.ts +3 -5
- package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.js +5 -14
- package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +2 -8
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/skills-manager/manifest.d.ts.map +1 -1
- package/dist/skills-manager/manifest.js +1 -3
- package/dist/skills-manager/manifest.js.map +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -3
- package/package.json +4 -2
- package/dist/runtime-api/intake/ingest-mutex.d.ts +0 -16
- package/dist/runtime-api/intake/ingest-mutex.d.ts.map +0 -1
- package/dist/runtime-api/intake/ingest-mutex.js +0 -18
- package/dist/runtime-api/intake/ingest-mutex.js.map +0 -1
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# Authoring a strategy with the Strategy Composer
|
|
2
|
+
|
|
3
|
+
Quickstart for a fresh Claude Code session (or a human) turning a strategy idea into a
|
|
4
|
+
deployable trading-strategy graph. This is the same teaching surface the eval harness
|
|
5
|
+
gives Qwen (`composer/eval/prompt.py`), adapted for an interactive session instead of a
|
|
6
|
+
scripted transcript.
|
|
7
|
+
|
|
8
|
+
## 1. Mental model
|
|
9
|
+
|
|
10
|
+
An idea becomes a strategy in four moves: write an **answers file** (a typed interview —
|
|
11
|
+
archetype, universe, intervals, cardinality, risk, and your EDGE in plain words) → run
|
|
12
|
+
`composer new` to generate a **COMPLETE, VALID graph** for that archetype, with the
|
|
13
|
+
mechanically-bindable values already filled in and the bespoke slots marked
|
|
14
|
+
`TODO(edit)` with a working default → **edit only what needs editing** (the TODO slots,
|
|
15
|
+
and nothing else unless you have a reason) → run `composer check` for **one GREEN/RED
|
|
16
|
+
verdict** with located, actionable teaching errors → iterate until GREEN → `composer
|
|
17
|
+
compile` (materialize `scan.py`) or `composer deploy --out <dir>` (stage the COMPLETE
|
|
18
|
+
standalone unit — runtime.yaml + scanners/ + strategy.yaml + manifest). The spec file (the graph YAML) **is** the state — there is no separate
|
|
19
|
+
journal — so at any point `composer status <file>` tells you exactly where you are in
|
|
20
|
+
`new → check → deploy` and names the next verb to run.
|
|
21
|
+
|
|
22
|
+
If the idea doesn't fit a known archetype, `composer new` falls back to a minimal valid
|
|
23
|
+
skeleton (still GREEN, still a real starting point). If the idea needs a capability the
|
|
24
|
+
registry doesn't have, `composer new` refuses to write anything and instead prints a
|
|
25
|
+
**gap report** — that is the correct, honest outcome for something not yet buildable, not
|
|
26
|
+
a bug to work around.
|
|
27
|
+
|
|
28
|
+
## 2. Setup
|
|
29
|
+
|
|
30
|
+
Run everything from the worktree root:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd /Users/duncan/Senpi/worktrees/trading_runtime/strategy-composer-poc
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The composer has no installed console script; invoke it as a module through `uvx` with
|
|
37
|
+
its runtime deps pinned inline (no venv setup needed):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx --with pydantic --with jinja2 --with pyyaml --with jsonschema python -m composer.cli <verb> ...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Write your working answers files and generated graphs to `drafts/` (untracked, gitignored
|
|
44
|
+
scratch space) — never into `composer/examples/` (those are the committed corpus
|
|
45
|
+
fixtures) or `composer/generated/` (registry-derived, do-not-edit).
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mkdir -p drafts
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**`scaffold` resolves itself — never set a manual `PYTHONPATH` for it.** `check`/`deploy` run
|
|
52
|
+
the emitted `scan.py` under the runtime's `scaffold` harness. The composer finds `scaffold`
|
|
53
|
+
automatically in BOTH layouts — a dev checkout (`src/scanners/external`) and a claw dist install
|
|
54
|
+
(`dist/scanners/external`, no `src/` segment) — so you do not export any path. If it genuinely
|
|
55
|
+
cannot be found, the `smoke` stage fails RED with a teaching error naming the fix: on a claw run
|
|
56
|
+
`npm run build` (its `copy:external-scanners` step stages the harness), or set
|
|
57
|
+
`COMPOSER_SCAFFOLD_DIR` to the dir that CONTAINS `scaffold/`. Do **not** `pip install scaffold` —
|
|
58
|
+
that is an unrelated PyPI package and will not satisfy the import (the harness ships with the
|
|
59
|
+
runtime, never from PyPI).
|
|
60
|
+
|
|
61
|
+
## 3. Verb reference
|
|
62
|
+
|
|
63
|
+
Every verb takes `--json` for machine-readable output; omit it for a human-readable
|
|
64
|
+
report.
|
|
65
|
+
|
|
66
|
+
| Verb | What it does | Example |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `new` | Interview answers in, a COMPLETE VALID graph out (or a gap report). Refuses to overwrite a hand-edited graph without `--force`. | `uvx --with pydantic --with jinja2 --with pyyaml --with jsonschema python -m composer.cli new my_strat --archetype trend_momentum --answers drafts/my_strat.answers.yaml -o drafts` |
|
|
69
|
+
| `validate` | Schema + graph-semantic teaching-error report (exit 0 iff clean). | `... python -m composer.cli validate drafts/my_strat.graph.yaml` |
|
|
70
|
+
| `check` | ONE verdict: validate + pure_fn inline tests + compile + one-tick smoke, staged so a failing stage skips the rest. | `... python -m composer.cli check drafts/my_strat.graph.yaml` |
|
|
71
|
+
| `compile` | Materialize the graph into `scan.py` on the `external_scanner` contract. | `... python -m composer.cli compile drafts/my_strat.graph.yaml -o drafts/scan.py` |
|
|
72
|
+
| `deploy` | Stage the COMPLETE standalone unit as a package tree (`strategy.yaml`, `main/runtime.yaml`, `main/scanners/scan.py`, `main/scanners/strategy_primitives/` — the vendored primitives snapshot — `main/<id>.graph.yaml`, `manifest.json`, `deploy.json`); HARD-gated on a GREEN `check`. Does not install. The verdict always reports the ABSOLUTE staged dir (a relative `-o` resolves under your current working dir), and a re-deploy of unchanged bytes is `UNCHANGED` — "already staged at `<abs>`, nothing rewritten". | `... python -m composer.cli deploy drafts/my_strat.graph.yaml -o drafts/dist` |
|
|
73
|
+
| `fund` | Create + fund the strategy's ONE wallet via MCP `strategy_create_custom_strategy`. `--budget <usd>` is REQUIRED and EXPLICIT (≥ $100) — agree it with the user; it is NEVER derived or split. Preflights the visible balance (free Hyperliquid + EVM USDC), creates, records `wallet.json` BEFORE polling, then polls briefly to ACTIVE. Idempotent + safe to re-run while funds settle (re-run IS the poll): existing ACTIVE wallet → `FUNDED` no-op; still initializing → `PENDING`; stale → CMP231. Short balance → CMP232 (states the shortfall + the bridged-funds visibility lag). Teaching errors CMP230-233 (+ CMP223 for missing creds). | `... python -m composer.cli fund drafts/dist/my_strat --budget 250` |
|
|
74
|
+
| `install` | Bind a staged unit onto THIS box: preflight (manifest verify + GREEN oracle verdict) → copy to a content-addressed immutable dir `<senpi-state>/strategies/<id>/<hash8>/` (temp+verify+atomic-rename) → bind the wallet into the COPIED `runtime.yaml` (staged source stays pristine) → invoke the frozen `openclaw senpi runtime create -p …` consumer. Pass `--wallet 0x…` explicitly, OR omit it to AUTO-BIND the funded `wallet.json` from `composer fund` (only if MCP confirms the strategy is ACTIVE; explicit `--wallet` always wins). Failures map to teaching errors (CMP200-219, + CMP206 when there is no ACTIVE wallet to auto-bind). Resumable-by-artifact. | `... python -m composer.cli fund drafts/dist/my_strat --budget 250` then `... install drafts/dist/my_strat` |
|
|
75
|
+
| `close` | SAFE TEARDOWN of a running strategy: STOP the runtime first (`openclaw senpi runtime delete`), CONFIRM it is gone, and only THEN trigger MCP `strategy_close` (flattens all positions, returns funds). Idempotent — re-running IS the poll to `CLOSED`. Target is a strategy name / canonical hash dir / runtime id / `0x…` wallet / strategyId; on-box artifacts resolve first, else legacy `strategy_list` attribution (skillName / strategyId). Failures map to teaching errors (CMP220-225). | `... python -m composer.cli close my_strat --state-dir ~/.openclaw/senpi-state` |
|
|
76
|
+
| `status` | Derives your lifecycle position (`absent` / `invalid` / `valid_undeployed` / `deployed_stale` / `deployed_current`) from the file + check + staged manifest, and names the next verb. Pass `--state-dir` to EXTEND the chain onto a box: `installable` → `installed_unregistered` → `installed_stopped` → `installed_running`, distinguishing which hash is installed/running vs merely a prior landed version. Functional liveness (scan() has actually ticked) is reported honestly as NOT-checked — `runtime list` only proves the process is up. No journal — the file is the state. | `... python -m composer.cli status drafts/my_strat.graph.yaml -o drafts/dist --state-dir ~/.openclaw/senpi-state` |
|
|
77
|
+
| `catalog` | The progressive-disclosure node index (names + one-liners + param summary) — level 1. Reference it; don't inline the whole thing into a prompt. | `... python -m composer.cli catalog` |
|
|
78
|
+
| `describe` | Full ports/params/example/cost for ONE node — level 2, on-demand deep-dive. | `... python -m composer.cli describe candles_fetch` |
|
|
79
|
+
|
|
80
|
+
When in doubt about what a node actually does or which ports it exposes, run `describe`
|
|
81
|
+
rather than guessing from the catalog one-liner.
|
|
82
|
+
|
|
83
|
+
## 4. The answers-file format
|
|
84
|
+
|
|
85
|
+
The answers file is the interview output: it captures the 7 decisions that are the
|
|
86
|
+
type-signature of `scan()` — archetype, universe, intervals, cardinality, memory
|
|
87
|
+
(dedup TTL), risk knobs, and the EDGE (free text, carried verbatim; the machine never
|
|
88
|
+
interprets it) — plus any `requires:` capabilities. Full field definitions live in
|
|
89
|
+
`composer/authoring/answers.py`; the shape:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
archetype: trend_momentum # trend_momentum | breakout | thesis_fund | classifier
|
|
93
|
+
strategy: my_strat # short id -> <id>.graph.yaml
|
|
94
|
+
universe:
|
|
95
|
+
whitelist: ["BTC", "ETH", "SOL"] # fixed asset list (trend_momentum / breakout)
|
|
96
|
+
long_basket: [] # thesis_fund LONG leg (use instead of whitelist)
|
|
97
|
+
short_basket: [] # thesis_fund SHORT leg
|
|
98
|
+
discover: false # classifier / breakout: derive the universe from the live board
|
|
99
|
+
dex: "" # discovery: venue filter
|
|
100
|
+
name_prefix: "" # discovery: instrument name prefix
|
|
101
|
+
max_leverage_cap: 5.0 # discovery: leverage ceiling
|
|
102
|
+
min_day_volume: 0.0 # discovery: daily-volume floor
|
|
103
|
+
intervals: ["4h"] # candle intervals, e.g. ["1h", "4h"]
|
|
104
|
+
max_positions: 1 # 1 = single best; >1 = rank a pool, cap to open slots
|
|
105
|
+
ttl_seconds: 240 # signal-dedup memory window
|
|
106
|
+
exit_preset: null # LIVE: protected_standard | protected_tight | protected_wide.
|
|
107
|
+
# null -> `composer new` BINDS protected_standard (PROTECTION BY DEFAULT).
|
|
108
|
+
unmanaged: false # explicit opt-out of exit protection; without it, an exit-less unit is RED (CMP120)
|
|
109
|
+
risk:
|
|
110
|
+
margin_pct: 5.0 # PERCENT of withdrawable (0,100], never a fraction/USD
|
|
111
|
+
leverage: 3.0 # signal_envelope also exposes clamp_leverage_to_venue (real venue-max clamp when the board is wired), valid_for_seconds, signal_type
|
|
112
|
+
drawdown_halt_pct: null # guard rail: halt the strategy at this equity drawdown % (else preset default)
|
|
113
|
+
max_consecutive_losses: null # guard rail: pause after N losses
|
|
114
|
+
cooldown_seconds: null # guard rail: post-loss cooldown seconds (>= 60)
|
|
115
|
+
max_entries_per_day: null # guard rail: per-day entry cap (>= 1)
|
|
116
|
+
edge: >
|
|
117
|
+
Your edge in plain words — the actual thesis (e.g. "ride assets breaking to fresh highs
|
|
118
|
+
with accelerating momentum, volume confirmation, and smart money leaning the same way").
|
|
119
|
+
This is carried verbatim into the graph; nothing parses it, so be as specific as you want.
|
|
120
|
+
requires: [] # [{name, kind, need}] — a capability that may not exist yet;
|
|
121
|
+
# a name the registry lacks becomes a gap report, nothing written
|
|
122
|
+
# --- OPTIONAL expressiveness (omit both for the archetype's working defaults) ---
|
|
123
|
+
indicators: # ADD registry indicator nodes and wire them into the scorer,
|
|
124
|
+
# so they become addressable by id from `scoring` below.
|
|
125
|
+
- id: fresh_raw # the node's graph id (a feature key = SOURCE NODE ID)
|
|
126
|
+
node: recent_high_bars_ago # any registry indicator (see `composer catalog`)
|
|
127
|
+
with: { window: 24, interval: "1h" } # the node's params
|
|
128
|
+
# port: bars_ago # optional: which output feeds the scorer (default: the node's sole output)
|
|
129
|
+
- id: fresh # a THRESHOLDED gate over the raw feature above (C1b)
|
|
130
|
+
node: threshold_gate # compare a per-asset feature vs a threshold -> 1.0/0.0
|
|
131
|
+
with: { op: lte, value: 5 } # op is lt|lte|gt|gte; here: bars_ago <= 5 means "fresh"
|
|
132
|
+
source: fresh_raw # CHAIN the input off fresh_raw's output (not candles)
|
|
133
|
+
scoring: # RE-SELECT the scorer's hard-vs-weighted split over the feature
|
|
134
|
+
# nodes (built-in + any `indicators` above). Each field omitted
|
|
135
|
+
# -> keep the archetype default; set -> override it.
|
|
136
|
+
required_gates: ["brk", "fresh", "vol", "lean"] # feature ids that must ALL pass (hard gates)
|
|
137
|
+
weights: { mom: 2.0, vol: 1.0, lean: 0.05 } # feature id -> weighted-sum weight
|
|
138
|
+
min_score: 0 # minimum score to emit
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Universe modes**: `whitelist` is a fixed list you already know (trend_momentum,
|
|
142
|
+
breakout, trend_conviction shapes). `long_basket`/`short_basket` are the two legs of a
|
|
143
|
+
fixed macro basket (thesis_fund — a relative-value long/short). `discover: true` (+ `dex`,
|
|
144
|
+
`name_prefix`, `max_leverage_cap`, `min_day_volume`) derives the tradeable set each tick
|
|
145
|
+
from the live instrument board by a signature instead of a fixed list (classifier and
|
|
146
|
+
breakout). `trend_momentum` is fixed-universe only — a `discover: true` answer is rejected
|
|
147
|
+
with a teaching error pointing at `breakout` / `classifier` (it never silently emits an
|
|
148
|
+
empty universe).
|
|
149
|
+
|
|
150
|
+
**Archetypes**: `trend_momentum` (fixed universe → indicators → hard-gates + weighted-sum
|
|
151
|
+
scorer), `breakout` (a directional range-break as the hard gate, fixed basket OR discovered
|
|
152
|
+
universe — the hawk shape), `classifier` (a pure_fn direction classifier on a discovered or
|
|
153
|
+
fixed universe), `thesis_fund` (fixed long/short basket, direction fixed per leg). An
|
|
154
|
+
`archetype` the composer doesn't recognize falls back to the **off-map skeleton** — still a
|
|
155
|
+
valid, generated starting graph, just minimal; expect to do more hand-editing from there.
|
|
156
|
+
|
|
157
|
+
**Expressiveness (`scoring` / `indicators`)**: both are OPTIONAL and default to the
|
|
158
|
+
archetype's built-in scorer. `scoring` re-selects which feature nodes are HARD gates
|
|
159
|
+
(`required_gates`) versus weighted contributors (`weights`) and the score floor
|
|
160
|
+
(`min_score`), addressing feature nodes by their SOURCE NODE ID. `indicators` adds
|
|
161
|
+
registry indicator nodes (e.g. `breakout_detect` with a `window`) and wires them into the
|
|
162
|
+
scorer so they become addressable from `scoring`. A `scoring` name that matches no feature
|
|
163
|
+
node, or an `indicators` node the registry lacks, is a loud generation error — never a
|
|
164
|
+
silent drop. This is how, e.g., the `breakout` archetype expresses a four-hard-gate
|
|
165
|
+
discovery variant without any hand editing.
|
|
166
|
+
|
|
167
|
+
**Thresholded gates (`threshold_gate` + `source`)**: a hard gate must be TRUTHY, so a raw
|
|
168
|
+
numeric feature (e.g. `recent_high_bars_ago`, whose output is bars-since-the-high) can't be
|
|
169
|
+
gated directly — 0 (a high set this bar) reads falsy, a stale 40 reads truthy, exactly
|
|
170
|
+
inverted. Add a `threshold_gate` indicator (`op` one of `lt|lte|gt|gte`, `value` the
|
|
171
|
+
threshold) and CHAIN it off the raw feature with `source:` — its input then reads that node's
|
|
172
|
+
output instead of candles, emitting a real `1.0`/`0.0` gate. So a "fresh entry within 5 bars"
|
|
173
|
+
gate is `fresh = threshold_gate(op: lte, value: 5, source: fresh_raw)` over `fresh_raw =
|
|
174
|
+
recent_high_bars_ago(...)`; a "don't chase a runaway" filter is `threshold_gate(op: lt, value:
|
|
175
|
+
15, source: own_ret)` over an `own_return_24h` feature. `source` names an EARLIER `indicators`
|
|
176
|
+
entry or a built-in feature node of the archetype; an unknown source, or one whose output
|
|
177
|
+
type doesn't match the consumer's input, is a loud generation error.
|
|
178
|
+
|
|
179
|
+
Get a blank skeleton for any archetype with `composer describe`-adjacent tooling, or just
|
|
180
|
+
copy the block above and strip fields you don't need — every field has a sane default via
|
|
181
|
+
`Answers()`, so a short file (just `archetype`, `strategy`, `universe`, `edge`) is enough
|
|
182
|
+
to get started.
|
|
183
|
+
|
|
184
|
+
## 5. Editing the generated graph
|
|
185
|
+
|
|
186
|
+
`composer new` writes `# TODO(edit): ...` comments at every slot that needs a real
|
|
187
|
+
decision (already filled with a working default so the graph validates unedited) — these
|
|
188
|
+
are the minimum you should look at, not the limit of what you may change. You have full
|
|
189
|
+
edit rights over the whole graph: add/remove nodes, rewire edges, retune any param.
|
|
190
|
+
|
|
191
|
+
Two ways to express bespoke logic:
|
|
192
|
+
|
|
193
|
+
- **Registry nodes** (`node: <name>`, see `catalog`) — parameterized, pre-built pipeline
|
|
194
|
+
steps: sources, indicators, filters, scorers, rankers, the emitter, and three stateful
|
|
195
|
+
nodes (`ttl_dedup_state` for signal dedup; `snapshot_diff_state` for a bounded rolling
|
|
196
|
+
per-key growth ledger; `cohort_cache_state` to pin an expensive fetch across ticks). Wire
|
|
197
|
+
per-asset features into a scorer's `features` input to MERGE them (keyed by producing node
|
|
198
|
+
id). The cohort/cross-trader vocabulary (the whalehunter thesis) is now expressible end to
|
|
199
|
+
end: `top_traders_fetch` → `cohort_cache_state` → `trader_states_fetch` → `aggregate_bias`
|
|
200
|
+
(the one sanctioned fn→primitive promotion: cross-trader net/gross bias per coin) +
|
|
201
|
+
`snapshot_diff_state` → a bespoke cohort-scoring `pure_fn` → emit. The **world-coverage sources** span the
|
|
202
|
+
whole scanner-usable Senpi MCP read surface (proven fully-mapped by the closure check, see
|
|
203
|
+
below): market signals (funding regime/history, cross-asset flows, prices, momentum events),
|
|
204
|
+
trader intelligence (top-traders + batch trader-state, the fast 4h smart-money leaderboard
|
|
205
|
+
`leaderboard_top_fetch` and its single-trader companion `leaderboard_trader_fetch`, leaderboard
|
|
206
|
+
trader positions, copy-strategy ranking `top_strategies_fetch`, the Arena leaderboard), and
|
|
207
|
+
own-account reads (order/PnL/clearinghouse/history, banked realized-PnL on open positions).
|
|
208
|
+
They emit `record_list` outputs — ordered lists of typed dict records — and, for
|
|
209
|
+
`funding_regime_fetch`, a `label` output (a market-wide categorical string like `LONG_CROWDED`).
|
|
210
|
+
Neither type is asset-keyed: their intended consumer is a `kind: pure_fn` node that declares a
|
|
211
|
+
`record_list`/`label` input and reads the fields itself (e.g. project addresses out of
|
|
212
|
+
`top_traders_fetch.traders` and wire that record_list straight into `trader_states_fetch.traders`
|
|
213
|
+
to pull the cohort's live books; or project `{wallet_address, coin}` pairs into
|
|
214
|
+
`open_positions_realized_pnl_fetch.positions`). Prefer `leaderboard_top_fetch` for CURRENT-
|
|
215
|
+
momentum cohorts (transient 4h delta-PnL) and `top_traders_fetch` for CONSISTENT-performer
|
|
216
|
+
cohorts (multi-window track record) — the two are deliberately different reads.
|
|
217
|
+
- **`kind: pure_fn` nodes** — how you write bespoke scoring/classification math the
|
|
218
|
+
registry vocabulary can't express (e.g. the cross-feature conviction waterfall in
|
|
219
|
+
`composer/examples/conviction_scorer.graph.yaml`, corpus_origin: bison). Rules, statically
|
|
220
|
+
enforced before anything runs:
|
|
221
|
+
- declare typed `inputs:` and `outputs:` (and optional `params:`) — no merge record, each
|
|
222
|
+
input is a single wired writer;
|
|
223
|
+
- the `fn:` block is **exactly one** synchronous top-level `def`, whose signature is
|
|
224
|
+
exactly the declared inputs then params, in order;
|
|
225
|
+
- only `math` and `statistics` may be imported; only a small safe-builtin set (numeric/
|
|
226
|
+
collection work) is allowed — no I/O, no `eval`/`exec`/`open`, no `ctx`/MCP access, no
|
|
227
|
+
dunder/reflection escapes, no `global`/`nonlocal`;
|
|
228
|
+
- it must `return` exactly its declared outputs;
|
|
229
|
+
- include `tests:` (inline fixture cases: `inputs:` → `expect:`) — these run as part of
|
|
230
|
+
`composer check`'s `pure_fn_tests` stage.
|
|
231
|
+
- **The escape hatch (`mcp_read` + the `raw` port)** — the LAST RESORT for a read the curated
|
|
232
|
+
source vocabulary doesn't wrap (or doesn't expose the fields you need). `mcp_read` calls a
|
|
233
|
+
named MCP tool with `args` passed verbatim and emits the unwrapped payload as the `raw` type;
|
|
234
|
+
you MUST wire that payload into a `kind: pure_fn` node — the NORMALIZER — which reads it and
|
|
235
|
+
returns TYPED outputs (with inline tests). What it's for: closing a genuine gap without
|
|
236
|
+
waiting for a registry add. What it is NOT: a way to bypass typing (a `raw` wire may feed
|
|
237
|
+
nothing but a pure_fn — CMP039), a general-purpose tool caller (READ-ONLY: the `tool` must be
|
|
238
|
+
in the committed snapshot and must not be a mutation — CMP060/CMP061), or a substitute for a
|
|
239
|
+
first-class node. **Reach for the registry/answers path FIRST** (`composer catalog` /
|
|
240
|
+
`composer describe`); use the hatch only when nothing fits, and be explicit in the `edge:`
|
|
241
|
+
about WHAT you're fetching and WHY the curated path can't express it. Never invent a tool —
|
|
242
|
+
if the read doesn't exist, it can't be faked here. `composer check` ANNOTATES every hatch use
|
|
243
|
+
(and flags when the tool already has a first-class node) so the usage stays visible; recurring
|
|
244
|
+
hatch use of the same shape is the maintainer's signal to wrap it as a real source node.
|
|
245
|
+
|
|
246
|
+
**Teaching errors are actionable, not decorative.** Every error from `validate`/`check`
|
|
247
|
+
carries a stable code (`CMPxxx`), a location (node id / port / line), what's wrong in one
|
|
248
|
+
sentence, and a concrete fix hint — a name to use, a bound to respect, or a command to
|
|
249
|
+
run. When a hint names a `describe <node>` command, run it before guessing; when it names
|
|
250
|
+
the nearest valid node/param, use that name rather than inventing one. Fix the errors it
|
|
251
|
+
names, re-run `check`, repeat until GREEN — don't rewrite unrelated parts of the graph.
|
|
252
|
+
|
|
253
|
+
## 6. Worked micro-example
|
|
254
|
+
|
|
255
|
+
Idea: *"Long BTC and ETH when 4h price is above its 200-period average and 1h RSI is not
|
|
256
|
+
overbought; take the single best signal, moderate risk."*
|
|
257
|
+
|
|
258
|
+
`drafts/momo_micro.answers.yaml`:
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
archetype: trend_momentum
|
|
262
|
+
strategy: momo_micro
|
|
263
|
+
universe:
|
|
264
|
+
whitelist: ["BTC", "ETH"]
|
|
265
|
+
intervals: ["1h", "4h"]
|
|
266
|
+
max_positions: 1
|
|
267
|
+
ttl_seconds: 240
|
|
268
|
+
risk:
|
|
269
|
+
margin_pct: 5.0
|
|
270
|
+
leverage: 3.0
|
|
271
|
+
edge: >
|
|
272
|
+
Go long BTC/ETH when 4h price is above its 200-period SMA and 1h RSI is not overbought
|
|
273
|
+
(< 70) — trend confirmation with a momentum-exhaustion guard.
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Three commands:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
uvx --with pydantic --with jinja2 --with pyyaml --with jsonschema python -m composer.cli \
|
|
280
|
+
new momo_micro --archetype trend_momentum --answers drafts/momo_micro.answers.yaml -o drafts
|
|
281
|
+
|
|
282
|
+
uvx --with pydantic --with jinja2 --with pyyaml --with jsonschema python -m composer.cli \
|
|
283
|
+
check drafts/momo_micro.graph.yaml
|
|
284
|
+
|
|
285
|
+
uvx --with pydantic --with jinja2 --with pyyaml --with jsonschema python -m composer.cli \
|
|
286
|
+
deploy drafts/momo_micro.graph.yaml -o drafts/dist
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
After `new`, open `drafts/momo_micro.graph.yaml` and adjust the `TODO(edit)` gate/scorer
|
|
290
|
+
slot to actually express "above SMA-200 AND RSI < 70" (the generated default is a working
|
|
291
|
+
placeholder, not necessarily your exact edge) — the `sma` and `rsi` indicator nodes and a
|
|
292
|
+
`hard_gates_all` scorer (see `composer/examples/trend_momentum_majors.graph.yaml` for the
|
|
293
|
+
pattern) are
|
|
294
|
+
what you'd wire in. Re-run `check` after each edit until GREEN.
|
|
295
|
+
|
|
296
|
+
## 7. Honest boundaries
|
|
297
|
+
|
|
298
|
+
- **Gap reports, not silent failures.** If the answers (or an edit) reference a data
|
|
299
|
+
source or capability the registry doesn't have — named under `requires:`, or a
|
|
300
|
+
hand-authored node type that doesn't exist — `composer new`/`check` will not fabricate
|
|
301
|
+
it. `new` prints a structured gap report (what was asked for, what kind, why, and the
|
|
302
|
+
nearest registry alternatives) and writes nothing. That's the correct outcome for
|
|
303
|
+
something not yet buildable; the fix is adding the capability to the registry, not
|
|
304
|
+
working around it in the graph.
|
|
305
|
+
- **Stateful nodes carry PRIOR-tick data, and the cohort cache does NOT skip the fetch.**
|
|
306
|
+
Every stateful node is split READ (prologue) / WRITE (epilogue): its outputs are read
|
|
307
|
+
BEFORE the pipeline runs and reflect the LAST persisted tick, while its late input is
|
|
308
|
+
recorded AFTER. Two consequences to author around: (1) `snapshot_diff_state.growth` is the
|
|
309
|
+
delta between the latest STORED snapshot and the oldest in the window — one tick behind this
|
|
310
|
+
tick's fresh input, and empty until ≥2 buckets have accumulated (a short warmup, exactly like
|
|
311
|
+
whalehunter's `requireGrowing` blocking until ~day 2). (2) `cohort_cache_state` does NOT
|
|
312
|
+
rate-limit — sources fire every tick and the compiler emits the upstream fetch
|
|
313
|
+
unconditionally (there is no conditional-fetch machinery). What it buys is COHORT STABILITY:
|
|
314
|
+
the read half serves the last-known-good cohort (pinned for `ttl_hours`, smoothing a transient
|
|
315
|
+
fetch failure, surviving process restarts via `ctx.state`), and the write half refreshes only
|
|
316
|
+
when the TTL lapses, the `version` changes, or the cache was empty. Cold start serves an empty
|
|
317
|
+
cohort for one warmup tick. State is BOUNDED by design (dedup prune, `max_snapshots` cap, one
|
|
318
|
+
cached cohort) — an unbounded fleet-box state series is a real ops hazard.
|
|
319
|
+
- **Ambiguous ideas need answers first, not invented specifics.** If a brief is missing
|
|
320
|
+
information you can't responsibly guess (which assets, long/short/both, how much risk),
|
|
321
|
+
ask the user rather than pick something plausible-sounding. Pin down the answers file,
|
|
322
|
+
then generate.
|
|
323
|
+
- **Protection by default.** Every strategy the composer emits is a COMPLETE standalone
|
|
324
|
+
runtime with a DSL `exit:` section (the two-phase trailing stop / time-cuts) — protection
|
|
325
|
+
is ON unless you deliberately pass `unmanaged: true`. The runtime schema legally allows an
|
|
326
|
+
exit-less config, so the composer enforces protection itself: an exit-less unit with no
|
|
327
|
+
`unmanaged` opt-out is RED at `check` (CMP120), and opting out prints a loud warning. Pick a
|
|
328
|
+
preset by shape — `protected_standard` (balanced, default), `protected_tight` (cut failures
|
|
329
|
+
fast), `protected_wide` (let a runner breathe). Tune only the `# TODO(edit)` slots
|
|
330
|
+
(phase-2 tier ladder, `max_loss_pct`) in the emitted `runtime.yaml`.
|
|
331
|
+
- **`deploy` stages a COMPLETE unit; it does not install.** `composer deploy` is hard-gated on
|
|
332
|
+
a GREEN `check` (which now includes the `runtime_validate` stage — the real Zod + DSL oracle)
|
|
333
|
+
and stages a standalone strategy PACKAGE TREE, idempotent and side-effect-free:
|
|
334
|
+
`strategy.yaml` (catalog + funding manifest), `main/runtime.yaml` (the complete runtime:
|
|
335
|
+
strategy/scanners/actions/**exit**/risk), `main/scanners/scan.py`,
|
|
336
|
+
`main/scanners/strategy_primitives/` (a vendored SNAPSHOT of `composer/primitives/`, a sibling
|
|
337
|
+
package of `scan.py`), `main/<id>.graph.yaml`, and `manifest.json` (hashing every artifact
|
|
338
|
+
including each vendored primitives file, with the runtime's Zod verdict). The emitted `scan.py`
|
|
339
|
+
imports `strategy_primitives`, NEVER `composer.*`, so the unit is self-contained: it runs on a
|
|
340
|
+
box that has none of the composer checkout — the scaffold puts the scanner dir on `sys.path` and
|
|
341
|
+
the vendored package resolves as a sibling of `scan.py`. The snapshot freezes the primitives at
|
|
342
|
+
deploy time (later `composer/primitives/` changes never mutate an already-staged strategy). There
|
|
343
|
+
is no host `runtime.yaml` to borrow — the staged unit is complete on its own. Install is `senpi-strategy-ops`:
|
|
344
|
+
it reads `strategy.yaml.instances[]`, mints one funded wallet per instance, and renders each
|
|
345
|
+
`runtime.yaml` onto its wallet. The composer never creates wallets, moves money, or installs.
|
|
346
|
+
- **`install` puts ONE instance of a staged unit onto a box — without touching the frozen runtime.**
|
|
347
|
+
`composer install <staged-dir> --wallet 0x…` (or a graph file plus `-o <staging-dir>`) is the box-side
|
|
348
|
+
step deploy stops short of. It (a) PREFLIGHTS — resolves the staged tree, verifies every artifact's
|
|
349
|
+
bytes against `manifest.json` (CMP201, tamper/hand-edit detection), and requires the recorded oracle
|
|
350
|
+
verdict to be GREEN (`VALID`; a `SKIPPED` verdict on an oracle-less box is allowed, `INVALID` is CMP202);
|
|
351
|
+
(b) COPIES the unit to a CONTENT-ADDRESSED, IMMUTABLE dir `<senpi-state>/strategies/<strategy>/<hash8>/`
|
|
352
|
+
— written to a temp sibling, every file re-verified against the manifest (CMP203), then atomic-renamed
|
|
353
|
+
into place, and NEVER edited after (so the frozen inline-YAML in the registry can't drift from the
|
|
354
|
+
scan.py re-read off disk on a scaffold respawn). `hash8` = the first 8 hex of a sha256 over the manifest
|
|
355
|
+
`artifacts` map, so identical staged content always maps to the same dir and a re-install is an idempotent
|
|
356
|
+
no-op; (c) BINDS the real `--wallet` for the `${..._WALLET}` placeholder in the COPIED `runtime.yaml` only
|
|
357
|
+
(the staged source stays pristine), records the binding in `install.json`, and re-runs the placeholder
|
|
358
|
+
policy — any remaining bare `${VAR}` after binding is CMP115; (d) INVOKES the FROZEN consumer,
|
|
359
|
+
`openclaw senpi runtime create -p <hashdir>/main/runtime.yaml` (PATH mode — never `--content`), behind an
|
|
360
|
+
injectable seam; (e) maps the known installRuntime failure classes (duplicate id CMP210,
|
|
361
|
+
one-runtime-per-wallet CMP211, wallet not ACTIVE CMP212, invalid YAML CMP213, entrypoint missing CMP214)
|
|
362
|
+
to teaching errors that name the exact next command. The runtime id is `<strategy>-<hash8>` (content-keyed,
|
|
363
|
+
so a duplicate is meaningful). Every step's completion is derivable from disk (staged tree / hash dir +
|
|
364
|
+
`install.json` / the box registry entry) — the verb is resumable-by-artifact, with no hidden session state.
|
|
365
|
+
The wallet is supplied by `--wallet`, or auto-bound from the `composer fund` `wallet.json` when `--wallet`
|
|
366
|
+
is omitted (see `fund`, below); the update/reinstall (delete+create) flow is OUT of scope (the sanctioned
|
|
367
|
+
teardown path is `composer close`, below).
|
|
368
|
+
- **`fund` creates + funds the wallet — inside the flow, with an EXPLICIT budget.** `composer fund
|
|
369
|
+
<staged-dir> --budget <usd>` is the funding step the composer used to hand off conversationally. `--budget`
|
|
370
|
+
is REQUIRED and EXPLICIT (≥ $100): agree it with the user and pass the exact amount. It is NEVER derived
|
|
371
|
+
from a `funding_share` nor split across wallets — that legacy ops derivation misfired on a real box
|
|
372
|
+
(it bridged in pieces and told a user with sufficient balance to top up), so this verb funds ONE wallet
|
|
373
|
+
with the ONE number you pass. Steps, resumable-by-artifact: (a) if `<senpi-state>/strategies/<strategy>/wallet.json`
|
|
374
|
+
exists (STRATEGY-level, ABOVE the `<hash8>` dirs — the wallet is the strategy's instance identity across
|
|
375
|
+
content versions), verify it via MCP `strategy_list` — ACTIVE → `FUNDED` no-op, still initializing →
|
|
376
|
+
`PENDING`, closed/gone → CMP231 (stale); (b) otherwise PREFLIGHT the visible balance via
|
|
377
|
+
`account_get_portfolio` (free Hyperliquid + free EVM USDC, summed as the backend funds a create) — short
|
|
378
|
+
of budget → CMP232 stating the current visible total, the exact shortfall, the bridged-funds VISIBILITY
|
|
379
|
+
LAG (funds moved HL→EVM can lag ~a minute; wait + re-run), and the top-up options (the composer NEVER
|
|
380
|
+
bridges); (c) sufficient → call MCP `strategy_create_custom_strategy` (`skillName` = the strategy name,
|
|
381
|
+
matching what `composer close`'s legacy discovery reads), record `wallet.json` BEFORE polling, then poll
|
|
382
|
+
briefly to ACTIVE. `strategy_create_custom_strategy` is a MONEY MUTATION: there is no `--yes` (the CLI is
|
|
383
|
+
agent-driven — the user's confirmation is conversational per the skill), but the verb prints an
|
|
384
|
+
unmistakable pre-action line (creating wallet, budget, funding source) in its result. Missing MCP creds
|
|
385
|
+
is a loud CMP223. Then `composer install` (omit `--wallet`) auto-binds the funded wallet.
|
|
386
|
+
- **`close` is the ONLY sanctioned teardown — never a raw `strategy_close`.** The invariant is
|
|
387
|
+
non-negotiable: STOP the runtime first (`openclaw senpi runtime delete <id>`), CONFIRM it is gone,
|
|
388
|
+
and ONLY THEN submit MCP `strategy_close(<strategyId>)` (which flattens ALL positions and returns
|
|
389
|
+
funds). A raw `strategy_close` with a live runtime STRANDS the runtime — it keeps trading a wallet the
|
|
390
|
+
server is tearing down. `composer close <target>` enforces this behind injectable seams: if the runtime
|
|
391
|
+
delete fails (CMP220) or the runtime is still present afterward (CMP221), `strategy_close` is WITHHELD
|
|
392
|
+
and the verb fails loud rather than strand or double-fire. It is IDEMPOTENT and SUBMIT-ONLY (no poll):
|
|
393
|
+
`strategy_close` is async, so the verb reports `CLOSING` and re-running it IS the poll — a re-run skips
|
|
394
|
+
the delete (runtime already gone) and skips the re-submit (strategy left the ACTIVE set), reporting
|
|
395
|
+
`CLOSED` once the strategy is terminal. It resolves the target from the composer's own on-box artifacts
|
|
396
|
+
first (a canonical hash dir's `install.json`, or a strategy name / runtime id under `--state-dir`), and
|
|
397
|
+
falls back to `strategy_list` attribution (skillName / strategyId + the wallet read from the strategy
|
|
398
|
+
record) so it also closes legacy ops-deployed strategies and orphan runtimes. Missing MCP creds
|
|
399
|
+
(`SENPI_API_KEY`) is a loud CMP223, not a silent skip.
|
|
400
|
+
- **`status --state-dir` is the resumable-by-artifact install chain.** With a box named, `status` walks
|
|
401
|
+
the current staged content DOWN onto the box entirely from artifacts + one read-only `runtime list`:
|
|
402
|
+
landed (canonical hash dir + `install.json`) → registered (`installed_runtimes.json`) → running. Multiple
|
|
403
|
+
hash dirs for one strategy are distinguished — which hash is installed/running vs a merely-landed prior
|
|
404
|
+
version. Liveness is reported HONESTLY: `runtime list` proves only that the process is up, so a running
|
|
405
|
+
install is annotated that functional liveness (scan() `runCount>0`, a fresh `lastRunFinishedAt`,
|
|
406
|
+
`consecutiveErrorCount==0` — see `senpi-strategy-ops/references/liveness-verification.md`) is NOT checked
|
|
407
|
+
here; run `openclaw senpi state -r <id> --json` for that. "running" ≠ "operating".
|
|
408
|
+
|
|
409
|
+
## 8. Debugging generation deterministically
|
|
410
|
+
|
|
411
|
+
Generation (`new`/`check`/`deploy`) is fully offline-reproducible: no creds, no network, no
|
|
412
|
+
wall-clock — the same answers always produce the same bytes. So any "the composer generated the
|
|
413
|
+
wrong thing" report replays exactly. The procedure (proven on the first real dev-test incident):
|
|
414
|
+
|
|
415
|
+
1. Reconstruct the user's answers file from the conversation (the graph's `metadata` block IS the
|
|
416
|
+
answers when you have the generated graph — spec-is-state).
|
|
417
|
+
2. Drive the CLI into a scratch dir: `python -m composer.cli new --archetype <a> --answers <f> -o /tmp/repro`
|
|
418
|
+
(deps: pyyaml, pydantic, jinja2, jsonschema — a throwaway venv or `uvx` is enough).
|
|
419
|
+
3. Diff the emitted graph against what the user agreed to — the delta names the lost intent.
|
|
420
|
+
4. Read the archetype's `build()` in `composer/authoring/archetypes/` for where that intent was
|
|
421
|
+
dropped (a hardcoded literal, an unread answer field, a missing archetype route).
|
|
422
|
+
|
|
423
|
+
Classify before fixing: COVERAGE (no archetype+params expresses it) · EXPRESSIVENESS (the answers
|
|
424
|
+
surface can't say it) · ROUTING (wrong archetype chosen) · INSTRUCTION (the skill never told the
|
|
425
|
+
agent to pass it). Each has a different smallest fix — and only genuinely-missing SOURCE
|
|
426
|
+
capability (no data feed in the world) is a registry gap; everything else is composer-side.
|
|
427
|
+
|
|
428
|
+
**Maintainer note (source coverage).** The source vocabulary is kept EXHAUSTIVE over the scanner-
|
|
429
|
+
usable Senpi MCP read surface, and that claim is enforced continuously by the CI closure check
|
|
430
|
+
(`composer/registry/mcp_closure.py`, offline by default). If a new server-side read tool appears,
|
|
431
|
+
CI goes red until it is either mapped by a new source node (declaring `mcp_tool="..."`) or added to
|
|
432
|
+
the allowlist with a reason. To pick up a catalog change, run
|
|
433
|
+
`MCP_SNAPSHOT_REFRESH=1 pytest python-tests/composer/test_mcp_closure.py` (fetches the live
|
|
434
|
+
unauthenticated `tools/list`, rewrites `mcp-tools-snapshot.json`, and surfaces input-schema drift),
|
|
435
|
+
then commit the refreshed snapshot. Full design in `docs/composer/design-models.md` §9.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""composer — the agent-facing whole for building trading strategies from typed graphs.
|
|
2
|
+
|
|
3
|
+
The composer turns a chat-stated idea into a running, verified strategy with no dead
|
|
4
|
+
ends: the LLM decides (interview, edge logic) and the machine composes, validates,
|
|
5
|
+
compiles, deploys, verifies, and issues every success claim. It is the union of a
|
|
6
|
+
registry-generated catalog, a teaching validator, a deterministic compiler, a primitive
|
|
7
|
+
library, archetype templates, and a status CLI — never a DSL (the exit-DSL legibility
|
|
8
|
+
lesson stands). Authored artifacts are typed dataflow DAGs (YAML) of nodes; the compiler
|
|
9
|
+
emits straight-line Python onto the existing external_scanner contract. This package is
|
|
10
|
+
importable to tests and to emitted scanners via the scaffold ``sys.path`` (no packaging
|
|
11
|
+
in the POC); the ``senpi_ta`` packaging/versioning question stays open, post-POC.
|
|
12
|
+
|
|
13
|
+
Subpackages:
|
|
14
|
+
registry — pydantic node registry; the single source of truth (D4/D8).
|
|
15
|
+
validator — schema + graph typecheck with located, fix-hinted errors.
|
|
16
|
+
compiler — deterministic graph -> scan.py materializer (D5/D6/D8).
|
|
17
|
+
primitives — runtime-owned Python implementations of primitive nodes.
|
|
18
|
+
templates — per-node emission templates + archetype templates-as-code (D7).
|
|
19
|
+
cli — the ``composer`` verbs (new/check/deploy/status/catalog/describe).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"registry",
|
|
24
|
+
"validator",
|
|
25
|
+
"compiler",
|
|
26
|
+
"primitives",
|
|
27
|
+
"templates",
|
|
28
|
+
"cli",
|
|
29
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""authoring — `composer new`, the machine-generated anchor (D7).
|
|
2
|
+
|
|
3
|
+
Typed interview answers in, a COMPLETE, VALID, COMPILING strategy graph out, with
|
|
4
|
+
mechanically-bindable values bound and bespoke slots TODO-marked WITH WORKING DEFAULTS so
|
|
5
|
+
the emitted graph validates and compiles before any edit (the anchoring principle: small
|
|
6
|
+
models edit a valid example far more reliably than they generate from a schema, and here the
|
|
7
|
+
anchor is machine-generated per-strategy rather than a copied file). The edge free-text is
|
|
8
|
+
carried verbatim as a comment above the scoring slot; the machine never interprets it.
|
|
9
|
+
|
|
10
|
+
Public API:
|
|
11
|
+
Answers / load_answers — the typed elicitation contract (file + flag overrides).
|
|
12
|
+
compose_new / NewResult — plan a `composer new` (gap check, build, no-clobber); no write.
|
|
13
|
+
detect_gaps / GapReport — feasibility at `new` time (D4 made mechanical).
|
|
14
|
+
ARCHETYPES — the CI-checked archetype templates (build(answers)->graph).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from .answers import (
|
|
20
|
+
Answers,
|
|
21
|
+
Indicator,
|
|
22
|
+
Risk,
|
|
23
|
+
Scoring,
|
|
24
|
+
Universe,
|
|
25
|
+
load_answers,
|
|
26
|
+
load_answers_file,
|
|
27
|
+
)
|
|
28
|
+
from .archetypes import ARCHETYPES, SKELETON
|
|
29
|
+
from .gaps import Gap, GapReport, detect_gaps
|
|
30
|
+
from .generate import NewResult, compose_new, render_graph
|
|
31
|
+
from .presets import DEFAULT_PRESET, EXIT_PRESETS, ExitPreset, preset_names, resolve_guard_rails
|
|
32
|
+
from .runtime_emit import answers_from_metadata, render_runtime_yaml
|
|
33
|
+
from .strategy_manifest import render_strategy_yaml
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Answers",
|
|
37
|
+
"Indicator",
|
|
38
|
+
"Risk",
|
|
39
|
+
"Scoring",
|
|
40
|
+
"Universe",
|
|
41
|
+
"load_answers",
|
|
42
|
+
"load_answers_file",
|
|
43
|
+
"ARCHETYPES",
|
|
44
|
+
"SKELETON",
|
|
45
|
+
"Gap",
|
|
46
|
+
"GapReport",
|
|
47
|
+
"detect_gaps",
|
|
48
|
+
"NewResult",
|
|
49
|
+
"compose_new",
|
|
50
|
+
"render_graph",
|
|
51
|
+
"EXIT_PRESETS",
|
|
52
|
+
"ExitPreset",
|
|
53
|
+
"DEFAULT_PRESET",
|
|
54
|
+
"preset_names",
|
|
55
|
+
"resolve_guard_rails",
|
|
56
|
+
"render_runtime_yaml",
|
|
57
|
+
"answers_from_metadata",
|
|
58
|
+
"render_strategy_yaml",
|
|
59
|
+
]
|