Skip to content

Measure every candidate scoring metric on stored runs, and record what they disagree about (#56) - #62

Open
adachi-440 wants to merge 32 commits into
mainfrom
feat/scoring-metric
Open

Measure every candidate scoring metric on stored runs, and record what they disagree about (#56)#62
adachi-440 wants to merge 32 commits into
mainfrom
feat/scoring-metric

Conversation

@adachi-440

Copy link
Copy Markdown
Contributor

What this is

ADR 0019 picked mean(x_e) − λ·std(x_e) as the competition score with λ = 0.25, both provisional. This branch builds the machinery to check that choice against data, runs the measurements, and writes down what they say.

It does not close #56. The metric family is settled by the evidence here; the λ / epoch-length pair is not, and the provisional z-score is still live in standings.ts. Refs #56.

What it delivers

Scoring machinerycore/src/scoring/epochScore.ts computes the ADR 0019 score from an epoch value series, metrics.ts computes every candidate (M1 / M4 / M7 / M9 / M13, plus M27 Borda across runs) from that same series, and npm run metrics -- <runDir...> rescores stored runs with no chain and no re-run. reconstruct.ts produces the series and medians the manipulable marks at each epoch boundary (G7). matrix.json now carries the two cross-sections the metrics are differences of, so a stored matrix stays rescorable after its run directories are gone.

Environment work the measurements neededcexDrift and flowTrend become injectable windows instead of whole-run config; persist lets a dislocation stay open; three regimes (b-harness, ruin-test, lucky-drift); a levered-long agent, because no existing strategy used aaveBorrow; and a fix for the Aave pool having only 100k USDC to lend, which was silently failing 99% of borrows as an arithmetic overflow.

The recorddocs/adr/0019-*.md and docs/scoring-metric-measurements.md.

What the measurements say

  • λ = 0.25 is too high. It puts the highest-earning strategy in the environment (week +4,000 on 163k, weekly Sharpe ~1.28) below an agent that does nothing. At a 12-block epoch, ~0.15 behaves as intended.
  • λ and the epoch length are one knob, not two. Effective strictness goes as λ / √(epoch length); λ = 0.25 at a 24-block epoch ranks the same as λ = 0.15 at 12. So λ cannot be chosen alone — this is the open decision.
  • M13 (Sharpe) cannot be the headline. Measured: an agent earning +26/week outranks one earning +4,890/week.
  • M9 earns its keep. A lucky leveraged week is real (M1 ranks it 2nd); M9 drops it to 9th. A blown-up leveraged position scores 13× worse than random.
  • The 1% bankruptcy floor is a NaN guard, not a ranking mechanism. 4.5× leverage into a 25% crash bottomed at 26% of the initial value — nowhere near it.
  • Two findings were retracted mid-way and are kept with their history: the per-round caps were blamed for the leverage ceiling (it was the Aave liquidity), and the funding mix was blamed for λ failing to separate agents (it was a missing benchmark subtraction).

Testing

test/epochScore.test.ts, metrics.test.ts, markMedian.test.ts, epochBoundaries.test.ts, events.test.ts, fundingGasBuffer.test.ts. The measurement runs themselves are in runs/ and reproducible with npm run metrics.

🤖 Generated with Claude Code

adachi-440 and others added 30 commits August 13, 2026 00:18
Proposes ADR 0019: score the live competition as a risk-adjusted log-growth
series over 4h epochs on a single continuous economy
(`mean_e(x_e) - lambda*std_e(x_e)`), retiring the field-relative z-score of
ADR 0017 §4. Records the benchmark choice (noop), the bankruptcy gates
(G1/G2), the gates deliberately not taken (G3/G4/G5/G6) and the funding fix
that "USDC-only" needs to actually be USDC-only.

Two points depart from what issue #56 recorded, both from review:

- G7 (TWAP of the manipulable marks) applies to *every* epoch boundary, not
  only the last one. Inflating an intermediate mark leaves the mean
  untouched but moves the variance by (2d/E)*[(x_k - x_k+1) + d], which is
  *negative* whenever d < x_k+1 - x_k: smoothing a bad epoch into the next
  one lowers std and raises the score. The manipulation also unwinds on the
  next block, so it is cheaper than the final-boundary one and repeatable 42
  times.
- The funding block needs a code change, not just YAML. `fundWallet` adds
  `GAS_BUFFER_WEI = 5 ETH` on top of `funding.ethWei`
  (sdk/src/chain.ts:528,543), so the proposed `ethWei: 1 ETH` actually lands
  6 ETH in the wallet: 15.2% of the portfolio in ETH beta, not the 2.9% the
  section is arguing for. Folding the buffer into `funding.ethWei` is now
  part of implementation step 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR 0019 §8 defined epoch boundaries in real time (4h), which is right for
the live chain but leaves the poc with nothing to run: an anvil run has no
simulated clock at all. The price path, the stress windows and the run
length are all counted in blocks, and `lst.simulatedSecondsPerBlock` is a
venue-local knob for the vault's yield accrual, not a run-wide clock. So the
harness gets a block count instead, and no global clock is introduced.

12 blocks/epoch (E=42 -> 504 blocks/week, ~17 min at blockTimeSec=2):

- leaves 3-5 blocks for G7's per-boundary median window. At 4 blocks/epoch
  (the 1 block = 1h reading) the window would equal the epoch, so its length
  could not be calibrated and no trading would fit inside an epoch.
- stays inside anvil's ~1,050 block history retention, so the existing
  post-run reconstruction still works. 24 blocks/epoch (1,008 blocks) pins
  the oldest cross-section against that limit and would require moving to
  online cross-sections first -- now recorded as an open item, since the
  live chain (#33) needs that move regardless.

Also corrects the gas risk: weekly gas consumption cannot be measured on a
compressed harness, because the action count follows the block count rather
than 168 hours. Budget it as per-action gas x allowed cadence x 168h.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#56)

`fundWallet` set every wallet's balance to `ethWei + wethWei +
GAS_BUFFER_WEI`, adding 5 ETH on top of whatever the roster asked for. The
official regimes set `funding.wethWei: "0"` and comment it "USDC-only
distribution: nobody starts already exposed", but leave `funding.ethWei`
unset, so agents actually started on 105 ETH -- 92.6% of initial value in
ETH beta (measured on depeg#701: 336,867 = 105 ETH + 25,000 USDC).

`netPnlUsdc` hid this by valuing the initial and final cross-sections at the
same final price, so the beta cancelled. ADR 0019's epoch series is
live-marked and does not cancel: every agent's std term would be dominated
by ETH volatility, and lambda would measure the market instead of the agent.
The endowment the ADR proposes (1 ETH + 100k USDC) would also have landed as
6 ETH = 15.2% of the portfolio rather than the 2.9% it argues for.

The buffer becomes a parameter. Agent wallets pass 0, which makes
`funding.ethWei` the balance the agent actually holds and makes the
ERIS_ECONOMIC_GAS lower-bound check (0.5 ETH, coordinator.ts) validate that
same number. Environment wallets keep the default: flow bots and the whale
run on `flowEthWei` (1000 ETH) where it is immaterial, and the admin USDC
top-up in protocols/aave.ts passes `ethWei=0` and pays for the grant out of
the buffer, so folding it there would have zeroed the admin balance
(anvil_setBalance overwrites rather than adds).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR 0019 step 3. The scorer reconstructed a per-block value curve but only
ever used its endpoints (alphaLast - alphaFirst), so nothing produced the
input the metric actually needs: the value at each epoch boundary, from
which x_e = ln(W_e / W_{e-1}) is formed.

`reconstructValueSeries` now samples those boundaries and returns them as
`epochSeries` (raw values per agent, aligned with the boundary blocks),
which rides into summary.json and events.jsonl with the rest of the
reconstruction meta. Raw values rather than returns or a score: lambda and
the epoch length are provisional, so the series has to stay recomputable
from stored data the way standings.json is derived from matrix.json.

- `run.epochBlocks` (ERIS_EPOCH_BLOCKS), default 12 = ADR 0019 §8's
  calibration epoch. 0 disables the series.
- Boundaries are read even when `scoreEvery` thins the curve: the thinned
  cross-sections are equity-curve resolution, the boundaries are the score.
  The reconstruction reads the union of the two.
- A trailing partial epoch is dropped. A short window yields a smaller log
  return by construction, which the metric would read as the agent slowing
  down.
- A missing boundary value stays `null` and warns, rather than defaulting to
  0 -- a gap in the series is not a bankrupt agent (G1/G2 land in step 4).
- The series is built from the ordinary live mark, not `alphaValueUsdc`
  (ADR 0019 §3: its beta removal is partial, so it would price the same bet
  differently depending on the instrument).

Unit tests cover the boundary math (43 marks for 42 epochs, equal widths,
partial-epoch drop, disabled/degenerate lengths). Not yet exercised on a
live run: that needs the deployer anvil.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

ADR 0019 step 2 (G7). A scoring cross-section reads its stable prices out of
a pool, so the holder can move them: buy into a thin pool in the boundary
block, be marked at the pushed price, unwind on the next one. Epoch
boundaries are now valued at the median of the blocks leading up to them
(`run.markMedianBlocks`, default 5 of a 12-block epoch), which forces a push
to be held for most of the window to count -- a spread-cost round trip
becomes a position.

Every boundary, not just the last one. Moving an intermediate mark leaves
the mean untouched (the next epoch cancels it) and changes only the
dispersion, and the change is *negative* whenever the push is in the
smoothing direction: ln W_k += d moves the variance by
(2d/E)*[(x_k - x_k+1) + d]. An agent whose epoch went badly can raise its
boundary mark, borrow the return from the next epoch, and lower its own std.

Scope: the market-priced stable seam. That is one continuity, not one venue
-- spot registry stables (#27) and the Liquity venue's Trove debt and
Stability Pool deposit both price off `ctx.stablePrices()`. The LST market
price and LP share reserves are named by the ADR too, but each builds its
price inside its own adapter's staged reads, so they stay live-marked for
now. `summary.json` reports `markMedian.surfaces` and the largest gap seen
between each stable's live probe and the median it was scored at, so a
partial G7 never reads as a complete one.

Only the boundaries are medianed: the cross-sections in between are the
equity curve, and smoothing those would hide real intra-epoch moves without
protecting any score. Blocks where the pool refused to quote are dropped
from the median rather than counted as par, which would drag the mark toward
$1 and erase the dislocation being measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 4: G1 (bankruptcy floor) -> G2 (scoring-side freeze) -> the score
itself, as a pure function over the stored series
(core/src/scoring/epochScore.ts). The coordinator applies it to the
reconstruction's epoch series and writes `epochScores` into summary.json
next to the series it was derived from, so lambda or the epoch length can be
changed and the run rescored without re-running it.

- G1 floors every mark at 1% of the starting value. Not merely ln(0)
  avoidance: Aave's valueUsdc is collateral - debt with no clamp, so a
  liquidated agent's total can be negative, where ln is NaN rather than
  -Infinity. Flooring also bounds the worst single-epoch return, which keeps
  one blowup from dominating the field's dispersion.
- G2 freezes the series at 0 from the epoch the floor was touched, including
  when the position recovers afterwards. It is a scoring rule and never
  blocks a tx: on a live chain a participant reaches the sequencer directly,
  so a chain-side stop would need sequencer-level power and would not
  transfer off anvil.
- A missing boundary carries the previous value forward at a return of 0 and
  is reported. Dropping it would shorten the series for exactly the agents
  the environment failed to read, and a shorter series has a smaller std.
- The denominator is uniform across the field rather than per-agent, so
  dying early cannot pay.

standings.ts (the practice harness's z-score) is untouched, per ADR 0019 §7:
how to align the practice harness is explicitly not decided yet.

Verified against a live depeg#701 backtest (the first run of the epoch
series and G7 on a chain): 9 epochs of 12 blocks, no gaps, and the median
moved the DAI mark by up to 54.9bps versus the boundary's live probe. Scored
offline from that summary.json, the metric ranks peg-arb-eager > peg-arb >
venue-arb > noop -- venue-arb outearns peg-arb on netPnlUsdc but pays for it
in dispersion, which is the metric doing what it was chosen to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two runs of depeg#701 (same seed, 120 blocks = 9 epochs), one on the current
distribution and one on the funding ADR 0019 §6 proposes.

Under 100 ETH + 25k USDC the four agents' std_e agree to within 1.1% -- they
are all just ETH's volatility -- so lambda does not separate anyone, and noop
scores -1.963e-3 rather than the 0 the metric is read against. Under 1 ETH +
100k USDC the spread is 199.9% and noop comes back to -6.18e-5, the residual
beta of the gas reserve that §4 predicted.

The same run also produced the first live example of lambda as a hurdle:
peg-arb earned +104 USDC and still scored -6.7e-6, because its mean/std of
0.234 sits just under lambda. And the 5%-of-endowment concern about
limits.agentUsdcUnits did not bite here -- neither run had a single rejected
submission, so the cap was not binding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implementing the remaining G7 surfaces turned out to be implementing
nothing. Both fall out, for different reasons, and the ADR now records why
rather than leaving a "partially applied" note that invites the work again.

LP shares are valued by composition -- reserves x the environment's fair
price, never the pool's own price -- and the agent's spot side is marked at
those same external prices. Pushing the pool moves value between the agent's
two buckets instead of creating any: what the trader loses the pool gains,
and the agent recovers only its own share of it. A wash when it owns the
whole pool, a loss otherwise.

The LST venue's scored mark is face value: `valueUsdc` is the vault's
redemption rate times the WETH fair and reads no pool at all. The pool quote
feeds `liquidatableValueUsdc`, which is reported next to the mark but never
summed into the value series (reconstruct.ts adds `value.valueUsdc`).

What makes the stables different is that there a pool quote *is* the mark of
a holding whose cost basis sits elsewhere, so moving the pool moves the
score. That seam is already covered.

The ValuationContext.medianReads plumbing written for this is reverted
unused; it is in the history if a venue ever marks a holding off a pool
quote again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md described scoring as taking `realizableWethWei` and dropping a
queue that cannot finalize inside the run. That was an earlier revision. The
value series sums `valueUsdc`, which the adapter sets to face value
(shareAssets + claimable + reachable + unreachable, times the WETH fair);
the realizable number goes to `liquidatableValueUsdc`, reported only for the
agents whose two marks disagree.

So a pending withdrawal that finalizes after the run is reported as
unrealizable and still counted at par in the score. That is the behaviour
issue #38 set out to avoid, and it is now also what ADR 0019 §3 asks for by
choosing the ordinary live mark. Recorded as an open item on the ADR rather
than resolved here: `lst` is outside the competition set, so nothing is
mis-scored today, and picking one of the two marks is a decision about what
the venue is supposed to measure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
matrix.json recorded netPnlUsdc and alphaUsdc, both of which are
differences, and pointed at a run directory for anything else. Of the 30
runs in the 2026-08-09 sweep, 5 had already lost their directory, so those
scenarios could not be rescored under a changed rule -- which is exactly
what standings.json being a derivative of matrix.json is supposed to allow
(ADR 0017 §4).

summary.json already carried initialValueUsdc and finalValueUsdc per agent;
they now ride into the matrix. Copied for disqualified agents too: the
disqualification is a ranking rule, and dropping what the agent actually did
would make that rule unauditable.

Note this does not retrofit the runs whose directories are gone. It stops
the next sweep from having the same hole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#56)

config/regimes/b-harness.yaml runs a week at the shape ADR 0019 scores: 42
epochs of 12 blocks, the §6 funding (1 ETH + 100k USDC, economic gas), and
three depeg windows instead of one, because a metric with a std term has to
be measured against a series that varies. The funding numbers live here and
nowhere else -- the seven official regimes keep 100 ETH + 25k, since they
are the practice harness and changing their funding would break comparison
with every stored matrix.

First run (b-harness#801, 504 blocks):

- The §4 KPI holds: noop comes last, and the ranking matches netPnlUsdc.
- lambda = 0.25 is a hard hurdle at this shape. One of four agents clears it
  (0.273 against 0.25), and two agents with positive netPnlUsdc (+63.57,
  +9.59) score negative.
- The noise floor is still the gas reserve's beta. noop's std_e is 2.214e-4
  against venue-arb's 2.213e-4 and peg-arb's 2.125e-4 -- effectively the
  same number. The 199.9% spread measured on the event-dense 120-block run
  falls to 38.6% over a week, because an agent moved in only 2-11 of the 41
  epochs while ETH moves in all of them.

So the metric works and the calibration does not yet: lambda, the event
density, the epoch length and the gas reserve all trade against each other,
and picking one needs several seeds at this shape rather than one.

Two incidental findings, both recorded on the ADR: `run.blocks: 504` yields
41 epochs rather than 42 (the scoring window is fromBlock..toBlock = 503
blocks), and weekly gas cannot be measured from the artifacts at all --
blocks.csv carries priorityFeeWei only, and no gasUsed or effective price is
recorded anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#56)

The single-seed reading ("lambda = 0.25 is a hard hurdle") does not survive
four more draws, and the conclusion moves.

Across seeds 801-805, mean/std per agent is peg-arb-eager 0.281 (sd 0.034),
peg-arb 0.111 (0.039), venue-arb 0.051 (0.039), noop 0.022 (0.020). The
ordering is stable -- the gap between the two peg-arb variants is about 4.4
seed-sds -- and lambda falls between them, so it is separating strategies
rather than noise. noop comes last in all five, which is §4's KPI.

What is miscalibrated is not lambda but what it is measuring. noop's std_e
(2.22e-4) is 93% of peg-arb's total (2.38e-4): the gas reserve's beta, which
shows up in all 41 epochs, swamps the agent's own risk, which shows up in
the 3-16 epochs where it traded. Taking the own-risk component as
sqrt(total^2 - noop^2) puts peg-arb at a Sharpe of 0.31 and eager at 0.375 --
peg-arb flips from never clearing lambda to clearing it. So the reserve's
beta has to come out before lambda is tuned against it.

Also recorded: venue-arb sent no transactions at all in three of the five
seeds (its scores are identical to noop's there), so the evidence rests on
two active strategies, not four.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

The metric catalogue defines M9's x_e as the excess log return over the
benchmark. ADR 0019 §1 wrote the formula without the benchmark term while §2
discussed its effect, and the implementation followed §1. That gap is what
the five-seed calibration had been measuring.

Scored raw, noop came out at -5.07e-5 with a std_e of 2.22e-4 -- 93% of an
active agent's dispersion. Every agent holds the same 1 ETH gas reserve, so
that number was ETH's volatility, not anyone's risk, and lambda was being
calibrated against it. Subtracting the benchmark epoch by epoch cancels it:
noop becomes exactly 0 (mean and std both), peg-arb's std_e falls from
2.38e-4 to 9.08e-5 and its Sharpe rises from 0.111 to 0.241, and the
ordering is unchanged. §4's claim that holding cash scores exactly zero is
only true in this form.

The benchmark is the roster's `baseline: true` entry rather than a synthetic
flat series: it has to carry the same gas reserve as everyone else for the
reserve to cancel. Without one the returns stay raw, `benchmarkApplied` says
so, and the run warns.

This also retires the funding argument in §6. Rescoring the two 120-block
controls as excess returns, lambda separates the field under the old
distribution too (100 ETH + 25k: 0.725 / 0.523 / 0.399 / 0.000), so "the
current distribution stops lambda discriminating" was a property of the
missing subtraction, not of the funding. The §6 distribution is kept for a
different reason, now stated: a benchmark that is 92% ETH makes "risk
against the benchmark" and "risk" different things, and shorting ETH would
be scored as a deviation rather than a position.

All five B harness seeds were rescored from their stored epoch series
without re-running anything, which is the first payoff of storing raw
boundary values rather than derived scores.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR 0017 §4 and ADR 0019 both promise that the scoring rule can be changed
and past runs rescored -- that is why matrix.json keeps raw scores and
summary.json keeps raw boundary values. Nothing had ever exercised it, so
the metric comparison the decision needs was being done in throwaway
one-liners. `npm run metrics -- <runDir...>` now reads the stored epoch
series and reports M1, M4, M7, M9, M13 and M27 side by side, with the
rankings each produces and where they disagree.

On the five B harness weeks it says something the single-metric view could
not: M9 at lambda = 0.25 is the only candidate that ranks doing nothing
above an active, profitable agent. M1, M4, M7 and M13 all put peg-arb (+92
to +165 USDC/week) above noop in every seed; M9 drops it below in three of
five, because its per-epoch Sharpe (0.174-0.300) straddles the hurdle. Borda
over the set: eager=5, peg-arb=11, venue-arb=17, noop=17 under every other
metric, against eager=5, noop=12, peg-arb=16 under M9.

Lowering lambda closes the gap exactly where the arithmetic says it should
-- at 0.20 noop falls to third, at 0.15 M9's Borda ordering matches M1's --
so this is a calibration of the hurdle, not evidence against the metric
family. Which of the two to move (lambda, or the environment's event
density, which ADR 0019 §4 says to reach for first) is still open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The numbers behind the metric decision were spread across ad-hoc scripts,
commit messages and a Notes section in ADR 0019 that had started to swell.
docs/scoring-metric-measurements.md now holds them: which runs were used,
the benchmark-subtraction finding, every candidate metric's ranking over the
five B harness weeks, the lambda sensitivity, the funding controls, and --
deliberately -- the things that cannot be measured yet.

The ADR keeps the conclusions that bear on the decision and points at the
record for the numbers. Two documents rather than one because they change on
different clocks: measurements grow with every run, the decision only when
it is actually revised, and duplicating the tables would leave one of them
quietly wrong.

Correcting one such staleness in the process: the ADR still said the §4 KPI
held in all five seeds ("noop comes last"), which was true of the raw-return
scoring it was written against. Under the excess returns the metric is
actually defined on, noop ranks above a profitable agent in three of five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…56)

The first version assumed the reader already knew what an epoch was, which
agents `peg-arb` and `venue-arb` are, what the catalogue's M-numbers mean,
and which ADR section each conclusion hangs off. Rewritten to stand on its
own: how the competition and the score work, what each candidate metric is,
what the four agents do, what the events in the test week are, and only then
the numbers -- which are unchanged.

Also promotes one line in the open questions from a footnote to its own row:
the test week contains depeg windows and nothing else, which is why
`venue-arb` never traded in three of the five seeds. Measuring lambda
against a single flavour of opportunity is a property of the harness, not of
the metric.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…56)

Two of the seven competition regimes were run-wide settings: a run either
had a drifting reference price, or uninformed flow that leaned together, or
it did not. That is expressible when a run is one scenario and meaningless
when a run is a week that has to contain several episodes on an undisclosed
schedule, so neither could enter the continuous economy at all.

They are now event types with the same trapezoid as everything else:

- `cexDrift` adds to the OU's per-block drift and weakens mean reversion by
  `kappaMultRange` while its window is open. A new event kind, `process`: an
  overlay multiplies the price the walk produced and leaves the walk itself
  untouched, which is right for a gap that heals, but it would let mean
  reversion erase a drift episode the moment the window closed. The seed
  picks the direction unless `side` fixes it.
- `flowTrend` multiplies the uninformed flow's size and holds a
  `trendCorrelation` / `persistBlocks` lean. The size fades with the
  trapezoid; the shape knobs do not, because half a correlation during the
  ramp is a different regime rather than a weaker one. Applied by the
  coordinator when it fills the flow bot's per-block wire, so the bot needs
  no knowledge of events and there is only one copy of the schedule.

Calibrated from the regimes they replace: drift 0.0015 with kappa 0.004
against the 0.02 default (`cex-drift.yaml`), and 3x size with persistence 12
and correlation 1.0 (`informed-flow.yaml`).

The b-harness week now carries both alongside its depeg windows. That is the
point: measured over five seeds, a week of nothing but depeg gave the
cross-venue arbitrageur no work at all -- zero transactions in three of them
-- so the scoring metric's lambda was being calibrated against a single kind
of skill. Verified on b-harness#801: two cexDrift episodes (the seed drew one
up and one down), one flowTrend, three depegs, all in their windows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reran the five B harness seeds with the two newly windowed regimes (a
drifting reference price, and order flow leaning across every venue) added
to the depeg windows. The ranking picture is unchanged: peg-arb still
straddles lambda = 0.25 and doing nothing still places second overall.

The cross-venue arbitrageur still sent zero transactions in three of the
five seeds, and its own log says why -- 503 rounds of "the widest gaps need
inventory this agent does not hold". Taking a cross-venue gap means selling
on the expensive side, which needs ETH. The roster is funded in USDC only
(1 ETH for gas), so the strategy can only take gaps that happen to point the
other way, and is silent whenever they do not. Not a shortage of
opportunity, and not a flaw in the agent: a constraint of the distribution.

That constraint is now obsolete. USDC-only funding existed because the old
metric charged everyone for the drift of whatever they were handed; the
benchmark subtraction cancels anything every entry holds equally, the
benchmark included. Funding the roster with ETH is therefore safe again, and
is the next thing to measure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The competition's regime table calls regime 5 a *non-mean-reverting*
deviation. The implementation was the opposite: every depeg window ended
with the environment buying the stable back, and the OU always pulls the
price to the anchor it started from, so nothing in the system could express
"the market repriced and stayed there".

That is not a cosmetic gap. With a guaranteed restore, "will par come back?"
has a known answer, so buying the discount is a free option bounded only by
size and the more patient of two otherwise identical strategies wins by
construction. It is a plausible explanation for peg-arb-eager -- which buys
a shallower discount and holds for par -- winning all five seeds by a factor
of three to five.

Two flags, both off by default:

- `persist: true` on depeg/eusdDepeg holds the level to the end of the run
  instead of decaying. Requires `decayBlocks: 0` rather than ignoring it, so
  a config cannot say "closes over 15 blocks" and mean the opposite. The
  teardown still buys back after the last competition block -- the startup
  check refuses a depegged pool, so leaving it would stop the next run --
  and that is outside every scored cross-section.
- `repriceAnchor: true` on cexDrift moves what the walk mean-reverts to, by
  exactly the drift the episode applied, compounded the same way the OU
  compounds its own step.

The B harness week now carries one of each, alongside the ones that close.
Its funding also gains 20 WETH: USDC-only was a rule the old metric needed
(it charged everyone for the drift of what they were handed) and the
benchmark subtraction has retired it, while the absence of inventory was
keeping the cross-venue arbitrageur out of three seeds in five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…th it (#56)

Third calibration pass, stopped after two of five seeds. Recorded because
both halves of what it found matter.

Funding the roster with 20 WETH took the cross-venue arbitrageur from 0-16
transactions to 274-296, and from 0-250 USDC to +3,405 and +4,146. It was
never short of opportunity; it was short of the inventory needed to sell on
the expensive side. It is also, once it can trade, the most profitable
strategy in this environment by a factor of ten to forty.

And that broke the harness's timing. Per-round work went from 60/90ms
(p50/p95) to 331/2128ms as anvil's execution queue backed up behind the
extra transactions, so 32 rounds overran the 2-second block interval and 43
blocks passed with no price update and no injected flow. The week stretched
from 504 chain blocks to 547-561, and the stalls land inside the event
windows -- the exact places the measurement is about. These numbers are not
comparable to the two earlier passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hain (#56)

`buildFlowContext` awaited each wallet in turn: 8 flow wallets, then 4 aave
actors at two reads each, ~16 sequential round trips per block. The client
batches contract reads into Multicall3, but only across calls issued in the
same await -- so the batching never applied to the loop that needed it most.

On an idle chain that cost 60ms per block and nobody noticed. Once the
roster was funded with WETH and the cross-venue arbitrageur started sending
~290 transactions per run, anvil's execution queue made each round trip ~5x
slower and the same 16 hops became 331ms (p95 2128ms) against a 2-second
block budget. 32 rounds overran it and 43 blocks passed with no price update
and no injected flow -- concentrated, of course, inside the event windows.

Issuing them together (b-harness#801, same seed and roster):

  stateFlow  p50/p95   331 / 2128 ms  ->  17 / 35 ms
  round      p50/p95   331 / 2128 ms  ->  23 / 44 ms
  blocks skipped              43      ->  0
  scored window        547 blocks     ->  503 (41 epochs, comparable again)

Faster than the pre-WETH baseline (60/90ms) as well, because the batching
now actually happens. The clock stays at 2 seconds; the earlier plan to slow
it to 4s was treating the symptom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cleanest pass yet -- five seeds, no skipped blocks, 41 epochs each, three
strategies active throughout -- and it settles the lambda question.

venue-arb earns +2,951 to +5,245 USDC a week, ten to forty times what the
peg strategies make, on roughly 163k of capital. Its dispersion is twenty
times theirs, so its per-epoch mean/std is 0.199 and it clears lambda = 0.25
in one seed out of five. M9 therefore ranks it last, below doing nothing,
while M1, M4 and M7 all rank it first.

Its weekly Sharpe is about 1.28 at a 2.5% weekly return. Calling that "below
the minimum needed to beat sitting in cash" is not defensible, so lambda =
0.25 is too high. At 0.15 the ordering keeps the earner on top and still
demotes the strategy that earns little for its dispersion, which is what the
risk term was chosen to do.

Also records the falsified hypothesis from the previous pass: making a depeg
permanent did not punish the patient buyer. A buyer purchases at the
discounted price and is marked at the discounted price, so a peg that never
returns is a missed profit rather than a loss. Punishing it needs the
dislocation to deepen after entry -- a long ramp -- not merely to persist.

(Restores the "not yet measurable" heading, which the previous doc commit
dropped while inserting a section above it.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… to nine (#56)

Four agents were measuring lambda against two kinds of skill, and two of the
four were the same code at different thresholds. A survey of all 21 agents in
example/agents found nothing that borrows: `lst-carry` is the only one, and
it needs a venue outside the competition set. So three of the metric's open
questions had no instrument at all.

`levered-long` is that instrument. It supplies its base as collateral,
borrows stables, buys more base, and lands on a target health factor. It is
deliberately a dumb long -- a probe rather than a contender -- because what
is being measured is the metric, not the strategy:

  - ADR 0019 §2 records "a lucky leveraged week is rewarded" as the known
    limitation of leaving beta in the mean. Nothing has ever taken leverage,
    so nobody has seen how large that is. With cexDrift now able to run in
    either direction, both the lucky and the unlucky week are reachable.
  - G1 (bankruptcy floor) and G2 (scoring freeze) have never fired -- every
    run reports bankruptAtEpoch: null. `levered-long-max` sits at a health
    factor of 1.15, where a downward drift can liquidate it.
  - G6 declines a leverage cap because "the protocols' collateral limits are
    enough". That has not been tried.

It sizes borrows to *land* on the target rather than to consume the headroom
Aave reports: the headroom is an LTV limit and the health factor is a
liquidation-threshold one, so spending the former overshoots the latter and
the position oscillates (the mistake the LST carry agent made). The
threshold is not in the observation, so it is inferred from the position:
measured 0.825 on the first run that opened one.

Also rostered three existing agents that were never used: `multi-arb` (an
independent cross-venue implementation -- it earns too, so venue-arb's
dominance is the opportunity rather than one author's code), `lp-provider`
(the only strategy that holds inventory and waits) and `random` (the lower
control: the field had shown lambda can demote a profitable strategy, never
that it demotes an unprofitable one).

Smoke-tested at 72 blocks: all nine act, both levered entries land on their
targets, and the coordinator stays at 18/44ms per round with no skipped
blocks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Widened the calibration roster from four to nine and reran three seeds. Two
of the metric's open questions now have answers, and one has a sharper
statement.

The catalogue rejects ratio metrics (M13) on the grounds that scale
invariance makes "stay small and safe" the optimum. That is now measured
rather than argued: `lp-provider` earns +26 USDC a week at a dispersion of
7e-6 and has the highest Sharpe in the field, so ranking by M13 puts the
agent that made $26 above the one that made $4,890. M9 keeps its absolute
scale and does not invert.

The lower control also holds: `random` loses 7,000-8,000 a week and comes
last under every metric, so lambda demotes an unprofitable strategy as well
as a profitable one -- the direction the field had never tested.

And the lambda problem is sharper than before. `multi-arb` (0.236) and
`venue-arb` (0.186) are independent implementations of the same strategy and
land on opposite sides of the 0.25 hurdle. The threshold is cutting through
a strategy class rather than separating classes.

The leveraged probes lost in all three seeds and were never liquidated, so
neither the "lucky leveraged week" that ADR 0019 §2 records as its known
limitation nor the bankruptcy gates G1/G2 have been exercised. Both need a
run with the drift direction and magnitude fixed rather than more random
seeds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built a run whose purpose was to bankrupt somebody -- five times leverage
against a 22-28% crash -- because G1 (the bankruptcy floor) and G2 (the
scoring freeze) have never executed. Nobody went bankrupt, and the two
reasons are both worth having.

The agent could not reach five times leverage. It got to a health factor of
1.10, about 1.45x, because the per-round caps (5,000 USDC borrowed, 5 WETH
supplied, 5,000 USDC swapped) make the loop that would take it further
longer than the week itself. So ADR 0019's G6 has its reasoning backwards:
it says the per-round limits cap size rather than leverage and the
protocols' collateral ratios do the real work, when in fact `limits.*` is
what binds. Under the current calibration a participant cannot take
dangerous leverage at all, which makes those caps a decision about how much
risk management the competition measures -- not, as the ADR files it, a
question of how many rounds it takes to deploy capital.

And an underwater position is not liquidated unless somebody calls
liquidationCall. The health factor reached 0.975 and nothing happened: no
liquidator in the roster, and the environment only liquidates the victims it
plants itself. The crash decayed, the position recovered to 1.523, and the
agent finished +768 ahead.

G1/G2 verification is also less urgent than it looked. With the
field-relative z-score retired, each participant's score comes from its own
series, so one blowup cannot poison anyone else's. The arithmetic is pinned
by unit tests; what remains untested on-chain is only whether the scorer
really reports a negative value for a wiped-out Aave position.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit carried the ADR correction but not this section: the
insert targeted the '10. not yet measurable' heading and replaced it, which
dropped the heading instead of writing above it -- the same slip as two
commits ago. Restored, with the run's findings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yesterday I recorded a correction to ADR 0019's G6: that the per-round
`limits.*` caps, not the protocols' collateral ratios, are what limits
leverage, on the evidence that an agent targeting five times leverage
stopped at 1.45x. That attribution was wrong and is retracted in both the
ADR and the measurement log.

The agent stopped because its borrows were reverting. Its own log says so --
19 submitted against 483 submit_failed, 480 of them `panic 0x11` -- and I
did not read it before concluding. The cause is Aave's USDC liquidity: under
local deploy the pool holds the 100,000 USDC that
`LOCAL_FLASH_LIQUIDITY_USDC_UNITS` supplies at startup, and two leveraged
agents plus four Aave flow actors exhaust it, after which every borrow fails
with an arithmetic panic that names nothing.

So G6 stands as originally written, and where the leverage ceiling actually
sits is simply unmeasured: nothing in this environment can borrow enough to
find out. That also voids the leveraged agents' results as evidence about
leverage -- their losses are mostly failed transactions -- and leaves ADR
0019 §2's "lucky leveraged week" and the G1/G2 bankruptcy gates still
untested. Fixing it is one constant, applied by cheatcode at startup, so no
state dump rebake.

The b-harness keeps its raised caps, but for the other reason they were
useful: the three peg-arb entries differ only in the fraction of balance
they trade, and at the practice regimes' 5,000 USDC cap the 5% and 10% twins
both clamp to the same number -- which is how the first attempt at that
comparison measured nothing. ruin-test goes back to the standard caps.

Unaffected by any of this: the size comparison itself (Sharpe 0.311 -> 0.188
-> 0.113 as one strategy's trade size goes 5% -> 10% -> capped), the Sharpe
metric's inversion, `random` as a lower bound, and lambda's behaviour. None
of them touch Aave.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rage (#56)

The hypothesis from the retraction was right: the borrows were failing
because Aave's USDC was gone. Raising the pool's startup liquidity from
100,000 to 2,000,000 -- one constant, supplied by cheatcode at run start, no
state dump rebake -- takes the leveraged agent from 19 submitted / 483
failed to 94 / 0, and it reaches its 1.05 health factor target (about 4.5x)
in 96 blocks where 504 were not enough before.

So this environment has been unable to support a borrowing strategy at all,
and said so only through an arithmetic panic that names nothing. Any
participant trying to lever would have seen their transactions fail for no
stated reason.

With that fixed, a 25% crash against 4.5x leverage finally produces a real
drawdown: value 163k -> 43k -> 121k, a net loss of 43,750 and the worst
score in the field at -6.02e-2, thirteen times worse than `random`. The
metric punishes a leveraged blowup exactly as designed -- not only for the
loss but for the dispersion, since a 74% fall followed by a 2.7x recovery is
two enormous epoch returns.

G1 and G2 still did not fire, and now we know why rather than guessing: the
bankruptcy floor sits at 1% of the starting value (1.6k of 163k) and the
worst trough the environment produced was 43k, or 26%. Reaching the floor
needs serial liquidation or a crash an order of magnitude larger. That
settles what G1 is for -- a guard against `ln` of a negative Aave position,
not a ranking device -- and it is the last of ADR 0019's rules to have gone
untested for a reason we can now state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR 0019 carries both lambda = 0.25 and a 4-hour epoch as provisional
numbers. Rescoring the stored per-block values at 6, 12, 24 and 48 blocks
per epoch -- no rerun needed -- shows they are not independent.

Sharpe scales with the square root of the epoch length (the mean grows
linearly with the period, the dispersion only as its root), so a fixed
lambda means something different at every epoch length. At 6 blocks the
agent earning +26 a week ranks first under lambda = 0.25; at 24 blocks the
one earning +4,890 does. Lengthening the epoch from 12 to 24 blocks achieves
what dropping lambda from 0.25 to 0.15 achieves.

Approximately, the hurdle's bite goes as lambda / sqrt(epoch length). So the
choice is one of the pair, not two numbers, and which to move is a separate
question: a longer epoch coarsens the measurement (10-20 observations in a
week) but leaves fewer epochs in which nothing happened, while a lower
lambda keeps the resolution and weakens the penalty on dispersion.

The 0.15 recommendation is therefore a recommendation about a pair, at this
harness's 12-block epoch. A live 4-hour epoch needs reading across by how
much trading a real epoch contains, not by the number itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two additions close the metric evaluation.

The last untested claim was ADR 0019 §2's own: that leaving drift in the
mean rewards a lucky leveraged week. Pinning both drift episodes upward
produced one. `levered-long` earned +2,122 and ranks 2nd of 11 under M1, M4
and M7 -- and 9th under M9, because its dispersion is 99 times that of the
small peg arbitrageur. So the "partial answer" the ADR credits lambda with
is, in this environment, close to a complete one. The same run also shows
leverage is not a direction bet you win by sizing up: at 4.5x the agent lost
13,117 in a week the drift favoured, because defending its health factor
means selling into every dip.

And §9.8 now states a verdict per metric with the measurement behind it,
rather than leaving eleven sections of evidence for a reader to assemble:
M9 conditional on the lambda/epoch pair, M4 and M1 defensible but blind to
the lucky gambler, M7 worthless here (identical to M1 at every rho tried),
M13 unusable (it crowns the agent that earned $26), M27 fine as an
aggregator.

What remains is not a measurement. §9.6 showed lambda and the epoch length
are one knob, so the decision is where to put the pair -- and that is a
judgement about how much dispersion the competition should tolerate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
adachi-440 and others added 2 commits August 16, 2026 02:44
The evidence sections were current; the pages a reader meets first were not.

§3 still described a four-agent roster funded with 1 ETH and 100k USDC, five
agents and 20 WETH ago, and described the test week as three depeg windows
when it now carries six windows of three kinds, two of which do not close.
It now lists all eleven agents with the section each was added for.

§4 listed four runs out of nine, so the numbers in §9.3 onwards could not be
traced to what produced them. It now lists every run with its roster size,
what changed, and which sections rest on it -- plus the warning that follows
from that: conditions differ between runs, so figures are comparable within
a run and not across them.

§10 still claimed the epoch length had never been varied, which §9.6 did.
§11 had grown to nine rows of which five were already resolved, including
one asking for agents that had since been added; the resolved ones move to a
single line so the open questions are legible again.

The measurement sections themselves are untouched. They are a record of what
was observed under the conditions of the time, including the readings later
retracted, and editing them to match today would destroy exactly what makes
the retractions useful.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The measurement log had grown into an experiment notebook and a report at
once, and read as neither. The verdict on the six candidate metrics sat at
line 517 of 572, so a first-time reader had to walk the whole time-series --
including the interpretations later retracted -- before learning which
metric was chosen.

Restructure around the reader: the verdict moves to §1, §2-§4 are the
premises, and §5 is the measurement log, explicitly marked as not needing a
read-through. Sections whose numbers are no longer usable now say so in the
heading (§5.5's leverage figures were invalidated by the Aave liquidity
defect).

The old §9.1-§9.8 were `##` headings -- siblings of §9 -- while numbered as
if they were its children. They are now §5.1-§5.7 at `###`, so the numbering
matches the hierarchy, and the parallel "結果 N" series is gone.

Merges, not deletions: §6+§7+§9 covered one question with one roster and
became §5.2; §9.4+§9.5 are one story (a broken measurement and its fix) and
became §5.5; the funding control experiment became a note in §4. Every
figure survives -- the only table dropped is a per-seed breakdown whose
content is stated in prose two lines away.

Fixes along the way:
- §11 credited the persistent-depeg finding to §9.4; it is from §9.2
- §9.8 cited itself
- the "その他" paragraph in §10 had no blank line before it, so it rendered
  as part of the G7 bullet
- the run table was missing the five-seed run behind §9.2 (it lives across
  matrix-2026-08-14T06-08 through 10-18)
- exponent notation had no key; §2 now gives one

571 -> 420 lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sim] Decide the competition scoring metric (excess log growth / mean-λ·std / Borda) and retire the provisional z-score

1 participant