/* HERCULEAN · Trade with an agent. Each day the player picks an agent system (architecture × backbone); one of its three recorded runs is drawn (only among runs that committed that day; HOLD if none did) and its real Trading decision is executed. Scoring follows analysis/score_runs.py: BUY/SELL/HOLD -> +1/-1/0, held from the open of the day to the next open (last day: open to close), adjusted prices, no costs. SR = sqrt(252)·mean/std of daily returns. */ (() => { const H = window.HERC, OH = H.ohlc, HIST = OH.start, N = H.dates.length, $ = id => document.getElementById(id); const AC = { ReAct: '#2a78d6', 'Claude Code': '#eb6834', Codex: '#1baf7a', Hermes: '#eda100', OpenClaw: '#e87ba4' }; const INI = { ReAct: 'RA', 'Claude Code': 'CC', Codex: 'CX', Hermes: 'HM', OpenClaw: 'OC' }; const MS = { 'Claude Sonnet 5': 'Sonnet 5', 'DeepSeek-V3.2': 'DS-V3.2', 'DeepSeek-V4-Pro': 'DS-V4-Pro', 'Qwen3.5-9B': 'Qwen-9B', 'Qwen3.5-4B': 'Qwen-4B' }; const CO = { MSFT: 'Microsoft', TSLA: 'Tesla', AAPL: 'Apple', NVDA: 'NVIDIA' }; const NAMES = ['Perseus', 'Theseus', 'Atalanta', 'Achilles', 'Odysseus', 'Penelope', 'Jason', 'Medea', 'Orpheus', 'Eurydice', 'Andromeda', 'Cassandra', 'Hector', 'Ajax', 'Electra', 'Antigone', 'Ariadne', 'Bellerophon', 'Castor', 'Pollux', 'Daedalus', 'Icarus', 'Hippolyta', 'Iphigenia', 'Nestor', 'Telemachus', 'Pandora', 'Psyche', 'Calypso', 'Circe', 'Paris', 'Helen']; const ACT = { B: 'BUY', S: 'SELL', H: 'HOLD' }, POS = { B: 1, S: -1, H: 0 }; const short = m => MS[m] || m, sysName = (a, m) => `${a} · ${short(m)}`; const pct = (v, d = 2) => (v >= 0 ? '+' : '−') + Math.abs(v * 100).toFixed(d) + '%'; const money = v => '$' + Math.round(v).toLocaleString('en-US'); const cls = v => v > 0 ? 'up' : v < 0 ? 'dn' : ''; const pickRand = a => a[Math.floor(Math.random() * a.length)]; let S; // game state let arch = 'Hermes', model = 'DeepSeek-V4-Pro', name = pickRand(NAMES), sym = 'MSFT', autoT = null, hover = null; // per-day returns on the chosen stock (open -> next open; last day open -> close) const returns = tk => H.dates.map((_, d) => { const b = OH.bars[tk][HIST + d], nx = OH.bars[tk][HIST + d + 1]; return (nx ? nx[0] : b[3]) / b[0] - 1; }); const metrics = (rets) => { let w = 1, peak = 1, mdd = 0; for (const r of rets) { w *= 1 + r; peak = Math.max(peak, w); mdd = Math.max(mdd, 1 - w / peak); } const n = rets.length, mu = n ? rets.reduce((p, q) => p + q, 0) / n : 0; const sd = n > 1 ? Math.sqrt(rets.reduce((p, q) => p + (q - mu) ** 2, 0) / (n - 1)) : 0; return { cr: w - 1, sr: sd > 0 ? Math.sqrt(252) * mu / sd : 0, mdd }; }; // the 30 systems on this stock: mean wealth over their three runs after each day (missing decision = flat) function field(tk, ret) { return H.agents.flatMap(a => H.models.map(m => { const runs = (H.runs.trading[`${a}|${m}|${tk}`] || []).filter(Boolean), w = new Array(N + 1).fill(0); runs.forEach(r => { let x = 1; w[0] += 1; for (let d = 0; d < N; d++) { x *= 1 + (POS[r[d]] || 0) * ret[d]; w[d + 1] += x; } }); return { a, m, w: w.map(v => v / (runs.length || 1)) }; })); } // where the player stands after day d (CR, ties go to the player) function standing(d) { const you = S.eq[d] - 1, all = S.field.map(f => ({ a: f.a, m: f.m, cr: f.w[d] - 1 })).sort((p, q) => q.cr - p.cr); const rank = 1 + all.filter(f => f.cr > you + 1e-12).length; return { you, all, rank, n: all.length + 1, top: all[0], bh: S.bh[d] - 1 }; } // the field on one CR axis, one lane per architecture (dots = backbones); you = the purple line and dot function strip(st, W, Hs) { const xs = [...st.all.map(f => f.cr), st.you, st.bh, 0], lo = Math.min(...xs), hi = Math.max(...xs), span = hi - lo || .01, pl = 26, pr = 10; const X = v => pl + (v - lo) / span * (W - pl - pr), top = 16, axY = Hs - 14, lh = (axY - top - 2) / H.agents.length; const LY = a => top + (H.agents.indexOf(a) + .5) * lh, r = Math.max(2.8, Math.min(4.6, lh * .36)); let g = ''; H.agents.forEach((a, i) => g += `` + `${INI[a]}`); g += ``; const lab = [[lo, pct(lo, 1), 'start'], [hi, pct(hi, 1), 'end'], [0, '0%', 'middle'], [st.bh, 'B&H', 'middle']], used = []; lab.reverse().forEach(([v, t, an]) => { const x = X(v); if (used.some(u => Math.abs(u - x) < 30)) return; used.push(x); g += `${t}`; }); g += ``; g += ``; st.all.forEach(f => g += `${sysName(f.a, f.m)} · ${pct(f.cr)}`); const t = st.top; g += `#1`; const yx = X(st.you); g += `you · ${pct(st.you)}` + `you`; return `${g}`; } // ranking rows: top 3, then the player's neighbourhood (`around` rows either side) function rankTable(st, around, compact) { const rank = st.rank, rows = st.all.map((f, i) => ({ ...f, r: i + 1 + (i + 1 >= rank ? 1 : 0) })); rows.splice(rank - 1, 0, { you: true, cr: st.you, r: rank }); const keep = new Set([0, 1, 2]); for (let k = -around; k <= around; k++) keep.add(rank - 1 + k); let tb = '', prev = -1; rows.forEach((f, i) => { if (!keep.has(i)) return; if (prev >= 0 && i > prev + 1) tb += '
⋯
'; prev = i; tb += `
#${f.r}${f.you ? `${name} your picks` : `${INI[f.a]}${sysName(f.a, f.m)}`}` + `${pct(f.cr)}${compact ? '' : `${i === 0 ? '—' : ((f.cr - rows[0].cr) * 100).toFixed(2) + ' pp'}`}
`; }); return tb; } function newGame(tk) { sym = tk; stopAuto(); S = { d: 0, ret: returns(tk), log: [], eq: [1], bh: [1], picks: {} }; S.field = field(tk, S.ret); $('trader').textContent = name; $('ava').textContent = name[0]; $('rev').innerHTML = 'Pick an architecture and a backbone, then trade. One of its 3 runs is drawn at random.'; renderAll(); } // ---------- one trading day ---------- function step() { if (!S || S.d >= N) return; const d = S.d, runs = H.runs.trading[`${arch}|${model}|${sym}`] || ['', '', '']; const ok = [0, 1, 2].filter(r => runs[r] && runs[r][d] && runs[r][d] !== '-'); const r = ok.length ? pickRand(ok) : Math.floor(Math.random() * 3); const a = ok.length ? runs[r][d] : 'H', miss = !ok.length; const pnl = POS[a] * S.ret[d]; S.eq.push(S.eq[d] * (1 + pnl)); S.bh.push(S.bh[d] * (1 + S.ret[d])); S.log.push({ d, a, arch, model, run: r + 1, pnl, miss }); const k = `${arch}|${model}`; S.picks[k] = (S.picks[k] || 0) + 1; S.d++; // the three recorded runs of this system on this day; the drawn one is outlined const calls = [0, 1, 2].map(i => { const c = runs[i] && runs[i][d]; return !c || c === '-' ? null : c; }); $('rev').innerHTML = `
${sysName(arch, model)}${miss ? 'HOLD · no commit' : pct(pnl)}
` + `
${H.dates[d].slice(5)} · its 3 runs${calls.map((c, i) => `r${i + 1}${c ? `${ACT[c]}` : '—'}`).join('')}
`; const rv = $('rev'); rv.classList.remove('flash'); void rv.offsetWidth; rv.classList.add('flash'); renderAll(true); if (S.d >= N) { stopAuto(); setTimeout(endCard, 700); } } function stopAuto() { clearInterval(autoT); autoT = null; $('autoBtn').classList.remove('on'); $('autoBtn').textContent = 'Auto ▸▸'; } // ---------- render ---------- function renderAll(fresh) { renderSide(fresh); draw(fresh); } function renderSide(fresh) { const done = S.d >= N, eq = S.eq[S.d], bh = S.bh[S.d], m = metrics(S.log.map(l => l.pnl)); $('dayK').textContent = done ? 'window closed' : `Day ${S.d + 1} · ${H.dates[S.d].slice(5)}`; $('eqv').textContent = money(1e5 * eq); $('eqr').innerHTML = `${pct(eq - 1)} vs ${pct(bh - 1)}`; const last = S.log[S.log.length - 1]; const pos = !last ? 'Flat' : { B: 'Long', S: 'Short', H: 'Flat' }[last.a]; $('kv').innerHTML = [['Position', pos], ['Max DD', (m.mdd * 100).toFixed(2) + '%'], ['Sharpe', S.log.length > 1 ? m.sr.toFixed(2) : '—']] .map(([k, v]) => `
${k}${v}
`).join(''); // P&L attribution: each picked system's days, win days and summed daily P&L (pp) const by = {}; S.log.forEach(l => { const k = `${l.arch}|${l.model}`, b = by[k] || (by[k] = { a: l.arch, m: l.model, days: 0, win: 0, pnl: 0 }); b.days++; b.pnl += l.pnl; if (l.pnl > 0) b.win++; }); const ag = Object.values(by).sort((p, q) => q.pnl - p.pnl), amax = Math.max(.002, ...ag.map(b => Math.abs(b.pnl))); $('attr').innerHTML = `
P&L attributionby agent system
` + (ag.length ? `
agent systemdays · wonP&L
` + ag.map(b => `
` + `${INI[b.a]}${short(b.m)}` + `${b.days} · ${b.win}` + `` + `${pct(b.pnl)}
`).join('') + '
' : '

Which of your picks made or lost money will show here.

'); const st = standing(S.d), gap = (st.you - st.top.cr) * 100; $('stand').innerHTML = `
Standing · ${sym} CRmean of 3 runs
` + `
${S.d ? `#${st.rank} / ${st.n}` : '—'}` + (S.d ? `${st.rank === 1 ? '+' + ((st.you - st.all[0].cr) * 100).toFixed(2) + ' pp ahead' : '−' + Math.abs(gap).toFixed(2) + ' pp to #1'}` : '') + '
' + (S.d ? '
' + `
#agent systemCR
${rankTable(st, 99, true)}
` : `

Trade to enter the ranking of the 30 agent systems (CR, mean of 3 runs, up to the same day).

`); const me = document.querySelector('#stList .tr.me'), li = $('stList'); // centre the player's row in the list (positions measured relative to the list itself) if (me && li) li.scrollTop = Math.max(0, me.getBoundingClientRect().top - li.getBoundingClientRect().top + li.scrollTop - (li.clientHeight - me.offsetHeight) / 2); lastSt = S.d ? st : null; plotStand(); requestAnimationFrame(plotStand); $('arch').innerHTML = H.agents.map(a => ``).join(''); $('model').innerHTML = H.models.map(x => ``).join(''); $('goBtn').disabled = done; $('autoBtn').disabled = done; $('log').innerHTML = H.dates.map((dt, d) => { const l = S.log[d]; if (!l) return `
${dt.slice(5)}D${d + 1}
${d === S.d ? 'today' : ''}
·
`; return `
${dt.slice(5)}D${d + 1}
` + `
${ACT[l.a]}${pct(l.pnl)}
${INI[l.arch]} · ${short(l.model)} · r${l.run}${l.miss ? ' · –' : ''}
`; }).join(''); const tl = $('log'), cur = tl.children[Math.min(S.d, N - 1)]; if (cur) tl.scrollTo({ left: Math.max(0, cur.offsetLeft - tl.clientWidth + cur.offsetWidth * 2.5), behavior: 'auto' }); } function head(i) { const b = OH.bars[sym], cur = b[i], prev = b[i - 1], chg = cur[3] / prev[3] - 1, d = Math.min(S.d, N), done = S.d >= N; const dt = new Date((done ? H.dates[N - 1] : H.dates[S.d]) + 'T12:00:00Z').toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }); $('head').innerHTML = `${sym}${CO[sym]}${cur[3].toFixed(2)}${pct(chg)}` + `${OH.dates[i]} · O ${cur[0].toFixed(2)} H ${cur[1].toFixed(2)} L ${cur[2].toFixed(2)} C ${cur[3].toFixed(2)} · Vol ${cur[4].toFixed(1)}M` + ''; void dt; void d; void done; } // the standing plot takes whatever height the card has left; redraw when that changes (fonts, resize) let lastSt = null; function plotStand() { const sp = $('stPlot'); if (!sp || !lastSt) return; sp.innerHTML = ''; sp.innerHTML = strip(lastSt, sp.clientWidth || 328, Math.max(48, Math.min(170, sp.clientHeight))); } if (window.ResizeObserver) new ResizeObserver(() => plotStand()).observe($('stand')); if (document.fonts) { document.fonts.ready.then(() => { plotStand(); S && draw(); }); document.fonts.addEventListener('loadingdone', () => { plotStand(); S && draw(); }); } let G = null; // geometry for hover function draw(fresh) { const svg = $('kline'), W = svg.clientWidth, Hh = svg.clientHeight; if (!W || !Hh) return; svg.setAttribute('viewBox', `0 0 ${W} ${Hh}`); const bars = OH.bars[sym], total = HIST + N, L = 6, R = 58, cw = (W - L - R) / total, X = i => L + (i + .5) * cw; const nShown = HIST + S.d, shown = bars.slice(0, nShown); const pTop = 20, pH = Hh * .5, aTop = pTop + pH + 10, aH = 13, vTop = aTop + aH + 8, vH = Hh * .08, eTop = vTop + vH + 22, eH = Hh - eTop - 46, tY = Hh - 16; // price scale from what has been revealed only (no peeking at the future) let lo = Math.min(...shown.map(b => b[2])), hi = Math.max(...shown.map(b => b[1])); const pad = (hi - lo) * .12; lo -= pad; hi += pad; const Y = v => pTop + (1 - (v - lo) / (hi - lo)) * pH, vmax = Math.max(...shown.map(b => b[4])); let g = ''; for (let t = 0; t <= 4; t++) { const v = lo + t / 4 * (hi - lo); g += `${v.toFixed(0)}`; } const x0 = X(HIST) - cw / 2, xn = X(nShown) - cw / 2; g += ``; if (S.d < N) g += ``; g += `Jan 2 · window opens`; // legends inside the panels g += `MA5` + `MA20` + `agent BUYagent SELL`; // daily decision strip (as in the Trading replay): one cell per window day g += `ACTION`; const AF = { B: '#0f9d58', S: '#d6304a', H: '#d9d7e3' }; for (let d = 0; d < N; d++) { const l = S.log[d], cx0 = X(HIST + d) - cw / 2 + .6, cwi = Math.max(1, cw - 1.2); if (l) g += ``; else g += ``; } // moving averages [[5, '#eda100'], [20, '#7800f0']].forEach(([n, c]) => { let p = ''; for (let i = n - 1; i < nShown; i++) { const v = bars.slice(i - n + 1, i + 1).reduce((s, b) => s + b[3], 0) / n; p += (p ? 'L' : 'M') + X(i).toFixed(1) + ',' + Y(v).toFixed(1); } g += ``; }); // candles + volume const w = Math.max(1.5, cw * .62); shown.forEach((b, i) => { const [o, h, l, c, v] = b, up = c >= o, col = up ? '#0f9d58' : '#d6304a', nw = fresh && i === nShown - 1 ? ' class="cnew"' : ''; g += ``; g += ``; }); // the agents' calls S.log.forEach((l, k) => { const i = HIST + l.d, b = bars[i], nw = fresh && k === S.log.length - 1 ? ' class="mk"' : ''; if (l.a === 'B') g += ``; if (l.a === 'S') g += ``; }); const lastC = shown[nShown - 1][3], lastUp = lastC >= shown[nShown - 2][3]; g += `${lastC.toFixed(2)}`; // equity panel const all = [...S.eq, ...S.bh], emin = Math.min(...all), emax = Math.max(...all), ep = Math.max((emax - emin) * .15, .01); const EY = v => eTop + (1 - (v - emin + ep) / (emax - emin + 2 * ep)) * eH; const ex = k => k === 0 ? x0 : X(HIST + k - 1) + cw / 2; // equity after day k sits at the right edge of that day g += `EQUITY$100k`; const line = arr => arr.map((v, k) => (k ? 'L' : 'M') + ex(k).toFixed(1) + ',' + EY(v).toFixed(1)).join(''); if (S.d) { g += ``; g += ``; g += ``; } else g += `your equity starts here`; // time bar under the chart: the 61-day window, filled up to today; the Day tag rides on it const xEnd = X(HIST + N - 1) + cw / 2, cur = Math.min(S.d, N - 1), tx = S.d >= N ? xEnd : X(HIST + S.d); if (S.d < N) g += ``; g += `` + ``; [20, 40].forEach(k => g += ``); g += `Jan 2Mar 31`; // date tag riding on the bar: month and day on top, weekday below const dd = new Date(H.dates[cur] + 'T12:00:00Z'), fmt = o => dd.toLocaleDateString('en-US', { ...o, timeZone: 'UTC' }); const l1 = S.d >= N ? 'Closed' : fmt({ month: 'short', day: 'numeric' }), l2 = S.d >= N ? 'Mar 31' : fmt({ weekday: 'short' }); const tw = Math.max(l1.length * 6.4, l2.length * 5.4) + 16, tcx = Math.min(Math.max(tx, x0 + tw / 2), xEnd - tw / 2); g += `` + `${l1}${l2}`; g += `your equity` + `Buy & Hold`; // date axis g += `${new Date(OH.dates[0] + 'T12:00:00Z').toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' })}`; g += ``; svg.innerHTML = `` + g; G = { X, cw, L, R, W, nShown, pTop, bot: eTop + eH, Y, EY, ex }; head(hover != null && hover < nShown ? hover : nShown - 1); } // hover crosshair + readout (chart and log tiles) function showDay(i, cx, cy) { if (!G || i < 0 || i >= G.nShown) return leave(); hover = i; head(i); const day = i - HIST, l = day >= 0 ? S.log[day] : null; $('hv').innerHTML = `` + (l ? `` : ''); document.querySelectorAll('.tile.on').forEach(t => t.classList.remove('on')); if (day >= 0 && $('log').children[day]) $('log').children[day].classList.add('on'); const tip = $('tip'); if (l && cx != null) { tip.innerHTML = `${sysName(l.arch, l.model)} · run ${l.run} → ${ACT[l.a]} · ${pct(l.pnl)}`; tip.style.left = cx + 14 + 'px'; tip.style.top = cy - 34 + 'px'; tip.classList.add('on'); } else tip.classList.remove('on'); } $('kline').addEventListener('mousemove', e => { if (!G) return; const r = e.currentTarget.getBoundingClientRect(); showDay(Math.round((e.clientX - r.left - G.L) / G.cw - .5), e.clientX, e.clientY); }); $('log').addEventListener('mouseover', e => { const t = e.target.closest('.tile'); if (t && S.log[+t.dataset.d]) showDay(HIST + +t.dataset.d); }); $('log').addEventListener('mouseleave', () => leave()); const leave = () => { hover = null; if ($('hv')) $('hv').innerHTML = ''; document.querySelectorAll('.tile.on').forEach(t => t.classList.remove('on')); $('tip').classList.remove('on'); if (G) head(G.nShown - 1); }; $('kline').addEventListener('mouseleave', leave); // ---------- picks & buttons ---------- $('arch').addEventListener('click', e => { const b = e.target.closest('button'); if (b) { arch = b.dataset.a; renderSide(); } }); $('model').addEventListener('click', e => { const b = e.target.closest('button'); if (b) { model = b.dataset.m; renderSide(); } }); $('goBtn').addEventListener('click', () => { stopAuto(); step(); }); $('autoBtn').addEventListener('click', () => { if (autoT) return stopAuto(); $('autoBtn').classList.add('on'); $('autoBtn').textContent = 'Stop ■'; step(); autoT = setInterval(step, 320); }); $('newBtn').addEventListener('click', startCard); document.addEventListener('keydown', e => { if ($('ov').classList.contains('on')) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); stopAuto(); step(); } }); addEventListener('resize', () => S && draw()); // ---------- overlays ---------- const spark = tk => { // pre-window history only const c = OH.bars[tk].slice(0, HIST).map(b => b[3]), lo = Math.min(...c), hi = Math.max(...c), up = c[c.length - 1] >= c[0]; const p = c.map((v, i) => `${(i / (c.length - 1) * 100).toFixed(1)},${(30 - (v - lo) / (hi - lo) * 26 - 2).toFixed(1)}`).join(' '); return ``; }; function open(html) { $('ovCard').innerHTML = html; $('ov').classList.add('on'); } function close() { $('ov').classList.remove('on'); } function startCard() { stopAuto(); open(`
HERCULEAN · Trading workflow

Welcome, ${name}

Pick a stock. For each of the 61 trading days (Jan 2 – Mar 31, 2026) you choose which agent system trades; one of its three recorded runs is drawn, and its real decision is executed. Can your picks beat Buy & Hold?

${H.tradeAssets.map(t => ``).join('')}
30agent systems3runs each$100kstartopen→openno costs
${S && S.d && S.d < N ? '' : ''}See the leaderboard
`); let tk = sym; $('ovCard').querySelectorAll('.stock').forEach(b => b.onclick = () => { tk = b.dataset.t; $('ovCard').querySelectorAll('.stock').forEach(x => x.classList.toggle('sel', x === b)); }); $('reroll').onclick = () => { let n; do n = pickRand(NAMES); while (n === name); name = n; $('nm').textContent = n; }; $('startBtn').onclick = () => { close(); newGame(tk); }; if ($('resume')) $('resume').onclick = close; } function endCard() { const m = metrics(S.log.map(l => l.pnl)), bh = metrics(S.ret), st = standing(N), rank = st.rank, top = st.top; const fav = Object.entries(S.picks).sort((p, q) => q[1] - p[1])[0], [fa, fm] = fav[0].split('|'); const beat = m.cr > bh.cr, gap = (m.cr - top.cr) * 100; const tb = rankTable(st, 1); open(`
${sym} · window closed · 61 days

${beat ? `${name} beat Buy & Hold` : `Buy & Hold wins this time, ${name}`}

Final equity${money(1e5 * (1 + m.cr))}B&H ${money(1e5 * (1 + bh.cr))}
CR${pct(m.cr)}B&H ${pct(bh.cr)}
SR${m.sr.toFixed(2)}B&H ${bh.sr.toFixed(2)}
MDD${(m.mdd * 100).toFixed(2)}%B&H ${(bh.mdd * 100).toFixed(2)}%
#${rank}of ${st.n} on ${sym} Trading ${rank === 1 ? 'ahead of #2 by ' + ((m.cr - st.all[0].cr) * 100).toFixed(2) + ' pp' : `${gap.toFixed(2)} pp behind #1`}
${strip(st, 580, 96)}
RankAgent system · CR, mean of 3 runsCRvs #1
${tb}
Your most-picked: ${sysName(fa, fm)} (${fav[1]} of 61 days)
See the leaderboard
`); $('again').onclick = () => { name = pickRand(NAMES.filter(n => n !== name)); startCard(); }; $('review').onclick = close; } newGame(sym); startCard(); window.__play = { step, state: () => S, set: (a, m) => { arch = a; model = m; renderSide(); }, endCard, close }; // for tests })();