Skip to content

Commit 97cf501

Browse files
authored
Merge pull request #57 from NyxFoundation/feat/scenario-evaluation-and-agent-contract
Scenario-based evaluation (ADR 0017) and LLM-rewritten strategies (ADR 0018)
2 parents 6fb90ca + 8a33f64 commit 97cf501

86 files changed

Lines changed: 5097 additions & 2064 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-backtest.yml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,39 @@ jobs:
8787
npm run gen:local-constants
8888
npm run gen:state-dump
8989
90+
# A scenario is (regime, seed) and the regime YAML carries no seed, so --seed is required
91+
# (ADR 0017 §1). --blocks shortens the run for CI; the regime's own 360 would take 12 minutes.
9092
- name: Backtest against the dump
91-
run: npm run backtest -- --regime calm-01 --blocks 12
93+
run: npm run backtest -- --regime calm --seed 101 --blocks 12 --seconds 90
94+
95+
# The matrix path is what the competition actually runs, and it is a different code path from
96+
# a single --regime run: it writes matrix.json / standings.json and has to survive a scenario
97+
# that fails without abandoning the rest. Two scenarios is enough to exercise the loop.
98+
- name: Replay a small scenario matrix
99+
run: |
100+
cat > "$RUNNER_TEMP/ci-scenarios.yaml" <<'YAML'
101+
regimes: [calm]
102+
seeds: [101, 202]
103+
YAML
104+
npm run backtest -- --scenarios "$RUNNER_TEMP/ci-scenarios.yaml" --blocks 12 --seconds 90
105+
node -e '
106+
const { readdirSync, readFileSync } = require("node:fs");
107+
const dir = readdirSync("runs").filter((d) => d.startsWith("matrix-")).sort().at(-1);
108+
if (!dir) throw new Error("no matrix directory was produced");
109+
const m = JSON.parse(readFileSync(`runs/${dir}/matrix.json`, "utf8"));
110+
const s = JSON.parse(readFileSync(`runs/${dir}/standings.json`, "utf8"));
111+
const failed = m.scenarios.filter((x) => !x.agents);
112+
if (failed.length) throw new Error(`scenarios produced no result: ${JSON.stringify(failed)}`);
113+
if (m.scenarios.length !== 2) throw new Error(`expected 2 scenarios, got ${m.scenarios.length}`);
114+
if (!s.agents?.length) throw new Error("standings ranked nobody");
115+
// Both metrics must survive into the matrix, since the scoring rule is expected to
116+
// change and matrix.json is what makes a finished run re-scorable (ADR 0017 §4).
117+
for (const sc of m.scenarios)
118+
for (const a of sc.agents)
119+
if (!("netPnlUsdc" in a) || !("alphaUsdc" in a))
120+
throw new Error(`${sc.regime}#${sc.seed} ${a.id} is missing a metric`);
121+
console.log(`matrix ok: ${m.scenarios.length} scenarios, ${s.agents.length} ranked`);
122+
'
92123
93124
# The backtest exits 0 even when the scorer quietly read nothing, which is the failure mode
94125
# this whole job exists to catch, so assert on the run's own output.

CLAUDE.md

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,27 @@ deployer/ venue デプロイ(自己完結サブパッケージ。workspace
2323
|------|------|--------|
2424
| `agent.ts``decide(obs, ctx)` export) | ルール戦略 | runtime/bot.ts が read→decide→send のループで駆動(`export const config = { intervalMs }` で間隔指定可) |
2525
| `agent.ts``run(ctx)` export) | 自走型 | bot.ts はループせず ctx(clients/observe/submit/log)を渡して委譲(例 liquidator) |
26-
| `prompt.md`(frontmatter: name/description 必須) | プロンプト型 | bot.ts が observation を添えて毎判断 LLM に action を出させる(例 my-arb) |
27-
28-
`runtime/`(汎用スクリプト: bot/read/send/llm/prompt/deploy/agentLog)と `lib/`(共有戦略ヘルパ)は予約名。
29-
同梱の全 agent は agent.ts と併置で **prompt.md も持ち、両方の動かし方を提供する**(runtime の既定は
30-
agent.ts 優先 = ADR 0015 §2。ただし **雛形 `config/example.yaml` のロスターは取引 agent を prompt モードで
31-
出荷**=Quick Start 既定は LLM 駆動。要 OLLAMA_API_KEY in .env.local、LLM 判断 ~10s/回なので run 長は
32-
100 blocks/300s 目安。noop はルールのまま。API キー無しでも `model: codex[:<m>]` / `claude-cli[:<m>]`
33-
**Codex/Claude Code サブスク CLI 実行**が可能 = docs/guide/llm-agents.md)。ロスターの `env` で切り替え:
34-
35-
- `ERIS_AGENT_MODE: "prompt"` — agent.ts があっても prompt.md(毎判断 LLM)で動かす
36-
- `ERIS_PROMPT_REVISE_EVERY: "<N>"` — prompt モードで N 判断サイクルごとに LLM が prompt 本文を
37-
**自己改訂**する(既定 0=off。改訂版は `runs/<id>/agents/<agentId>.prompt.v<K>.md` に版付き保存され
38-
以後のサイクルで使用。`ERIS_PROMPT_REVISE_PERSIST: "1"` で agent ディレクトリの prompt.md にも書き戻し)
26+
| `agent.ts` + `improve.md`(frontmatter: name/description 必須) | **自己改善型**(ADR 0018) | decide を毎ブロック駆動しつつ、LLM が取引経路の****で戦略コードを書き換える |
27+
28+
`runtime/`(汎用スクリプト: bot/read/send/llm/improve/deploy/agentLog)と `lib/`(共有戦略ヘルパ)は予約名。
29+
30+
**プロンプト型(毎判断 LLM)は ADR 0018 で廃止**。実測で 1 判断 8〜28 ブロック・行動回数がルール型の
31+
1/64 で競技として成立しなかった(ADR 0017 §5 B1)。`ERIS_AGENT_MODE` / `ERIS_PROMPT_*` は fail-fast する。
32+
`improve.md` は prompt.md の改名ではない(前者は「いつ・何を根拠に・どう直すか」、後者は「この observation で
33+
どう動くか」)。ロスターの `env`:
34+
35+
- `ERIS_AGENT_FROZEN: "1"` — improve.md を無視して戦略を固定。**ADR 0018 §5 が要求する frozen 対照**
36+
(自己改善が効いたかを毎 run 見えるようにする)をディレクトリ複製なしで作る
37+
- `ERIS_LLM_MODEL: "<model>"` — 改訂呼び出しのバックエンド(improve.md の frontmatter が優先)。
38+
API キー無しでも `codex[:<m>]` / `claude-cli[:<m>]` でサブスク CLI 実行可 = docs/guide/llm-agents.md
39+
- `ERIS_IMPROVE_LOG_CALLS: "1"` — 改訂の生のやり取りを `agents/<id>.llm.jsonl` に残す(既定 off)
40+
41+
改訂は `{notes, executorTs}``{notes, revertTo: <version>}` を返し、`executorTs: null`
42+
「今の戦略を維持」。生成コードは **cheatcode 静的検査 → コンパイル → 2 秒の実行上限**を通ってから設置。
43+
**自動 rollback は無い**(閾値に妥当な値が無いため。旧実装は 18 run 中 0 件発火、逆に「少しでも負けたら」
44+
だと全員が負けるレジームで毎回巻き戻る)。戻すかどうかはモデルの判断で、版履歴を渡して `revertTo` で行う。
45+
LLM バックエンドが無くても run は完走し、改訂失敗が記録されて戦略は無改変で走り続ける。
3946
directShim / relay / stdin-stdout プロトコルは廃止済み(ERIS_AGENT_DIRECT_TX は退役)。
40-
プロンプト型の action 形式は sdk の zod スキーマ(`sdk/src/actionSchema.ts`)から `<schema>` を生成し、
41-
validate 失敗はエラー内容を会話に追記して再試行(上限超過は noop = fail-closed)。
4247

4348
## 設定(YAML 単一ソース。ADR 0013)
4449

@@ -72,7 +77,10 @@ agents:
7277
- `npm run build:contracts` — モックオラクル + PriceFeed を forge build(sim:realtime の前提。`out/` 未生成なら最低 1 回)
7378
- `npm run gen:local-constants` — deployments.json → `sdk/src/constants.local.ts` 生成(同梱 `deployer/` のローカルデプロイ出力を読む)
7479
- `npm run gen:state-dump` — 稼働中の deployer anvil から配布用 state dump + manifest(生成元コミット・deployments 同梱・fingerprint)を `backtest/state/` へ生成(ADR 0016。dump 前に `.local-snapshot` のクリーン断面へ revert し、constants.local.ts も同じ deployments から再生成)
75-
- `npm run backtest -- --regime <name>` — 参加者バックテスト(ADR 0016 Phase 0 = B1 実時間再生)。state dump をロードした専用 anvil(既定 port 8547)で `config/regimes/<name>.yaml`(+seed)を再生する。`--repeat N`(snapshot/revert 反復・run 毎に採点再構成)/ `--agents <roster>`(regime 既定ロスターの差し替え)/ `--protocols` 等の一回上書き。**override は実効 regime YAML に書き出されて agent プロセスにも伝播**(coordinator だけに効かせると agent が観測で死ぬ)。fingerprint 不一致は manifest 同梱 deployments から constants を自動再生成、genesis 不一致は fail-fast
80+
- `npm run backtest -- --regime <name> --seed <N>` — シナリオ 1 本を再生(ADR 0016 Phase 0 = B1 実時間再生)。state dump をロードした専用 anvil(既定 port 8547)で `config/regimes/<name>.yaml` + seed を再生する。**シナリオ = (regime, seed)** で regime YAML は seed を持たないので `--seed` は必須(ADR 0017 §1)。`--agents <roster>`(regime 既定ロスターの差し替え)/ `--protocols`/`--blocks`/`--score-every` 等の一回上書き。**override は実効 regime YAML に書き出されて agent プロセスにも伝播**(coordinator だけに効かせると agent が観測で死ぬ)。fingerprint 不一致は manifest 同梱 deployments から constants を自動再生成、genesis 不一致は fail-fast
81+
- `npm run backtest -- --scenarios config/scenarios/public.yaml` — シナリオ行列を 1 つの anvil 上で全部再生し順位を出す(ADR 0017)。`{regimes, seeds}` の直積で、シナリオ間は snapshot/revert。`runs/matrix-<id>/matrix.json`(シナリオ × agent の生スコア。**netPnlUsdc と alphaUsdc の両方**)と `standings.json`(レジーム内 z-score → レジーム等重み平均)を書く。順位は派生物で、採点方法は将来見直す前提(matrix.json から再計算できる)。`--metric netPnlUsdc|alphaUsdc` / `--repeat N`(較正の診断用。採点は 1 回が既定)
82+
- **公式レジーム**: `calm` / `cex-drift`(OU に drift、kappa 弱化)/ `informed-flow`(相関した方向性フロー)/ `whale`(単発大口の点イベント)/ `lending-incident`(暴落 + victim + 清算)/ `crash`(価格ギャップのみ。流動性引き抜きは issue #52 待ち)。`depeg` は採点方法の見直し待ち。`lst` は競技セット外
83+
- `--score-every N` は採点断面の間引き。成績は初期/最終断面しか使わない(`alphaByAgent = alphaLast − alphaFirst`)ので**スコアは不変**、equity curve が粗くなるだけ
7684
- `npm run typecheck` / `npm run test` — 型チェック / ユニットテスト
7785
- `npm run check:strategy` — 戦略コードの cheatcode 静的検査(入口ゲート)
7886
- `npm run check:boundaries` — workspace 依存方向(example → sdk ← core)の検査
@@ -180,7 +188,7 @@ OU の base price はそのまま進め、その上に **SEED 由来でランダ
180188
runtime/send.ts が同じファイルに mempool 活動(`kind:"mempool"`: submitted / submit_failed /
181189
rejected)を自己申告で追記する(coordinator が submitted を数えられなくなる穴を塞ぐ。ADR 0006 §5)。
182190
出力先は coordinator が渡す env `ERIS_RUN_DIR` / `ERIS_AGENT_ID` で決まる。run 後の診断はこれを一次情報にする。
183-
prompt 型は `ERIS_PROMPT_LOG_CALLS: "1"`(ロスターの env)で LLM との生の対話(system 全文・送信
191+
自己改善型は `ERIS_IMPROVE_LOG_CALLS: "1"`(ロスターの env)で LLM との生の対話(system 全文・送信
184192
messages・生応答・エラー)を `agents/<agentId>.llm.jsonl` に残せる(opt-in。プロンプト調整の一次情報)。
185193
186194
## spot EC2 で重い run を回す(ローカル逼迫の回避。spot skills)

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ flowchart LR
4545
- **Multi-agent competition** — agents run as fully independent processes, subscribe to blocks at their own pace, and sign and send directly themselves. In-block ordering is determined by anvil `--order fees` (descending priority fee).
4646
- **Controllable fair price** — the coordinator generates a SEED-derived deterministic fair price every block and writes it to the on-chain `PriceFeed` and mock oracles. Aave health factors and GMX mark prices follow it.
4747
- **Market stress & liquidation** — price spikes/crashes can be injected to trigger the Aave liquidation path.
48-
- **LLM-driven autonomous agents**a single `prompt.md` is the strategy itself. The LLM emits an action on every decision and can even self-revise the prompt (no hand-written trading logic).
48+
- **Self-improving agents**the strategy trades every block on its own, and an LLM periodically rewrites it in-run from its own track record. The LLM is never in the trade path.
4949
- **Fork-free local deploy mode** — avoids cold-state RPC round trips to the fork backend (fork RPC latency), and multi-asset (WETH/WBTC) works too.
5050
- **Backtesting** — with a distributed state dump plus official regimes (market scenarios), a strategy can be verified over and over under the same environment and the same scoring (`--repeat` to read the distribution).
5151

@@ -77,13 +77,13 @@ cd ..
7777

7878
### Choose an LLM backend
7979

80-
**The default roster is LLM-driven**: the trading agents run in prompt mode (`prompt.md`, one LLM call per decision), so they need an LLM backend to trade. Pick one — without it the run still completes, but the trading agents fail closed to `noop` and trade nothing:
80+
**The default roster is self-improving**: the trading agents are rule strategies that trade every block on their own, and an LLM periodically rewrites them ([Self-improving agents](docs/guide/llm-agents.md)). A backend is therefore optional — without one the run completes normally, the revisions are recorded as failed, and the strategies keep trading unchanged. Pick one to see the improvement loop actually work:
8181

8282
| backend | setup |
8383
|---|---|
8484
| **Ollama Cloud** (default; model `gpt-oss:120b`) | put `OLLAMA_API_KEY=...` in `.env.local` |
8585
| **Local ollama** (no key) | `ERIS_OLLAMA_BASE_URL=http://127.0.0.1:11434/api` in `.env.local`, and set a locally-pulled model via the roster env `ERIS_LLM_MODEL` |
86-
| **Claude Code / Codex subscription** (no API key; spawns the logged-in CLI) | in `config/local.yaml`, swap each prompt agent's `env:` for the commented variant with `ERIS_LLM_MODEL: "claude-cli:haiku"` (or `"codex"`) |
86+
| **Claude Code / Codex subscription** (no API key; spawns the logged-in CLI) | in `config/local.yaml`, add `ERIS_LLM_MODEL: "claude-cli:haiku"` (or `"codex"`) to the agent's `env:` |
8787

8888
To skip LLMs entirely and run the same strategies rule-based (`agent.ts`), remove the `env:` line from each agent in the roster. Details: [LLM Agents](docs/guide/llm-agents.md).
8989

@@ -119,8 +119,8 @@ Once you bake a state dump from a deployed anvil, you can **replay official regi
119119

120120
```bash
121121
npm run gen:state-dump # bake once from the running deployer anvil
122-
npm run backtest -- --regime calm-01 --repeat 5 # calm market, 5 times (prints mean alphaUsdc)
123-
npm run backtest -- --regime crash-01 # crash + Aave liquidation scenario
122+
npm run backtest -- --regime calm --seed 101 # one scenario (regime + seed)
123+
npm run backtest -- --scenarios config/scenarios/public.yaml # the whole public set + standings
124124
```
125125

126126
For details, see [Backtesting](docs/guide/backtest.md).
@@ -138,7 +138,7 @@ For details, see [Backtesting](docs/guide/backtest.md).
138138
| [Backtesting](docs/guide/backtest.md) | Replaying state dump + official regimes, iterating with `--repeat`, sparring, what is and isn't measurable |
139139
| [Run Output and Analysis](docs/guide/run-output.md) | The output files under `runs/<id>/` and how to analyze a run afterwards |
140140
| [Protocols and Actions](docs/guide/protocols-and-actions.md) | Reference: actions per venue, stablecoin accounting, oracle control |
141-
| [LLM-driven Autonomous Agents](docs/guide/llm-agents.md) | prompt.md-type agents (per-decision LLM, self-revision, conversation log) |
141+
| [Self-improving Agents](docs/guide/llm-agents.md) | agent.ts + improve.md (in-run strategy rewriting, sandbox, rollback, frozen control) |
142142

143143
**How the environment works / operations**:
144144

0 commit comments

Comments
 (0)