Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ agents:
- `npm run gen:state-dump` — 稼働中の deployer anvil から配布用 state dump + manifest(生成元コミット・deployments 同梱・fingerprint)を `backtest/state/` へ生成(ADR 0016。dump 前に `.local-snapshot` のクリーン断面へ revert し、constants.local.ts も同じ deployments から再生成)
- `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
- `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 回が既定)
- **公式レジーム**: `calm` / `cex-drift`(OU に drift、kappa 弱化)/ `informed-flow`(相関した方向性フロー)/ `whale`(単発大口の点イベント)/ `lending-incident`(暴落 + victim + 清算 + 同じ窓の引き抜き)/ `crash`(価格ギャップ + 同じ窓での引き抜き。3 venue が同時に薄くなる)`depeg`(レジストリの stable を外す方)は issue #27 待ち。`lst` / `liquity` は競技セット外(venue 単体検証用)
- **公式レジーム**: `calm` / `cex-drift`(OU に drift、kappa 弱化)/ `informed-flow`(相関した方向性フロー)/ `whale`(単発大口の点イベント)/ `lending-incident`(暴落 + victim + 清算 + 同じ窓の引き抜き)/ `crash`(価格ギャップ + 同じ窓での引き抜き。3 venue が同時に薄くなる)/ `depeg`(レジストリの stable が $1 でなくなる。issue #27。`lst` / `liquity` は競技セット外(venue 単体検証用)
- `--score-every N` は採点断面の間引き。成績は初期/最終断面しか使わない(`alphaByAgent = alphaLast − alphaFirst`)ので**スコアは不変**、equity curve が粗くなるだけ
- `npm run typecheck` / `npm run test` — 型チェック / ユニットテスト
- `npm run check:strategy` — 戦略コードの cheatcode 静的検査(入口ゲート)
Expand Down Expand Up @@ -183,11 +183,13 @@ ours なのは 2 つだけ(core は無改変):
(venue の初回 live run で全償還が `Unable to redeem any amount` で revert して判明)。helper は
`fetchPrice()` で価格を確定させた同一 tx 内でヒントを計算する。periphery であって core の改変ではない

- **eUSD は TOKENS レジストリに入れない**。stable として登録すると scorer の spot 掃引が $1 で評価してしまい、
デペグした CDP stablecoin に phantom value を与える(issue #39 が名指しで禁じている失敗)。
アダプタが**プールの約定価格**で評価する(mark = probe サイズの両側 mid / realizable = 自分サイズの
get_dy と、債務は get_dx で買い戻しコスト)。gas compensation 200 eUSD は借り手の負債ではないので差し引く。
ICR<100% の Trove は 0 で clamp(担保を捨てて歩き去れる = CDP の実際の性質)
- **eUSD は TOKENS レジストリに入れない**……**だったが issue #27 (b) で昇格した**。外していた理由は
「レジストリが stable を $1 で値付ける」だけで、それが消えたため。今は**市場価格 stable**(下の節)で、
価格の所有者は共通 probe = `sdk/src/stables.ts`。**spot の eUSD 残高は scorer の spot 掃引が値付け、
liquity アダプタは値付けない**(二重計上の回避)。アダプタに残るのは Trove と Stability Pool で、
realizable は自分サイズの get_dy、債務は get_dx で買い戻しコスト。gas compensation 200 eUSD は
借り手の負債ではないので差し引く。ICR<100% の Trove は 0 で clamp(担保を捨てて歩き去れる = CDP の
実際の性質)
- **担保は native ETH**(core が `msg.value` で受ける)。action 側は WETH wei 建てで、`buildTxs` が
`WETH.withdraw` を前置する。ただし**ガスと同じ残高**なので、全部突っ込むと閉じる tx すら送れなくなる。
observation に `ethBalanceWei` / `suggestedGasReserveWei` を出すが**強制はしない**(self-stranding は正当な負け)
Expand Down Expand Up @@ -227,6 +229,62 @@ ours なのは 2 つだけ(core は無改変):
両方積め」と書いているが、その理由(既定ロスターが prompt モード = LLM が毎判断する)は ADR 0018 で
消えている。今の prompt.md は改訂方針であって毎判断プロンプトではない

### 市場価格 stable(レジストリの stable を $1 断定でなく市場から値付ける。issue #27)

**「stable = $1」はコードがそう書いていたから**だった。`chain.ts` が active stable を全部足して
`usdcUnits` 1 本に潰し、`valuation.ts` が `kind === "stable"` を無条件に 1 と値付けていたので、
デペグした stable も par で採点されていた(#39 が eUSD をレジストリの**外**に置いて避けていた
phantom value そのもの)。issue #27 でこれを 3 段階で外した:

1. **観測に内訳を出す** — `obs.balances.stables[<symbol>] = {token, decimals, balance, priceUsdc,
marketQuoted}`。`marketQuoted: false` は「市場が答えなかったので par を仮置きした」で、
**`priceUsdc: 1` を「ペグが保たれている」と読んではいけない**
2. **`usdcUnits` を native USDC だけに narrow** — 9 箇所の参加者向け用途は全部**予算**であって評価では
ない(評価は `inventory.valueUsdc`)。合計値は予算として元々間違っていた(USDT は USDC プールで
使えないし、funding は stable ごとに同額を配るので実際に使える額の約 2 倍を表示していた)
3. **market から値付ける**(`sdk/src/stables.ts`)— **両側の executable probe の幾何平均**
`sqrt(sell × buy)`(片側だけだと売り側に張り付いて過小評価する。LST / Liquity と同じ規律)。
両側とも固定 notional なので**1 stage で済み**、採点断面の 1 multicall に相乗りできる。
quote が返らなければ **par に落として `par-fallback` で報告**(黙って par が最悪、黙って 0 は
「100% ディスカウント = 無限の裁定」に読めてもっと悪い)

- **USDC は numéraire で $1 固定**(issue #27 "Settled")。全 metric が USDC 建てなので、ここを
浮かせると過去 run の数字の意味が変わる。`marketPricedStables()` は USDC の leg を無視する
- **market を持つ stable は funding で配らない**(`fundWallet` は par stable にだけ配る)。cheatcode で
eUSD を湧かせるのは Trove が発行していない stablecoin を流通させることだし、これから割れる stable を
全員に配ると損が「誰も選んでいないポジションの β」になる。**買って初めて持てる**のがこの regime の要
- **α でも live mark**(base の fair と違い、peg の乖離は protocol が強制する価格に対する dislocation で、
それを閉じるのが venue の存在理由。固定参照で評価すると測りたいものが打ち消える)
- `STABLE_MARKET_LEGS`(`sdk/src/constants.ts`)が「どの stable がどのプールで値付くか」の単一ソース。
leg は `venue` を持ち、**その protocol が有効な run にだけ**その stable が入る(sweep されるが取引
できない stable は無い方がまし)。eUSD → `liquity` / DAI → `curve`
- **eUSD はレジストリに昇格**((b))。#39 が外していた理由(レジストリが stable を par で値付ける)は
消えたので、**価格の所有権を移した**(二重計上の回避 = `TokenKind: "lst"` と同じ論点)。
liquity アダプタは spot eUSD 残高を**もう値付けない**(scorer の spot sweep が値付ける)。Trove の
債務と Stability Pool 預入は venue のものとして残り、価格は `ctx.stablePrices()` から読む
- **DAI が 2 つ目の市場価格 stable**((c))。deployer の USDC/DAI stableswap-ng plain pool(100k/100k)を
使う。**A は 2000 → 100**(#39 と同じ較正: A=2000 だと半分売っても 4.4bps しか動かず、永久に
コストを超えない)。eUSD と違い**償還フロアが無い**ので、ディスカウントは「戻ると信じるかどうか」で
あって行使できる請求権ではない = 別のスキル
- **`stableSwap` action**(curve アダプタ所有。プールが Curve stableswap-ng だから)—
`{type, stable, tokenIn, amountIn, slippageBps?}`。無いとデペグは「見えるだけ」になる
(#39 が `liquitySwapEusd` を足したのと同じ理由)。**per-round 上限は USDC の 6 decimals 建てなので
18 decimals の stable では換算が要る**(実測でこれを忘れると sell が毎回 reject され、買いだけ通って
「閉じられないポジションの含み益」になる: 42 reject / 6 accept)
- **`depeg` ストレスイベント**(`stress.events`。`stable:` 必須)— 環境が窓の間だけその stable を
プールへ売り、閉じたら買い戻す。機構は `core/src/realtime/stableDepeg.ts` に共通化してあり、
`eusdDepeg` も同じ実装を通る(イベント名は #39 の `stress_eusd_depeg*` のまま。他の stable は
`stress_depeg*` + payload の `stable`)。**毎ブロック目標へ reconcile**(一撃だと dropped block で
取り残される)で、売却量はチェーンから読み直す(revert しても窓がずれない)
- Aave の aggregator にも伝播する(`sdk/src/protocols/oracles.ts`。3 経路すべて)。ただし
**今どの market-priced stable も Aave reserve ではない**ので現状は no-op で、listing した日に効く
- レジームは `config/regimes/depeg.yaml`(公式セット入り = ADR 0017 の 7 本目)、参照 agent は
`example/agents/peg-arb/`。実測(seed 701): 環境が depth の 59% を売って最大 89.5bps のディスカウント、
peg-arb +139.6 / peg-arb-eager +195.7 / noop 0。**買い手が反対側を取ったぶん、環境が買い戻すと
プールは stable 不足になって par を超える**(裁定側が解消できていれば −6bps 程度で収まるが、
解消できないと大きく行き過ぎる。上限バグで sell が全 reject された run では −143bps まで振れ、
「閉じられないポジションの含み益」が +439 と表示された)

実時間化(ADR 0005)の前提: **SEED(=regime) は市場条件のラベル**で価格パスは再現可能だが、tx タイミング/着順は非決定 → 同一 regime でも結果はぶれる。run 長は `ERIS_RUN_BLOCKS` 固定で揃える。run の比較が要るときは同一 config を複数回回してサンプルを貯め、`runs/<id>/summary.json` を集計する(旧 evaluate/gate は撤去済み)。

## アーキテクチャ(環境とエージェント実行の分離。ADR 0006 / ADR 0015)
Expand Down
87 changes: 87 additions & 0 deletions config/regimes/depeg.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# config/regimes/depeg.yaml — a registry stable that stops being a dollar (issue #27). ADR 0016 §2
#
# Run: npm run backtest -- --regime depeg --seed 701 [--agents <roster>]
#
# What this regime tests that the others do not: an asset whose price the scorer used to assert.
# Every stable in the registry was worth $1 because the code said so, which meant no strategy could
# be right or wrong about one. Since issue #27 a stable with a market is marked at what that market
# pays, in the wallet and in an LP leg alike -- so holding the wrong dollar costs something, and
# buying the discount is a position with a real downside rather than free money.
#
# It is deliberately not the liquity regime with a different token. eUSD's discount is bounded by a
# redemption a CDP will always honour, so the trade is a claim you can enforce. DAI here has no
# floor at all: the only reason to expect par to return is that the dislocation is a window rather
# than a repricing, and an agent still holding at the last block is marked at the pool, not at par.
# The skill being measured is the difference between those two situations.
#
# Funding stays USDC-only (ADR 0017 §4), and note that this now means it literally: fundWallet
# grants the endowment to par stables only, so nobody starts holding DAI. Every unit of exposure to
# the depeg is a unit somebody chose to buy.

run:
blocks: 120 # the depeg window plus enough afterwards for the peg to come back
seconds: 900 # a stress run ends by block count (ADR 0009)
blockTimeSec: 2 # fixed to the regime (ADR 0016 §2)
# curve is not optional: it owns the stableSwap action, which is the only way to trade the pair.
protocols: [uniswap, balancer, curve, aave]
economicGas: false
localDeploy: true # the DAI/USDC pool is one the deployer seeded; a fork has no such thing
reportDir: ./runs

funding:
wethWei: "0" # USDC-only: netPnlUsdc carries β, so nobody may start already exposed
usdcUnits: "25000000000"

limits:
agentWethWei: "1000000000000000000"
agentUsdcUnits: "5000000000"

flow:
uninformedMaxWethWei: "1000000000000000000"
informedMaxWethWei: "2000000000000000000"
balancerMaxWethWei: "1000000000000000000"
curveMaxWethWei: "1000000000000000000"
informedArbFeeBps: 30
uninformedArrivalRate: "0.9"
uninformedSizeSigma: "1.0"
# There is no DAI flow bot. The pair sits at par unless the event below pushes it or an agent
# trades it, which is the same arrangement the liquity regime uses and for the same reason: an
# emergent depeg would make the regime's character a function of who entered.

stress:
events:
# magnitude is the fraction of the pool's seeded DAI depth the environment has sold at the top
# of the trapezoid. The pool is 100k/100k at A=100, the same shape #39 calibrated the eUSD pool
# to, so the measured curve carries over: ~40% sold is ~114bps below par, 50% is ~175bps, 60%
# is ~282bps.
#
# The cost to clear is lower here than in the liquity regime -- there is no 50bps redemption
# fee, only the pool's 0.01% on each leg -- but so is the certainty, because nothing forces the
# price back except the window closing. The range spans "barely worth it" to "obviously worth
# it" on purpose, and the ramp passes through every level in between where waiting is right.
- {
type: depeg,
stable: DAI,
magnitudeRange: [0.35, 0.6],
windowFrac: [0.25, 0.6],
rampBlocks: 4,
holdBlocks: 12,
decayBlocks: 15,
}

agents:
- id: noop
wallet: AGENT1_PRIVATE_KEY
baseline: true
description: does nothing (baseline)
- id: peg-arb
wallet: AGENT2_PRIVATE_KEY
description: buys DAI below par and sells it back as the peg recovers
- id: peg-arb-eager
dir: peg-arb
wallet: AGENT3_PRIVATE_KEY
description: the same strategy that buys a shallower discount and holds for par
env: { ERIS_PEG_ARB_BUY_BPS: "10", ERIS_PEG_ARB_SELL_BPS: "0" }
- id: venue-arb
wallet: AGENT4_PRIVATE_KEY
description: WETH-only cross-venue arbitrage (keeps the AMM venues honest)
12 changes: 9 additions & 3 deletions config/scenarios/public.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@
#
# Run: npm run backtest -- --scenarios config/scenarios/public.yaml --agents <roster>

# Six of the seven regimes ADR 0017 lists. The seventh, depeg, waits on issue #39 (the CDP venue that
# brings the stable to break) and then #27 -- not on the scoring rework, since both keep USDC as the
# $1 quote unit.
# All seven regimes ADR 0017 lists. `depeg` was the last one outstanding: it needed a stable the
# scorer would actually mark down, which issue #27 delivered by pricing registry stables from their
# market instead of asserting $1. USDC stays the $1 quote unit, so the scoring rework (#56) was
# never a dependency.
# `crash` carries both halves (issue #52: the gap and the liquidity withdrawal, across uniswap,
# balancer and curve), and `lending-incident` thins its books on the crash window too. Measured at
# a 50% pull, the cost of taking 10 WETH roughly doubles on uniswap/balancer and quadruples on
# curve, while the price a small trade sees moves <=0.1bps.
#
# Note that adding depeg moves every standing: the z-score is taken within a regime and then
# averaged across them (ADR 0017 §3), so a seventh regime redistributes the weights. Comparisons
# against a matrix run before this one have to be re-run rather than read across.
regimes:
- calm
- cex-drift
- informed-flow
- whale
- lending-incident
- crash
- depeg

seeds: [101, 202, 303, 404, 505]
20 changes: 17 additions & 3 deletions core/src/coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,25 @@ import type { Address, Hex } from "viem";
import { accountAddress, getBalances } from "@eris/sdk/chain.js";
import type { ProtocolId, RawTxIntent, TxIntent } from "@eris/sdk/types.js";
import { baseTokens } from "@eris/sdk/markets.js";
import { enabledAdapters } from "@eris/sdk/protocols/registry.js";
import { enabledAdapters, getAdapter } from "@eris/sdk/protocols/registry.js";
import type { FlowKind, SimContext } from "@eris/sdk/protocols/types.js";
import { FlowProcess, type FlowOrderWire } from "./flowProcess.js";
import type { FlowContextWire } from "./flow/logic.js";
import { readAaveFlowReserves } from "@eris/sdk/protocols/aave.js";
import { stableBalanceOf, TOKENS } from "@eris/sdk/constants.js";

// The stable a venue actually trades against. usdcUnits used to be every stable summed, which was a
// serviceable stand-in for it; since issue #27 narrowed that field to native USDC, a flow bot on
// Balancer's USDC.e leg would have sized against a balance it was not spending.
function venueStableUnits(
protocol: ProtocolId,
balances: Parameters<typeof stableBalanceOf>[0],
): bigint {
return stableBalanceOf(
balances,
getAdapter(protocol).stableToken ?? TOKENS.USDC.address,
);
}

// ---------------------------------------------------------------------------
// observation / flow / submit
Expand Down Expand Up @@ -51,7 +65,7 @@ export async function buildFlowContext(
wethSupplied: r.wethSupplied.toString(),
usdcBorrowed: r.usdcBorrowed.toString(),
wethWei: b.wethWei.toString(),
usdcUnits: b.usdcUnits.toString(),
usdcUnits: venueStableUnits("aave", b).toString(),
});
}
}
Expand All @@ -62,7 +76,7 @@ export async function buildFlowContext(
const b = await getBalances(ctx.publicClient, wallet.address);
flowBalances[`${protocol}:${kind}`] = {
wethWei: b.wethWei.toString(),
usdcUnits: b.usdcUnits.toString(),
usdcUnits: venueStableUnits(protocol, b).toString(),
};
}
}
Expand Down
Loading
Loading