Files
Adriano Dal Pastro 9612560479 research(xsec): sweep cross-sectional su Hyperliquid (43 script/257 config) + verifica avversariale
Nuova harness condivisa xslib.py (panel HL certificato, score per-asset causale, book
long-k/short-k vol-targeted leak-free) + 43 script in runs/ su 11 famiglie (MOM/REV/VOL/
DIST/LIQ/VAL/STRUCT/UNIV). Scoring = earns_slot (full>0 AND hold-out>0 AND marginal ADDS
al portafoglio live AND corr XS01<0.6, con jackknife drop-one-month).

Find: 42/257 config earns_slot=True, ma TUTTE con corr TP01 -0.2..-0.4 e PnL ~solo 2025.
Verify (verify_survivors.py, 3 scettici deterministici):
 - S1 redundancy: cluster low-vol = UNA scommessa (XV01=XU02=1.00, XV02/XV03 r 0.44-0.67);
   XM09/XL02/XS06b/XR02 distinti (corr media off-diag +0.20).
 - S2 short-beta: cluster low-vol carica 0.44-0.70 su short-market -> NON market-neutral,
   e' un tilt short-alt-beta di regime. XM09(0.08)/XR02(-0.21) NON short-beta.
 - S3 per-anno: cluster low-vol decade (XV01/XU02 2026 -0.09); XL02 morto (2025 -0.14,
   2026 -0.43); XM09 (0.82/0.50/0.74) e XR02 (0.84/0.40/2.68) positivi in tutti e 3 gli anni.

Esito: nessuna sleeve nuova. Cluster low-vol RIGETTATO (regime-bet), XL02 RIGETTATO (overfit).
2 LEAD genuini (XM09 trend-gated x-sec momentum, XR02 reversal vol-gated) -> forward-monitor,
non deployabili (panel 2.5y regime unico + STAT-MODE esecuzione). Portafoglio live invariato.

Incluso anche options_vrp_managed.py (A/B VRP01 hold-to-expiry vs gestione attiva del doc
credit-spread): la gestione attiva DISTRUGGE l'edge (combo FULL managed Sh -1.29 vs HtE +0.96,
il delta-exit taglia i vincenti) -> scartata, VRP01 resta hold-to-expiry.

Diari: 2026-06-20-xsec-strategies-sweep.md, 2026-06-20-vrp-active-management.md.
gitignore: data/paper_portfolio/ (stato runtime paper) + scripts/research/xsec/runs/out/ (output rigenerabile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:36:57 +00:00

238 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const meta = {
name: 'xsec-strategies-hyperliquid',
description: 'Search NEW cross-sectional / multi-asset strategies on the 51 certified Hyperliquid alts, distinct from XS01: honest backtest each, verify, score marginal vs the live TP01+XS01+VRP01 stack',
phases: [
{ title: 'Find', detail: 'one agent per cross-sectional mechanism via shared xslib' },
{ title: 'Verify', detail: '3 adversarial skeptics per promising finding (overfit/distinctness/short-history)' },
{ title: 'Synthesize', detail: 'rank survivors, marginal contribution to the live portfolio' },
],
}
// fam | id | name | kind hint | idea
const CATALOG = [
// --- MOM: cross-sectional momentum variants ---
['MOM','XM01','Single-L momentum sweep','Score = past_return(close,L); long top-k/short bottom-k. Grid L in {20,30,60,90,120}. Try universe "all","majors",top20. (Known: momentum on full 49-universe is NEGATIVE — confirm; majors is XS01 turf.)'],
['MOM','XM02','Multi-L z-blend momentum','Score = mean of xs_zscore(past_return(close,L)) over L in {30,90} (and try {20,60,120}). Like XS01 blend. Compare "all" vs "majors".'],
['MOM','XM03','Vol-scaled (risk-adj) momentum','Score = past_return(close,L) / roll_std(ret,L). Risk-adjusted momentum (Sharpe-like). Grid L in {30,60,90}.'],
['MOM','XM04','Residual / idiosyncratic momentum','Score = cumulative residual_return(ret, win) over last L (beta-removed momentum). Cleaner than raw momentum? win=60, L in {30,60}.'],
['MOM','XM05','Momentum acceleration','Score = past_return(close,L_short) - past_return(close,L_long) (is momentum accelerating). L_short=20,L_long=60.'],
['MOM','XM06','52-day-high proximity','Score = close / rolling_max(high,W) (closeness to recent high). W in {60,90}.'],
['MOM','XM07','Sharpe-rank momentum','Score = roll_mean(ret,L) / roll_std(ret,L). Rank by realized Sharpe. L in {30,60,90}.'],
['MOM','XM08','Momentum consistency (frog-in-pan)','Score = past_return(close,L) * fraction_of_up_days(ret,L) (smooth momentum beats jumpy). L=60.'],
['MOM','XM09','Market-trend-gated momentum','XS momentum but only ACTIVE when the equal-weight market (market_ret) is in an uptrend (trailing sum>0); else flat. L=60.'],
['MOM','XM10','Rank-weighted continuous momentum','Instead of top-k/bottom-k, weight ALL assets by demeaned xs_rank(past_return) (continuous book). Implement weights yourself via a fine score + large k≈A/2, or note xslib top-k is the proxy. L=60.'],
// --- REV: reversal ---
['REV','XR01','Short-term reversal','Score = -past_return(close,L) (long losers/short winners). Grid L in {1,3,5,7}. (Known smoke: REV5 negative — confirm/diagnose.)'],
['REV','XR02','Reversal gated by high-vol regime','Short-term reversal active only when market vol is high (panic) else flat. L=3.'],
['REV','XR03','Residual short-term reversal','Score = -(sum of residual_return over last L). Idiosyncratic reversal (beta-removed). L in {3,5}.'],
['REV','XR04','Volume-shock reversal','Long recent losers that ALSO had a volume spike (volume_z high): score = -past_return*(volume_z>1). L=3.'],
['REV','XR05','Overreaction reversal (mid-horizon)','Score = -past_return(close,L) for L in {20,30} (mean-reversion of multi-week moves).'],
// --- VOL/RISK anomalies (the frontier) ---
['VOL','XV01','Low realized-vol anomaly','Score = -roll_std(ret,W) (long low-vol / short high-vol alts). Grid W in {20,30,60}, universe all/majors/top20, long-short AND long-only. (Smoke: ADDS — verify hard.)'],
['VOL','XV02','Low idiosyncratic-vol anomaly','Score = -roll_std(residual_return(ret,60), 30) (low idio vol). Distinct from total vol?'],
['VOL','XV03','Low-beta anomaly (BAB)','Score = -roll_beta(ret,60) (long low-beta / short high-beta). Betting-against-beta.'],
['VOL','XV04','Low downside-vol / semivariance','Score = -roll_std(min(ret,0), W) (only downside dispersion). W=30.'],
['VOL','XV05','Low max-drawdown anomaly','Score = -rolling_maxdrawdown(close,W) (prefer smooth equity). W=60.'],
['VOL','XV06','Low vol-of-vol','Score = -roll_std(roll_std(ret,10), 30). Stability of volatility.'],
// --- DIST: distribution shape ---
['DIST','XD01','Low-skew / anti-lottery','Score = -roll_skew(ret,60) (short high-skew lottery alts, long low-skew). Lottery-preference premium.'],
['DIST','XD02','High-skew momentum (opposite)','Score = +roll_skew(ret,60). Test the OTHER sign (does positive skew pay in crypto?).'],
['DIST','XD03','Coskewness with market','Rank by rolling coskewness of asset returns with market; long low-coskew. win=60.'],
// --- LIQ: volume / liquidity ---
['LIQ','XL01','Amihud illiquidity premium','Score = mean(|ret| / (close*volume)) over W (illiquidity). Long illiquid? Test both signs. W=30.'],
['LIQ','XL02','Volume-trend momentum','Score = volume_z(vol,30) combined with positive return (rising-volume winners). '],
['LIQ','XL03','Low-turnover anomaly','Score = -roll_mean(close*volume, 30) (long low dollar-volume names). Test sign.'],
['LIQ','XL04','Dollar-volume momentum','Score = past_return of dollar-volume (assets gaining liquidity/attention). W=30.'],
// --- VAL: value / mean-reversion to anchor ---
['VAL','XVa1','Distance-from-MA value','Score = -(close/roll_mean(close,W) - 1) (long the ones furthest BELOW their MA = cheap). W in {60,100}.'],
['VAL','XVa2','Cross-sectional RSI reversal','Compute RSI(14) per asset (use al.rsi per column); score = -RSI (long oversold). '],
['VAL','XVa3','Price-to-high value','Score = -(close / rolling_max(close,W)) (long the most beaten-down vs their high). W=90.'],
// --- STRUCT: structure / combos / construction ---
['STRUCT','XS01b','Double-sort momentum × low-vol','Score = xs_zscore(past_return(close,60)) + xs_zscore(-roll_std(ret,30)). Combine momentum and low-vol.'],
['STRUCT','XS02b','Long-mom + short-rev multi-horizon','Score = xs_zscore(past_return(close,90)) + xs_zscore(-past_return(close,5)). Long-term winners that dipped short-term.'],
['STRUCT','XS03b','Beta-hedged momentum','XS momentum book but subtract market beta exposure (score=residual momentum; or note xslib book is already ~dollar-neutral). Compare net vs market-hedged.'],
['STRUCT','XS04b','Ensemble z-vote','Score = mean of xs_zscore over {momentum90, -vol30, -skew60, -beta60}. Diversified cross-sectional signal.'],
['STRUCT','XS05b','Risk-parity legs (inverse-vol)','Momentum selection but weight legs by inverse own-vol (approximate via score = past_return and rely on xslib; document the limitation). L=60.'],
['STRUCT','XS06b','Correlation-to-market diversifier','Score = -rolling_corr(asset_ret, market_ret, 60) (long alts least correlated to the pack). win=60.'],
['STRUCT','XS07b','Trend-quality (R^2) ranking','Score = R^2 of a linear fit of log price over last W (smooth trenders). Long high-R2-up. W=60.'],
['STRUCT','XS08b','Lead-lag vs BTC','Score = past_return(close,L) of alts conditional on BTC having risen (alts that lag BTC catch up). L=10.'],
// --- UNIV: universe / rebalance sensitivity (same core signal, vary the frame) ---
['UNIV','XU01','Momentum universe sweep','Best momentum z-blend, run on universe in {majors, top20, top30, all}. Where does x-sec momentum live? (Maps the small-cap dilution.)'],
['UNIV','XU02','Rebalance/holding sweep','Low-vol or momentum with H in {5,10,20,30} and k in {3,5,8}. Turnover vs signal decay.'],
['UNIV','XU03','Long-only top-k (alt selection)','Low-vol / momentum LONG-ONLY top-k (captures alt-beta + selection, executable at small capital unlike the 38-leg book). Note: NOT market-neutral.'],
['UNIV','XU04','Liquidity-filtered momentum','Momentum but only on the top-20 by median dollar-volume (avoid illiquid noise). Compare to "all".'],
]
const ROOT = '/opt/docker/PythagorasGoal'
const CHEAT = `SHARED LIB (built & validated): ${ROOT}/scripts/research/xsec/xslib.py
Top of your script: import sys; sys.path.insert(0, "${ROOT}/scripts/research/xsec"); import xslib as xs; import numpy as np
PANEL: xs.load_panel(universe) -> Panel(.syms, .index, .close, .open, .high, .low, .vol, .ret) all numpy (n_days x n_assets).
universe: "all" (49 alts, >=700d), "majors" (19 XS01 majors), a list of syms, or an int N (top-N by $-volume).
Certified Hyperliquid 1d, 2024-2026 (~900 days). DVOL not here (that's BTC/ETH only).
CAUSAL HELPERS (value at row i uses data <= i): xs.past_return(close,L), xs.roll_std/roll_mean/roll_skew/ewm_mean(mat,win),
xs.xs_zscore(mat) (cross-sectional z per row), xs.xs_rank(mat), xs.market_ret(ret), xs.roll_beta(ret,win),
xs.residual_return(ret,win) (idiosyncratic), xs.volume_z(vol,win). For per-asset TA (RSI etc.) loop columns with altlib (sys.path has it: import altlib as al; al.rsi(col)).
BACKTEST/EVAL (no look-ahead: weight at bar i earns return of bar i+1 — built in):
xs.study_xs("NAME", lambda P: score_matrix(P), universe="all", H=10, k=5, long_short=True, target_vol=0.20)
score_matrix(P) -> np.ndarray (n_days x n_assets), HIGHER = long. Ranked cross-sectionally each H days;
long top-k / short bottom-k (long_short) or long-only top-k. Vol-targeted, fee 0.10% RT on turnover.
Returns {name,universe,H,k,long_short,n_assets,n_days, full:{sharpe,maxdd,ret,cagr}, holdout:{sharpe,...} (2025+),
yearly, corr_tp01, corr_xs01, corr_active, marginal:{verdict(ADDS/REDUNDANT/DILUTES/NEUTRAL),corr,
holdout_uplift_w20, jackknife_min_uplift, robust_oos}, earns_slot}.
PRINT xs.fmt(rep) and print("JSON:", xs.as_json(rep)).
THE BAR: a finding matters only if it is (1) positive FULL & hold-out 2025+, (2) DISTINCT from XS01 (corr_xs01 < 0.6 —
else it is just XS01), (3) marginal verdict ADDS to the live portfolio with robust_oos=True (survives the OOS jackknife).
earns_slot encodes exactly this. HONESTY: the panel is ~2.5 YEARS -> every result is SUGGESTIVE, not robust; a single
good config or one lucky quarter is NOT an edge. Report negatives plainly (most cross-sectional signals will fail here).`
function finderPrompt([fam, id, name, idea]) {
return `You are studying ONE cross-sectional / multi-asset trading mechanism on the certified Hyperliquid alt panel for PythagorasGoal. Goal of the wave: find something DISTINCT from the existing XS01 (plain cross-sectional momentum) that ADDS to the live TP01+XS01+VRP01 portfolio. Implement honestly with the shared library, backtest, report STRUCTURED results.
MECHANISM ${id} [${fam}] — ${name}
IDEA: ${idea}
CHEATSHEET
${CHEAT}
STEPS
1. Write ${ROOT}/scripts/research/xsec/runs/${id}.py: import xslib as xs, implement the score_matrix CAUSALLY, try a SMALL grid (<=5 study_xs calls total: vary universe / H / k / long_short / a param), pick the BEST config by marginal robustness (prefer earns_slot, then hold-out, then distinctness from XS01). Print xs.fmt(rep)+"JSON:"+xs.as_json(rep) for the best.
2. Run: cd ${ROOT} && uv run python scripts/research/xsec/runs/${id}.py (fix NaN/shape errors and re-run until it produces numbers).
3. Fill the schema from your BEST config, HONESTLY. promising=true ONLY if earns_slot is true OR (full>0 AND hold-out>0 AND corr_xs01<0.6 AND marginal verdict is ADDS). Remember the ~2.5y caveat — be skeptical.
CONSTRAINTS: keep <=5 backtests (each scans ~49 assets x 900 days). Score matrices must be (n_days x n_assets), higher=long, causal. Don't fabricate — every number from a real run.
Your final message IS the schema (data row), not prose.`
}
const FIND_SCHEMA = {
type: 'object',
required: ['id','name','family','implemented','best_universe','best_H','best_k','long_short','full_sharpe','holdout_sharpe','worst_maxdd','corr_xs01','corr_tp01','marginal_verdict','robust_oos','earns_slot','promising','summary'],
properties: {
id: { type: 'string' }, name: { type: 'string' }, family: { type: 'string' },
implemented: { type: 'boolean' },
best_universe: { type: 'string' }, best_H: { type: 'number' }, best_k: { type: 'number' },
long_short: { type: 'boolean' },
full_sharpe: { type: 'number' }, holdout_sharpe: { type: 'number' },
worst_maxdd: { type: 'number' },
corr_xs01: { type: 'number', description: 'correlation to existing XS01 (must be <0.6 to be distinct)' },
corr_tp01: { type: 'number' },
marginal_verdict: { type: 'string', enum: ['ADDS','REDUNDANT','DILUTES','NEUTRAL','N/A'] },
holdout_uplift_w20: { type: 'number' },
robust_oos: { type: 'boolean', description: 'survives the OOS drop-best-month jackknife' },
earns_slot: { type: 'boolean' },
promising: { type: 'boolean' },
summary: { type: 'string' },
caveats: { type: 'string' },
script_path: { type: 'string' },
},
}
function verifyPrompt(spec, find, kk) {
const [fam, id, name] = spec
const angles = [
'OVERFIT TO 2.5y / SHORT-HISTORY: the panel is only 2024-2026 with a ~1.5y hold-out. Re-run the best config and its neighbors (other universe/H/k). Is the edge a plateau or one lucky cell? Split the hold-out: is it carried by ONE quarter or the partial-2026 stub? Re-check jackknife (drop-best-month). Default real=false if it leans on a short window or single config.',
'DISTINCTNESS FROM XS01 & LEAK: is corr_xs01 really < 0.6, or is this XS01 in disguise (same momentum signal re-skinned)? Read xslib to confirm the score is causal (no future bar in rolling/beta/residual; weight at i applies to i+1). Confirm the mechanism is economically DIFFERENT from cross-sectional momentum. Default real=false if redundant with XS01 or leaky.',
'MARGINAL & EXECUTABILITY: re-verify it ADDS to the LIVE active portfolio (marginal uplift hold-out positive AND robust_oos) — not just standalone-positive. Is the book executable (a 10-leg market-neutral alt book needs ~20k capital; a long-only top-k is lighter)? Is turnover/fee realistic? For volume/illiquidity signals, are they an artifact of thin alts? Default real=false if it does not robustly improve the live stack.',
]
return `You are an ADVERSARIAL SKEPTIC (#${kk + 1}) for PythagorasGoal. A finder claims cross-sectional mechanism ${id} [${fam}] "${name}" is promising on the Hyperliquid alt panel. REFUTE it — this project was wrecked once by fake edges, and here the history is only ~2.5 years so the overfit risk is HIGH. Assume false-positive until proven otherwise.
FINDER'S CLAIM:
${JSON.stringify(find)}
Run script: ${find.script_path || ROOT + '/scripts/research/xsec/runs/' + id + '.py'}
Trusted leak-free lib: ${ROOT}/scripts/research/xsec/xslib.py
YOUR ANGLE: ${angles[kk % 3]}
Read the script, run your own checks (cd ${ROOT} && uv run python ...), quote the numbers you produce, and decide. Default to real=false when uncertain. Return ONLY the schema.`
}
const VERIFY_SCHEMA = {
type: 'object',
required: ['id','real','confidence','reason'],
properties: {
id: { type: 'string' },
real: { type: 'boolean', description: 'true only if the edge survives your adversarial check AND robustly adds to the live stack' },
confidence: { type: 'number' },
overfit_short_history: { type: 'boolean' },
redundant_with_xs01: { type: 'boolean' },
leak_suspected: { type: 'boolean' },
corrected_full_sharpe: { type: 'number' },
corrected_holdout_sharpe: { type: 'number' },
reason: { type: 'string', description: 'specific, with numbers you produced' },
},
}
// ===========================================================================
phase('Find')
log(`Searching ${CATALOG.length} cross-sectional mechanisms on the 51-alt Hyperliquid panel, one agent each. Frontier: distinct from XS01, additive to the live stack.`)
const results = await pipeline(
CATALOG,
(spec) => agent(finderPrompt(spec), { label: `find:${spec[1]}`, phase: 'Find', schema: FIND_SCHEMA, model: 'sonnet', effort: 'medium' }),
(find, spec) => {
if (!find) return { id: spec[1], name: spec[2], family: spec[0], promising: false, verify: [] }
if (!find.promising) return { ...find, verify: [] }
return parallel([0, 1, 2].map((kk) => () =>
agent(verifyPrompt(spec, find, kk), { label: `verify:${spec[1]}.${kk}`, phase: 'Verify', schema: VERIFY_SCHEMA, effort: 'high' })
)).then((votes) => ({ ...find, verify: votes.filter(Boolean) }))
}
)
phase('Synthesize')
const clean = results.filter(Boolean)
const enriched = clean.map((r) => {
const v = r.verify || []
const realVotes = v.filter((x) => x && x.real).length
const survived = r.promising && v.length >= 2 && realVotes >= Math.ceil(v.length / 2)
return { ...r, real_votes: realVotes, n_verify: v.length, survived }
})
const survivors = enriched.filter((r) => r.survived)
const killed = enriched.filter((r) => r.promising && !r.survived)
log(`Find done: ${clean.length} studied. Promising: ${enriched.filter(r => r.promising).length}. Survived adversarial verify: ${survivors.length}.`)
const compact = enriched.map((r) => ({
id: r.id, name: r.name, family: r.family, universe: r.best_universe, H: r.best_H, k: r.best_k, ls: r.long_short,
full: r.full_sharpe, hold: r.holdout_sharpe, dd: r.worst_maxdd, corr_xs01: r.corr_xs01, corr_tp01: r.corr_tp01,
marginal: r.marginal_verdict, robust: r.robust_oos, earns_slot: r.earns_slot, promising: r.promising,
survived: r.survived, real_votes: r.real_votes, summary: r.summary,
verify: (r.verify || []).map((x) => x ? `[real=${x.real} conf=${x.confidence}] ${x.reason}` : '').filter(Boolean),
}))
const SYNTH_SCHEMA = {
type: 'object',
required: ['headline', 'survivors', 'ranking', 'recommendations', 'dead_families'],
properties: {
headline: { type: 'string', description: '2-4 sentences: did a NEW cross-sectional mechanism, distinct from XS01 and additive to the live stack, emerge — net of the ~2.5y caveat?' },
survivors: { type: 'array', items: { type: 'object', required: ['id', 'name', 'why', 'suggested_role'], properties: {
id: { type: 'string' }, name: { type: 'string' }, why: { type: 'string' },
suggested_role: { type: 'string', description: 'new sleeve candidate / lead to forward-monitor / needs longer history' },
distinct_from_xs01: { type: 'string' } } } },
ranking: { type: 'array', items: { type: 'string' } },
recommendations: { type: 'string', description: 'concrete: what (if anything) to deep-validate or add, weight, and how to handle the short history' },
dead_families: { type: 'array', items: { type: 'string' } },
},
}
const synthPrompt = `You are the SYNTHESIZER for a PythagorasGoal wave that searched ${CATALOG.length} CROSS-SECTIONAL / multi-asset mechanisms on the 51 certified Hyperliquid alts (1d, 2024-2026), then adversarially verified every promising one. This is the frontier the previous BTC/ETH sweep pointed to (single-asset directional is exhausted at the ~1.3 ceiling).
LIVE stack (do not re-derive): TP01 (TSMOM trend BTC/ETH, defensive), XS01 (cross-sectional MOMENTUM on 19 HL majors, top5/bottom5, blend+dispersion-gate, vol-target — corr ~-0.12 to TP01), VRP01 (modeled options short-vol). A NEW cross-sectional sleeve is only valuable if it is (1) robust despite the SHORT ~2.5y history, (2) DISTINCT from XS01 (corr < 0.6 — not momentum re-skinned), and (3) ADDS to the live active portfolio out-of-sample (marginal uplift + robust_oos jackknife). Honesty is prime: on 2.5 years, be very skeptical; a clean set of negatives is an acceptable outcome.
Full result table (verify = the skeptics' findings):
${JSON.stringify(compact)}
Survivors (passed adversarial verify): ${JSON.stringify(survivors.map((s) => ({ id: s.id, name: s.name, full: s.full_sharpe, hold: s.holdout_sharpe, corr_xs01: s.corr_xs01, corr_tp01: s.corr_tp01, marginal: s.marginal_verdict, real_votes: s.real_votes })))}
Promising-but-killed: ${JSON.stringify(killed.map((s) => ({ id: s.id, name: s.name, why: (s.verify || []).map((v) => v && v.reason).filter(Boolean) })))}
Produce the synthesis. Be concrete and skeptical about the short history. If a genuinely distinct, additive mechanism survived (e.g. a risk/low-vol anomaly orthogonal to momentum), say what it is, whether it is a sleeve candidate or a lead needing more history, and its correlation profile. If nothing robust survived, say so plainly.`
const synthesis = await agent(synthPrompt, { schema: SYNTH_SCHEMA, effort: 'high', label: 'synthesize' })
return {
n_studied: clean.length,
n_promising: enriched.filter((r) => r.promising).length,
n_survived: survivors.length,
survivors: survivors.map((s) => ({ id: s.id, name: s.name, family: s.family, full: s.full_sharpe, hold: s.holdout_sharpe, corr_xs01: s.corr_xs01, corr_tp01: s.corr_tp01, marginal: s.marginal_verdict, real_votes: s.real_votes, summary: s.summary })),
promising_killed: killed.map((s) => ({ id: s.id, name: s.name })),
all_grades: clean.map((r) => ({ id: r.id, name: r.name, full: r.full_sharpe, hold: r.holdout_sharpe, corr_xs01: r.corr_xs01, marginal: r.marginal_verdict, earns_slot: r.earns_slot, promising: r.promising })),
synthesis,
}