Files
polymarket-analyst/index.html
T
2026-07-12 16:06:52 -04:00

2036 lines
123 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Polymarket Analyst — 10 AI Agents Compete to Trade Prediction Markets</title>
<meta name="description" content="Hourly AI analysis of Polymarket with ten competing autonomous paper-trading agents, whale-copy portfolios, and a live leaderboard." />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏆</text></svg>">
<style>
:root{
--bg:#070b14;--bg2:#0b1120;--panel:rgba(22,29,46,.66);--panel-solid:#161d2e;
--panel-2:rgba(36,45,68,.6);--border:rgba(255,255,255,.09);--border-strong:rgba(255,255,255,.16);
--text:#eef2f9;--muted:#94a1bb;--green:#34d399;--red:#fb7185;--yellow:#fbbf24;
--accent:#7c8cff;--accent2:#a878ff;--cyan:#38d2e6;
--shadow:0 18px 50px -20px rgba(0,0,0,.7);
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background:var(--bg);color:var(--text);line-height:1.5;-webkit-font-smoothing:antialiased;
min-height:100vh;overflow-x:hidden}
h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif}
/* ---------- layered background imagery ---------- */
.bg{position:fixed;inset:0;z-index:-2;overflow:hidden;
background:
radial-gradient(1200px 700px at 80% -10%, rgba(124,140,255,.18), transparent 60%),
radial-gradient(900px 600px at 5% 10%, rgba(168,120,255,.16), transparent 55%),
radial-gradient(1000px 800px at 50% 110%, rgba(56,210,230,.12), transparent 60%),
linear-gradient(180deg,#070b14,#0a1020 40%,#070b14)}
.bg::before{content:"";position:absolute;inset:0;opacity:.5;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M44 0H0V44' fill='none' stroke='%23ffffff' stroke-opacity='0.035' stroke-width='1'/%3E%3C/svg%3E")}
.bg .orb{position:absolute;border-radius:50%;filter:blur(70px);opacity:.55;animation:float 18s ease-in-out infinite}
.orb.a{width:480px;height:480px;left:-120px;top:-80px;background:radial-gradient(circle,#5b6cff,transparent 70%)}
.orb.b{width:520px;height:520px;right:-140px;top:140px;background:radial-gradient(circle,#a878ff,transparent 70%);animation-delay:-6s}
.orb.c{width:440px;height:440px;left:35%;bottom:-180px;background:radial-gradient(circle,#21b6c9,transparent 70%);animation-delay:-12s}
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(40px,-30px)}}
.bg .chartline{position:absolute;left:0;right:0;bottom:0;width:100%;height:240px;opacity:.5}
/* ---------- app shell ---------- */
.app{max-width:1240px;margin:0 auto;padding:0 22px 60px}
.topnav{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:18px;flex-wrap:wrap;
padding:14px 18px;margin:14px 0 8px;border:1px solid var(--border);border-radius:18px;
background:rgba(10,15,28,.72);backdrop-filter:blur(16px);box-shadow:var(--shadow)}
.brand{display:flex;align-items:center;gap:11px;min-width:0}
.logo{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;font-size:20px;
background:linear-gradient(135deg,var(--accent),var(--accent2));box-shadow:0 6px 20px -6px var(--accent)}
.brand-name{font-size:18px;font-weight:700;letter-spacing:-.2px;line-height:1}
.brand-sub{font-size:11px;color:var(--muted);margin-top:2px}
.tabs{display:flex;gap:4px;background:rgba(255,255,255,.04);padding:4px;border-radius:12px;border:1px solid var(--border);max-width:100%;overflow-x:auto}
.tab{appearance:none;border:0;background:transparent;color:var(--muted);font-family:inherit;
font-size:13.5px;font-weight:600;padding:8px 14px;border-radius:9px;cursor:pointer;transition:.18s;white-space:nowrap}
.tab:hover{color:var(--text)}
.tab.active{color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent2));box-shadow:0 6px 18px -8px var(--accent)}
.nav-right{margin-left:auto;display:flex;align-items:center;gap:10px;min-width:0}
.status{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--muted);
padding:6px 11px;border-radius:9px;background:rgba(255,255,255,.04);border:1px solid var(--border);min-width:0}
#statusText{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px}
.dot{width:8px;height:8px;border-radius:50%;background:var(--muted)}
.dot.live{background:var(--green);animation:pulse 1.6s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(52,211,153,.5)}70%{box-shadow:0 0 0 7px rgba(52,211,153,0)}100%{box-shadow:0 0 0 0 rgba(52,211,153,0)}}
.btn{border:1px solid var(--border-strong);border-radius:10px;padding:9px 15px;font-size:13.5px;
font-weight:600;font-family:inherit;cursor:pointer;background:rgba(255,255,255,.05);color:var(--text);transition:.16s}
.btn:hover{border-color:var(--accent);transform:translateY(-1px)}
.btn.primary{background:linear-gradient(135deg,var(--accent),var(--accent2));border-color:transparent;color:#fff;box-shadow:0 8px 22px -10px var(--accent)}
.btn.primary:hover{filter:brightness(1.08)}
.btn:disabled{opacity:.55;cursor:wait;transform:none}
.btn.ghost{background:transparent}
/* ---------- hero ---------- */
.hero{position:relative;border:1px solid var(--border);border-radius:22px;padding:34px 32px;margin:14px 0 22px;
overflow:hidden;background:linear-gradient(120deg,rgba(124,140,255,.16),rgba(168,120,255,.08) 50%,rgba(56,210,230,.10));box-shadow:var(--shadow)}
.hero-art{position:absolute;right:-10px;top:-10px;bottom:-10px;width:46%;opacity:.85;pointer-events:none}
.hero h1{margin:0 0 10px;font-size:34px;letter-spacing:-.8px;line-height:1.08;max-width:660px}
.hero h1 .grad{background:linear-gradient(100deg,#a9b6ff,#d9b4ff 50%,#8fe8f3);-webkit-background-clip:text;background-clip:text;color:transparent}
.hero p{margin:0;color:#c3cce0;font-size:15.5px;max-width:580px}
.hero .badges{display:flex;gap:9px;flex-wrap:wrap;margin-top:18px}
.hbadge{font-size:12px;font-weight:600;color:#dfe6f5;padding:6px 12px;border-radius:999px;background:rgba(255,255,255,.07);border:1px solid var(--border)}
/* ---------- panels & cards ---------- */
.tabpanel{display:none;animation:fade .35s ease}
.tabpanel.active{display:block}
@keyframes fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.grid2{display:grid;grid-template-columns:1.05fr .95fr;gap:20px}
@media (max-width:920px){.grid2{grid-template-columns:1fr}}
.card{background:var(--panel);border:1px solid var(--border);border-radius:18px;padding:20px 22px;
margin-bottom:20px;backdrop-filter:blur(14px);box-shadow:var(--shadow)}
.card.hoverable{transition:.2s}
.card.hoverable:hover{border-color:var(--border-strong);transform:translateY(-2px)}
.card-h{display:flex;align-items:center;justify-content:space-between;margin:0 0 16px}
.card-h h3{margin:0;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;font-weight:600}
.section-title{display:flex;align-items:center;gap:11px;font-size:20px;margin:6px 0 16px;font-weight:700}
.section-title::before{content:"";width:5px;height:22px;border-radius:3px;background:linear-gradient(var(--accent),var(--accent2))}
.muted{color:var(--muted)}.small{font-size:12.5px}
/* ---------- stat cards ---------- */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(165px,1fr));gap:14px;margin-bottom:20px}
.stat{position:relative;background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;overflow:hidden;backdrop-filter:blur(14px);transition:.2s}
.stat:hover{transform:translateY(-2px);border-color:var(--border-strong)}
.stat::after{content:"";position:absolute;left:0;top:0;height:3px;width:100%;background:linear-gradient(90deg,var(--accent),var(--accent2))}
.stat .ic{font-size:18px;opacity:.9}
.stat .label{color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.6px;margin-top:8px}
.stat .value{font-size:25px;font-weight:700;margin-top:4px;font-family:'Space Grotesk',sans-serif;letter-spacing:-.5px}
.pos-val{color:var(--green)}.neg-val{color:var(--red)}
/* ---------- charts ---------- */
.chart-wrap{width:min(100%,1120px);margin:10px auto 0}
#chart,#chart2{width:100%;height:auto;display:block;overflow:visible}
.legend{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px 12px;margin-top:14px}
.leg{display:flex;align-items:center;gap:8px;font-size:12px;color:#cdd6e8;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:7px 9px;min-width:0}
.leg b{margin-left:auto;font-family:'Space Grotesk',sans-serif}
.legdot{width:9px;height:9px;border-radius:50%;display:inline-block;box-shadow:0 0 0 3px rgba(255,255,255,.06)}
.rangebar{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}
.rangebtn{appearance:none;border:1px solid var(--border);background:rgba(255,255,255,.04);color:var(--muted);
font:700 11px 'Inter',sans-serif;padding:5px 8px;border-radius:8px;cursor:pointer}
.rangebtn:hover{color:var(--text);border-color:var(--border-strong)}
.rangebtn.active{color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent2));border-color:transparent}
/* ---------- leaderboard ---------- */
.lb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(225px,1fr));gap:16px;margin-bottom:20px}
.lb-card{position:relative;background:var(--panel);border:1px solid var(--border);border-radius:18px;padding:18px;
backdrop-filter:blur(14px);cursor:pointer;transition:.2s;overflow:hidden}
.lb-card:hover{transform:translateY(-3px);border-color:var(--border-strong);box-shadow:var(--shadow)}
.lb-top{display:flex;align-items:center;gap:10px;margin-bottom:14px}
.medal{font-size:20px;min-width:26px}
.lb-emoji{font-size:24px}
.lb-name{font-weight:700;font-size:15px;font-family:'Space Grotesk'}
.lb-blurb{font-size:11.5px;color:var(--muted);line-height:1.4;margin-top:2px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.lb-mid{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:12px}
.lb-eq{font-size:26px;font-weight:700;font-family:'Space Grotesk';letter-spacing:-.5px}
.lb-ret{font-size:15px;font-weight:700;font-family:'Space Grotesk'}
.lb-foot{display:flex;align-items:center;justify-content:space-between;gap:8px}
.rankpill{position:absolute;top:14px;right:14px;font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;background:rgba(255,255,255,.07);border:1px solid var(--border);color:var(--muted)}
/* ---------- segmented agent selector ---------- */
.seg{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:18px}
.segbtn{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:600;padding:8px 13px;border-radius:11px;cursor:pointer;
background:rgba(255,255,255,.05);border:1px solid var(--border);color:var(--muted);transition:.16s;user-select:none}
.segbtn:hover{color:var(--text);border-color:var(--border-strong)}
.segbtn.active{color:#fff;border-color:transparent}
.agent-brief{display:grid;grid-template-columns:1.2fr repeat(5,minmax(105px,.5fr));gap:14px;margin-bottom:18px}
.brief-main{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;backdrop-filter:blur(14px)}
.brief-title{display:flex;align-items:center;gap:10px;font-family:'Space Grotesk';font-weight:700;font-size:18px;margin-bottom:7px}
.brief-main p{margin:0;color:#cdd6e8;font-size:13.5px}
.brief-mini{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:14px;padding:14px}
.brief-mini .k{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
.brief-mini .v{margin-top:5px;font-size:14px;font-weight:700}
.card-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.select{appearance:none;border:1px solid var(--border);border-radius:9px;background:rgba(255,255,255,.05);color:var(--text);
font:600 12.5px 'Inter',sans-serif;padding:7px 28px 7px 10px;cursor:pointer}
.select:focus{outline:1px solid var(--accent)}
.loading-empty{display:grid;gap:10px;place-items:center;padding:28px 12px;text-align:center}
.loading-ring{width:34px;height:34px;border-radius:50%;border:3px solid rgba(255,255,255,.12);border-top-color:var(--accent);animation:spin .9s linear infinite}
.empty-copy{max-width:420px;color:var(--muted);font-size:13.5px}
@keyframes spin{to{transform:rotate(360deg)}}
.report-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(255px,1fr));gap:14px}
.report{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:15px;padding:16px;border-top:3px solid var(--accent)}
.report-title{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
.report-name{font-family:'Space Grotesk';font-weight:700}
.report-rank{font-size:11px;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:3px 8px;white-space:nowrap}
.report p{margin:0 0 9px;color:#cdd6e8;font-size:13px;line-height:1.5}
.report p:last-child{margin-bottom:0}
.report b{color:var(--text)}
/* ---------- suggestions ---------- */
.filterbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}
.chip{font-size:13px;font-weight:600;padding:7px 14px;border-radius:999px;cursor:pointer;background:rgba(255,255,255,.05);border:1px solid var(--border);color:var(--muted);transition:.16s;user-select:none}
.chip:hover{border-color:var(--accent);color:var(--text)}
.chip.active{background:linear-gradient(135deg,var(--accent),var(--accent2));border-color:transparent;color:#fff}
.chip .ct{opacity:.75;font-size:11px;margin-left:5px}
.sug-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media (max-width:760px){.sug-grid{grid-template-columns:1fr}}
.sug{position:relative;background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;border-left:4px solid var(--accent);backdrop-filter:blur(14px);transition:.2s}
.sug:hover{transform:translateY(-3px);border-color:var(--border-strong);box-shadow:var(--shadow)}
.sug .q{font-weight:600;font-size:15px;line-height:1.35;margin-top:6px}
.sug .event{color:var(--muted);font-size:12px;margin:3px 0 12px}
.sug-row{display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-bottom:9px}
.cat-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 9px;border-radius:7px}
.pill{font-size:12px;font-weight:700;padding:4px 11px;border-radius:999px}
.pill.YES{background:rgba(52,211,153,.16);color:var(--green)}
.pill.NO{background:rgba(251,113,133,.16);color:var(--red)}
.conv-bar{flex:1;min-width:90px;height:7px;background:rgba(255,255,255,.07);border-radius:999px;overflow:hidden}
.conv-bar>span{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--green))}
.rationale{font-size:13px;color:#cdd6e8;margin:10px 0}
.drivers{display:flex;gap:6px;flex-wrap:wrap}
.driver{font-size:11px;background:rgba(255,255,255,.05);border:1px solid var(--border);padding:3px 9px;border-radius:7px;color:var(--muted)}
.metrics{display:flex;gap:16px;font-size:12px;color:var(--muted);margin-top:12px;flex-wrap:wrap}
.metrics b{color:var(--text)}
a.market-link{color:var(--accent);text-decoration:none;font-size:12px;font-weight:600}
a.market-link:hover{text-decoration:underline}
a.market-title{color:var(--text);text-decoration:none}
a.market-title:hover{color:#fff;text-decoration:underline;text-decoration-color:var(--accent)}
/* ---------- positions / log ---------- */
.pos{display:flex;justify-content:space-between;align-items:center;padding:13px 0;border-bottom:1px solid var(--border)}
.pos:last-child{border-bottom:none}
.pos .pq{font-size:13.5px;max-width:62%;font-weight:500}
.pos .sub{color:var(--muted);font-size:11.5px;margin-top:3px}
.right{text-align:right}
.log-item{font-size:12.5px;padding:9px 0;border-bottom:1px solid var(--border);color:#cdd6e8}
.log-item:last-child{border-bottom:none}
.badge{font-weight:700;font-size:10px;padding:2px 7px;border-radius:5px;margin-right:7px}
.badge.OPEN{background:rgba(124,140,255,.2);color:var(--accent)}
.badge.CLOSE{background:rgba(251,191,36,.2);color:var(--yellow)}
.badge.STOP{background:rgba(251,113,133,.2);color:var(--red)}
.badge.GAIN{background:rgba(52,211,153,.18);color:var(--green)}
.badge.EXIT{background:rgba(251,191,36,.18);color:var(--yellow)}
.log-date{color:var(--muted)}
.empty{color:var(--muted);font-size:13.5px;padding:14px 0;text-align:center}
@media (max-width:700px){
.app{padding:0 10px 42px;max-width:100%;overflow:hidden}
.topnav{display:grid;grid-template-columns:1fr;gap:12px;padding:14px;margin:10px 0 8px;border-radius:14px}
.brand-sub{white-space:normal}
.tabs{width:100%;padding-bottom:5px}
.tab{padding:8px 12px}
.nav-right{width:100%;margin-left:0;display:flex;align-items:stretch;flex-wrap:wrap;gap:8px}
.nav-right .status{flex:1 1 100%}
#statusText{max-width:none;white-space:normal}
.status{align-items:flex-start}
.btn{padding:9px 11px}
.section-title{font-size:19px}
.stats{grid-template-columns:1fr;gap:12px}
.chart-wrap{width:100%;margin-top:8px}
.legend{grid-template-columns:1fr}
.agent-brief{grid-template-columns:1fr}
.invest-grid{grid-template-columns:1fr}
.invest-agent{grid-template-columns:auto 1fr}
.invest-agent input{grid-column:1 / -1;width:100%}
.paper-grid{grid-template-columns:1fr}
.pos{align-items:flex-start;gap:12px}
.pos .pq{max-width:none}
.right{min-width:92px}
}
/* ---------- about ---------- */
.about-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;margin-bottom:8px}
.feature{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:20px;backdrop-filter:blur(14px)}
.feature .fic{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;font-size:20px;margin-bottom:12px;background:linear-gradient(135deg,rgba(124,140,255,.25),rgba(168,120,255,.18));border:1px solid var(--border)}
.feature h4{margin:0 0 7px;font-size:15px}
.feature p{margin:0;font-size:13px;color:var(--muted);line-height:1.55}
.steps{counter-reset:s;list-style:none;padding:0;margin:0}
.steps li{position:relative;padding:0 0 16px 44px;font-size:14px;color:#cdd6e8}
.steps li::before{counter-increment:s;content:counter(s);position:absolute;left:0;top:-2px;width:30px;height:30px;border-radius:9px;display:grid;place-items:center;font-weight:700;font-size:13px;color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent2))}
.disclaimer{border:1px solid rgba(251,191,36,.3);background:rgba(251,191,36,.07);border-radius:14px;padding:16px 18px;font-size:13px;color:#e6dcc2;margin-top:8px}
.invest-grid{display:grid;grid-template-columns:1.05fr .95fr;gap:18px}
.invest-agent{display:grid;grid-template-columns:auto 1fr auto;gap:13px;align-items:center;padding:14px 0;border-bottom:1px solid var(--border)}
.invest-agent:last-child{border-bottom:none}
.invest-agent input{width:112px;border:1px solid var(--border);border-radius:9px;background:rgba(255,255,255,.05);color:var(--text);font:700 13px 'Inter',sans-serif;padding:8px 10px}
.invest-agent input:focus{outline:1px solid var(--accent)}
.invest-name{font-family:'Space Grotesk';font-weight:700}
.invest-copy{color:var(--muted);font-size:12.5px;margin-top:2px}
.invest-total{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:14px}
.invest-num{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:14px;padding:14px}
.invest-num .k{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.6px}
.invest-num .v{font-family:'Space Grotesk';font-size:24px;font-weight:700;margin-top:3px}
.checklist{list-style:none;margin:0;padding:0;display:grid;gap:10px}
.checklist li{display:flex;gap:9px;color:#cdd6e8;font-size:13.5px}
.checklist li::before{content:"";width:9px;height:9px;border-radius:50%;margin-top:6px;background:var(--accent);box-shadow:0 0 0 4px rgba(124,140,255,.12);flex:0 0 auto}
.exposure-row{display:flex;justify-content:space-between;gap:12px;padding:10px 0;border-bottom:1px solid var(--border);font-size:13px}
.exposure-row:last-child{border-bottom:none}
.risk-note{border:1px solid rgba(251,113,133,.28);background:rgba(251,113,133,.08);border-radius:14px;padding:14px 16px;font-size:13px;color:#f4c6cf}
.paper-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:18px}
.account-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
.input{border:1px solid var(--border);border-radius:9px;background:rgba(255,255,255,.05);color:var(--text);font:600 13px 'Inter',sans-serif;padding:9px 10px;min-width:0}
.input:focus{outline:1px solid var(--accent)}
.trade-list{display:grid;gap:12px}
.trade-card{border:1px solid var(--border);border-radius:14px;background:rgba(255,255,255,.035);padding:14px}
.trade-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:8px}
.trade-title{font-weight:700;font-size:13.5px;line-height:1.35}
.trade-actions{display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-top:10px}
.trade-actions .input{width:98px}
.trade-meta{display:flex;gap:10px;flex-wrap:wrap;color:var(--muted);font-size:11.5px}
.exact-table{display:grid;gap:10px}
/* ---------- misc ---------- */
.toast{position:fixed;bottom:24px;right:24px;z-index:100;background:rgba(18,24,40,.92);border:1px solid var(--accent);padding:13px 19px;border-radius:13px;font-size:14px;opacity:0;transform:translateY(12px);transition:.28s;pointer-events:none;max-width:360px;backdrop-filter:blur(12px);box-shadow:var(--shadow)}
.toast.show{opacity:1;transform:translateY(0)}
footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color:var(--muted);font-size:12.5px;text-align:center}
::-webkit-scrollbar{width:11px;height:11px}
::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:6px;border:3px solid transparent;background-clip:padding-box}
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.2);background-clip:padding-box}
</style>
</head>
<body>
<div class="bg" aria-hidden="true">
<div class="orb a"></div><div class="orb b"></div><div class="orb c"></div>
<svg class="chartline" viewBox="0 0 1440 240" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="cl" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#7c8cff" stop-opacity="0.28"/><stop offset="100%" stop-color="#7c8cff" stop-opacity="0"/>
</linearGradient></defs>
<path d="M0,170 C120,150 180,110 300,120 C420,130 470,70 600,90 C720,108 800,40 940,70 C1080,100 1160,150 1280,120 C1360,100 1410,130 1440,120 L1440,240 L0,240 Z" fill="url(#cl)"/>
<path d="M0,170 C120,150 180,110 300,120 C420,130 470,70 600,90 C720,108 800,40 940,70 C1080,100 1160,150 1280,120 C1360,100 1410,130 1440,120" fill="none" stroke="#7c8cff" stroke-opacity="0.5" stroke-width="2"/>
</svg>
</div>
<div class="app">
<nav class="topnav">
<div class="brand">
<div class="logo">🏆</div>
<div><div class="brand-name">Polymarket Arena</div><div class="brand-sub">10 agents · strategies, copycats &amp; whales</div></div>
</div>
<div class="tabs" id="tabs">
<button class="tab" data-tab="overview">Overview</button>
<button class="tab" data-tab="leaderboard">Leaderboard</button>
<button class="tab" data-tab="suggestions">Suggestions</button>
<button class="tab" data-tab="portfolio">Portfolios</button>
<button class="tab" data-tab="invest">Invest</button>
<button class="tab" data-tab="paper">Paper Trade</button>
<button class="tab" data-tab="about">About</button>
</div>
<div class="nav-right">
<span class="status"><span class="dot" id="statusDot"></span><span id="statusText">idle</span></span>
<button id="runBtn" class="btn primary">Run cycle</button>
<button id="copyStateBtn" class="btn ghost">Copy Sync</button>
<button id="importStateBtn" class="btn ghost">Import Sync</button>
<button id="resetBtn" class="btn ghost">Reset</button>
</div>
</nav>
<!-- ============ OVERVIEW ============ -->
<section class="tabpanel" data-tab="overview">
<div class="hero">
<svg class="hero-art" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs><linearGradient id="ha" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#7c8cff" stop-opacity=".0"/><stop offset="1" stop-color="#a878ff" stop-opacity=".5"/></linearGradient></defs>
<g fill="none" stroke-width="2.5">
<path d="M20,250 L120,160 L200,200 L290,80 L380,40" stroke="#34d399"/>
<path d="M20,255 L120,210 L200,150 L290,170 L380,120" stroke="#fbbf24" opacity=".8"/>
<path d="M20,248 L120,230 L200,235 L290,210 L380,200" stroke="#fb7185" opacity=".7"/>
</g>
<g fill="#d9b4ff"><circle cx="380" cy="40" r="6"/></g>
</svg>
<h1>Ten AI agents race to <span class="grad">beat the market</span></h1>
<p>Five in-house strategies, a copycat that shadows the leader, and four agents that mirror the live positions of the top traders on Polymarket — all paper-trading for the top of the leaderboard.</p>
<div class="badges">
<span class="hbadge">🏆 10 competing agents</span>
<span class="hbadge">🐋 Copies real Polymarket whales</span>
<span class="hbadge">🐒 Copycat meta-agent</span>
<span class="hbadge">🔴 Live data</span>
</div>
</div>
<div class="stats" id="stats"></div>
<div class="grid2">
<div class="card hoverable">
<div class="card-h"><h3>Agent Returns</h3><div><span class="small muted" id="lastRun"></span><div class="rangebar" data-chart-ranges></div></div></div>
<div class="chart-wrap"><svg id="chart" viewBox="0 0 960 320" preserveAspectRatio="xMidYMid meet"></svg></div>
<div class="legend" id="comboLegend"></div>
<div class="small muted" style="margin-top:10px">Use the range buttons to zoom from one week to all history. New live points are added whenever you click "Run cycle"; the dashed line tracks the average agent return.</div>
</div>
<div class="card hoverable">
<div class="card-h"><h3>Top Picks Today</h3><span class="small muted">highest conviction</span></div>
<div id="topPicks"></div>
</div>
</div>
</section>
<!-- ============ LEADERBOARD ============ -->
<section class="tabpanel" data-tab="leaderboard">
<div class="section-title">🏆 Leaderboard <span class="small muted" id="lbSub" style="font-weight:400"></span></div>
<div class="lb-grid" id="leaderboard"></div>
<div class="card">
<div class="card-h"><h3>Returns — all agents</h3><div><span class="small muted">$10,000 start each</span><div class="rangebar" data-chart-ranges></div></div></div>
<div class="chart-wrap"><svg id="chart2" viewBox="0 0 960 320" preserveAspectRatio="xMidYMid meet"></svg></div>
<div class="legend" id="comboLegend2"></div>
<div class="small muted" style="margin-top:10px">Past week is a backtest. After that, each hourly run adds another return snapshot for every agent.</div>
</div>
</section>
<!-- ============ SUGGESTIONS ============ -->
<section class="tabpanel" data-tab="suggestions">
<div class="section-title">💡 Hourly Suggestions <span class="small muted" id="sugDate" style="font-weight:400"></span></div>
<div class="card">
<div class="filterbar" id="filterbar"></div>
<div class="small muted" id="focusNote" style="margin-top:6px"></div>
</div>
<div class="sug-grid" id="suggestions"></div>
</section>
<!-- ============ PORTFOLIOS ============ -->
<section class="tabpanel" data-tab="portfolio">
<div class="section-title">🤖 Agent Portfolios</div>
<div class="seg" id="agentSel"></div>
<div class="agent-brief" id="agentBrief"></div>
<div class="stats" id="statsPf"></div>
<div class="grid2">
<div class="card"><div class="card-h"><h3>Open Positions</h3><div class="card-tools"><select class="select" id="positionSort" aria-label="Sort open positions">
<option value="pnl_desc">Best P&amp;L</option>
<option value="pnl_asc">Worst P&amp;L</option>
<option value="value_desc">Largest Value</option>
<option value="newest">Newest</option>
<option value="category">Category</option>
</select></div></div><div id="positions" aria-live="polite"></div></div>
<div class="card"><div class="card-h"><h3>Activity Log</h3></div><div id="history"></div></div>
</div>
<div class="card">
<div class="card-h"><h3>Hourly Agent Reports</h3><span class="small muted" id="reportDate"></span></div>
<div class="report-grid" id="agentReports"></div>
</div>
</section>
<!-- ============ INVEST ============ -->
<section class="tabpanel" data-tab="invest">
<div class="section-title">💸 Invest in Agents</div>
<div class="invest-grid">
<div class="card">
<div class="card-h"><h3>Allocation draft</h3><span class="small muted">planning only</span></div>
<div class="invest-total" id="investTotals"></div>
<div id="investAgents"></div>
</div>
<div>
<div class="card">
<div class="card-h"><h3>How real money should work</h3></div>
<ol class="steps">
<li><b>User owns funds</b> — connect a wallet or regulated brokerage account; the site should not custody deposits directly.</li>
<li><b>Consent per agent</b> — users choose which agents can trade and the maximum amount each may control.</li>
<li><b>Execution layer</b> — agent signals become signed user orders through Polymarket's trading flow or a compliant partner.</li>
<li><b>Risk controls</b> — stop-loss, gain-stop, exposure caps, market exclusions, and emergency pause stay visible before any order is sent.</li>
</ol>
</div>
<div class="card">
<div class="card-h"><h3>Compliance checklist</h3></div>
<ul class="checklist">
<li>Eligibility, location, age, KYC, and sanctions screening before real trading.</li>
<li>Clear disclosure that prediction markets are risky and users can lose the full amount committed.</li>
<li>No pooled customer funds unless handled by a properly regulated structure.</li>
<li>Audit logs for every signal, user approval, order, fill, stop, and withdrawal.</li>
</ul>
</div>
<div class="card">
<div class="card-h"><h3>Execution status</h3><span class="small muted">not enabled</span></div>
<p class="muted" style="margin-top:0">Real orders require user-signed trading credentials and an eligible account. The current site only creates an allocation draft.</p>
<div class="card-tools">
<a class="btn ghost" href="https://docs.polymarket.com/trading/overview" target="_blank" rel="noopener">Trading docs</a>
<a class="btn ghost" href="https://polymarket.com/terms-of-use" target="_blank" rel="noopener">Terms</a>
<button class="btn primary" disabled>Wallet trading locked</button>
</div>
</div>
<div class="risk-note">
<b>Important:</b> this site still does not move money or place orders. This tab is the bridge design: it lets people plan allocations and shows the safe path toward wallet-based or regulated real-money execution.
</div>
</div>
</div>
<div class="card" style="margin-top:18px">
<div class="card-h"><h3>Exact trades agents are holding</h3><span class="small muted">side, size, price, link</span></div>
<div class="exact-table" id="exactTrades"></div>
</div>
</section>
<!-- ============ PAPER TRADING ============ -->
<section class="tabpanel" data-tab="paper">
<div class="section-title">🧾 Paper Trading Accounts</div>
<div class="paper-grid">
<div>
<div class="card">
<div class="card-h"><h3>Account</h3><span class="small muted">browser paper money</span></div>
<div class="account-row">
<select class="select" id="paperAccountSelect" aria-label="Paper account"></select>
<input class="input" id="paperAccountName" placeholder="New account name" />
<button class="btn primary" id="createPaperAccountBtn">Create</button>
</div>
<div class="stats" id="paperStats"></div>
<div class="card-tools">
<select class="select" id="copyAgentSelect" aria-label="Agent to copy"></select>
<button class="btn" id="copyAgentBtn">Copy agent exactly</button>
</div>
<p class="small muted" style="margin:12px 0 0">Copying an agent replaces this paper account with that agent's current cash and open positions. No real orders are placed.</p>
</div>
<div class="card">
<div class="card-h"><h3>Paper positions</h3></div>
<div id="paperPositions"></div>
</div>
</div>
<div>
<div class="card">
<div class="card-h"><h3>Browse Polymarket ideas</h3><span class="small muted">place paper bets</span></div>
<div class="filterbar" id="paperFilterbar"></div>
<div class="trade-list" id="paperMarketList"></div>
</div>
<div class="card">
<div class="card-h"><h3>Paper activity</h3></div>
<div id="paperHistory"></div>
</div>
</div>
</div>
</section>
<!-- ============ ABOUT ============ -->
<section class="tabpanel" data-tab="about">
<div class="section-title">️ The competition</div>
<p class="muted" style="margin-top:-6px;max-width:820px">Every agent starts with <b>$10,000</b> in paper money. The five in-house strategies trade the same hourly suggestions their own way; the <b>Copycat</b> shadows whichever strategy is winning; and the four leading-account copy agents mirror live positions from high-profit real traders on Polymarket (auto-discovered from its public leaderboard). All mark to market hourly and climb — or sink — the board.</p>
<div class="about-grid" id="aboutAgents"></div>
<div class="card" style="margin-top:16px">
<div class="card-h"><h3>The hourly cycle</h3></div>
<ol class="steps">
<li><b>Fetch</b> — page through hundreds of live markets and tag each by category.</li>
<li><b>Score</b> — rank every market 0100 by conviction and estimate its edge vs. a fair-value model.</li>
<li><b>Suggest</b> — surface the strongest picks per category with a plain-English rationale.</li>
<li><b>Compete</b> — the strategy agents trade the same suggestions their own way, stop-loss weak positions, scale out through gain-stop tiers, exit stale or fading trades, then snapshot equity.</li>
</ol>
</div>
<div class="card">
<div class="card-h"><h3>Device sync</h3></div>
<p class="muted" style="margin:0">Portfolios, returns, reports, suggestions, and chart history now sync through the site automatically. <b>Copy Sync</b> and <b>Import Sync</b> remain as a fallback for moving the exact state between devices.</p>
</div>
<div class="disclaimer">⚠️ <b>Paper trading only — not financial advice.</b> Nothing here places real orders or moves money. The analysis is a transparent heuristic. Portfolios sync through the site when cloud state is available, with this browser keeping a local backup.</div>
</section>
<footer>
Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
<a class="market-link" href="https://github.com/theodore-song/polymarket-analyst" target="_blank" rel="noopener">Source on GitHub</a>
</footer>
</div>
<script>
"use strict";
/* ============================================================
Polymarket Arena — fully client-side.
Fetches live markets, scores them, and runs ten competing
paper-trading agents. State syncs through /api/state when configured and uses localStorage as the on-device cache.
============================================================ */
const GAMMA = "https://gamma-api.polymarket.com";
const STARTING_BALANCE = 10000;
const STOP_LOSS_TIERS = [
{id:"15",drawdown:0.15,sellFrac:0.33,label:"-15%"},
{id:"30",drawdown:0.30,sellFrac:0.33,label:"-30%"},
{id:"45",drawdown:0.45,sellFrac:1.00,label:"-45%"},
];
const GAIN_STOP_TIERS = [
{id:"75",gain:0.75,multiple:1.75,sellFrac:0.25,label:"+75%"},
{id:"150",gain:1.50,multiple:2.50,sellFrac:0.25,label:"+150%"},
{id:"300",gain:3.00,multiple:4.00,sellFrac:0.25,label:"+300%"},
];
const EXIT_STALE_DAYS = 14;
const EXIT_RESOLUTION_DAYS = 2;
const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v2";
const FOCUS_KEY = "pma_focus_v1";
const VIEW_KEY = "pma_view_v1";
const PF_SORT_KEY = "pma_portfolio_sort_v1";
const CHART_RANGE_KEY = "pma_chart_range_v1";
const INVEST_KEY = "pma_invest_allocations_v1";
const PAPER_KEY = "pma_paper_accounts_v1";
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
const SYNC_KEYS=[AGENTS_KEY,SUG_KEY,FOCUS_KEY,VIEW_KEY,PF_SORT_KEY,CHART_RANGE_KEY,INVEST_KEY,PAPER_KEY];
const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff",
"Pop Culture":"#c77dff",Other:"#94a1bb",All:"#7c8cff",Copy:"#22d3ee"};
const DATA_API = "https://data-api.polymarket.com";
const LB_API = "https://lb-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
const setFocus = (c) => localStorage.setItem(FOCUS_KEY, c);
/* ---------- the ten competing agents ----------
Each shares the same hourly suggestions but ranks/sizes them differently. */
const AGENTS = [
{id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy",
blurb:"Looks for the widest gap between the model's fair value and the current market price. It prefers trades where the crowd appears too pessimistic or too optimistic, then sizes positions moderately so one bad read does not dominate the portfolio.",
rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction),
maxNew:4, maxFrac:0.10, minConv:45, kelly:0.25},
{id:"momentum", name:"Momentum Chaser", emoji:"🚀", color:"#34d399", kind:"strategy",
blurb:"Chases markets where attention is accelerating. It weights conviction by fresh 24-hour volume, so it tends to enter fast-moving events with strong liquidity and recent trader interest.",
rank:(s)=>[...s].sort((a,b)=>(b.volume_24hr*(b.conviction+20))-(a.volume_24hr*(a.conviction+20))),
maxNew:5, maxFrac:0.12, minConv:40, kelly:0.30},
{id:"favorite", name:"Favorite Backer", emoji:"🛡️", color:"#38d2e6", kind:"strategy",
blurb:"A lower-drama agent that only considers outcomes already priced as favorites. It tries to grind out steadier returns by backing high-probability markets when the model still sees a useful edge.",
rank:(s)=>[...s].filter(x=>x.entry_price>=0.6).sort((a,b)=>b.entry_price-a.entry_price||b.conviction-a.conviction),
maxNew:4, maxFrac:0.14, minConv:38, kelly:0.40},
{id:"longshot", name:"Longshot Hunter", emoji:"🎰", color:"#fbbf24", kind:"strategy",
blurb:"The swing-for-upside strategy. It hunts cheaper contracts that the model thinks are being ignored, accepting more volatility in exchange for the chance that one mispriced longshot rerates sharply.",
rank:(s)=>[...s].filter(x=>x.entry_price<=0.4).sort((a,b)=>a.entry_price-b.entry_price||b.conviction-a.conviction),
maxNew:5, maxFrac:0.08, minConv:38, kelly:0.20},
{id:"diversifier", name:"The Diversifier", emoji:"🌐", color:"#c77dff", kind:"strategy",
blurb:"Builds a broad basket instead of making a few concentrated bets. It spreads smaller, flatter positions across many high-conviction ideas to reduce single-market risk and show how the whole suggestion pool performs.",
rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction),
maxNew:8, maxFrac:0.05, minConv:35, kelly:0.15, flat:true},
{id:"copycat", name:"The Copycat", emoji:"🐒", color:"#ec4899", kind:"copycat",
blurb:"A meta-agent that does not pick markets directly. It watches the in-house strategies, identifies the current leader, and mirrors that leader's open book so the site can test whether copying the winner keeps working."},
{id:"whale1", name:"Apex Mirror", emoji:"🐋", color:"#2dd4bf", kind:"whale", slot:0,
blurb:"Copies one of the highest-profit real Polymarket traders discovered from the public leaderboard. It mirrors that trader's largest live positions, then applies the same paper-trading risk rules as the other agents."},
{id:"whale2", name:"Deepwater Echo", emoji:"🐳", color:"#fb923c", kind:"whale", slot:1,
blurb:"Tracks a second leaderboard trader so the race is not dependent on a single whale. Its portfolio shows how another successful wallet's live exposure would behave inside the same $10,000 paper bankroll."},
{id:"whale3", name:"Signal Shark", emoji:"🦈", color:"#a3e635", kind:"whale", slot:2,
blurb:"Follows a third high-profit trader with a separate set of live holdings. This agent is useful for comparing whether different whale styles cluster around the same markets or take totally different risks."},
{id:"whale4", name:"Tide Rider", emoji:"🐡", color:"#60a5fa", kind:"whale", slot:3,
blurb:"Rounds out the whale bench by mirroring a fourth public leaderboard trader. It gives the competition another real-world reference point alongside the model-driven strategies."},
];
const agentById = (id) => AGENTS.find(a=>a.id===id) || AGENTS[0];
/* ---------- small utils ---------- */
const $ = (id) => document.getElementById(id);
const fmtUSD = (n) => "$" + Number(n).toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2});
const fmtPct = (n) => (n>=0?"+":"") + Number(n).toFixed(2) + "%";
const signClass = (n) => (n>0?"pos-val":n<0?"neg-val":"");
const marketSearchUrl=(q)=>`https://polymarket.com/search?query=${encodeURIComponent(q||"")}`;
const positionUrl=(pos)=>pos&&pos.url?pos.url:marketSearchUrl(pos&&pos.question);
function partsInLocalTime(d=new Date()){
const parts=new Intl.DateTimeFormat("en-CA",{timeZone:"America/Toronto",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",hourCycle:"h23"}).formatToParts(d);
return Object.fromEntries(parts.map(p=>[p.type,p.value]));
}
const todayStr = () => {const p=partsInLocalTime();return `${p.year}-${p.month}-${p.day}`;};
const currentCycleHour = () => {const p=partsInLocalTime();return `${p.year}-${p.month}-${p.day}T${p.hour}`;};
function cycleHourFromIso(iso){
const d=new Date(iso);
if(isNaN(d))return null;
const p=partsInLocalTime(d);return `${p.year}-${p.month}-${p.day}T${p.hour}`;
}
const nowIso = () => new Date().toISOString();
const cycleIso = () => SIM_DAY ? new Date(SIM_DAY+"T12:00:00Z").toISOString() : nowIso();
let SIM_DAY = null; // set during backtest so logs/snapshots use the simulated date
let SNAP_TS = null; // set during a live cycle so every agent gets the same chart x-value
const logDay = () => SIM_DAY || todayStr();
function toast(msg){
let t=document.querySelector(".toast");
if(!t){t=document.createElement("div");t.className="toast";document.body.appendChild(t);}
t.textContent=msg;t.classList.add("show");
clearTimeout(t._h);t._h=setTimeout(()=>t.classList.remove("show"),3600);
}
function setStatus(text,live){$("statusText").textContent=text;$("statusDot").classList.toggle("live",!!live);}
/* ---------- Polymarket client ---------- */
function parseJsonField(v){if(v==null)return [];if(Array.isArray(v))return v;try{return JSON.parse(v);}catch(e){return [];}}
function toNum(v,d=0){const n=Number(v);return Number.isFinite(n)?n:d;}
function daysUntil(iso){if(!iso)return null;const dt=new Date(iso);if(isNaN(dt))return null;return (dt-new Date())/86400000;}
const CATEGORY_RULES=[
["Politics",["politics","election","elections","us-politics","geopolitics","trump","world-leaders","government","congress","policy","biden","2026-election","democrats","republicans"]],
["Crypto",["crypto","bitcoin","ethereum","btc","eth","solana","memecoins","defi","stablecoin","xrp"]],
["Sports",["sports","soccer","football","nba","nfl","mlb","nhl","tennis","basketball","baseball","ufc","boxing","cricket","golf","f1","fifa-world-cup","games","tournament-futures","epl"]],
["Economy",["economy","business","fed","inflation","interest-rates","gdp","jobs","recession","stocks","earnings","financials","tariffs"]],
["Pop Culture",["pop-culture","entertainment","movies","music","tv","awards","celebrity","gaming","ai"]],
];
function classifyCategory(tags){
const slugs=tags.map(t=>(t.slug||t.label||"").toLowerCase());
for(const [cat,keys] of CATEGORY_RULES){if(slugs.some(s=>keys.includes(s)))return cat;}
return "Other";
}
function normalizeMarket(raw){
const outcomes=parseJsonField(raw.outcomes);
const prices=parseJsonField(raw.outcomePrices).map(p=>toNum(p));
if(outcomes.length!==2||prices.length!==2)return null;
if(raw.acceptingOrders===false||raw.closed)return null;
const yes=prices[0]; if(yes<=0||yes>=1)return null;
const events=raw.events||[]; const ev=events.length?events[0]:{};
const tags=Array.isArray(raw.tags)?raw.tags:[];
return {category:classifyCategory(tags),tags:tags.map(t=>t.label).filter(Boolean).slice(0,4),
id:String(raw.id),question:(raw.question||"").trim(),event:(ev.title||"").trim(),
clob_token_ids:parseJsonField(raw.clobTokenIds).map(String),
yes_price:+yes.toFixed(4),no_price:+prices[1].toFixed(4),
volume:toNum(raw.volumeNum||raw.volume),volume_24hr:toNum(raw.volume24hr),volume_1wk:toNum(raw.volume1wk),
liquidity:toNum(raw.liquidityNum||raw.liquidity),end_date:raw.endDateIso||raw.endDate,
days_to_resolution:daysUntil(raw.endDate),image:raw.image||"",
url:ev.slug?`https://polymarket.com/event/${ev.slug}`:""};
}
async function fetchMarkets(pages=4,perPage=100){
const all=[];
for(let i=0;i<pages;i++){
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",
limit:String(perPage),offset:String(i*perPage),order:"volume24hr",ascending:"false"});
const r=await fetch(`${GAMMA}/markets?${params}`);
if(!r.ok){if(i===0)throw new Error("Polymarket API "+r.status);break;}
const raw=await r.json(); if(!raw.length)break; all.push(...raw);
}
return all.map(normalizeMarket).filter(m=>m&&m.question);
}
async function fetchMarketPrice(id){
try{const r=await fetch(`${GAMMA}/markets/${id}`);if(!r.ok)return null;return normalizeMarket(await r.json());}
catch(e){return null;}
}
/* ---------- Analysis engine ---------- */
const W={LIQ:0.25,MOM:0.25,MIS:0.30,TIME:0.20};
const MIN_VOLUME=20000;
const SUGGESTION_TOTAL=40;
const SUGGESTION_PER_CATEGORY=10;
const VISIBLE_SUGGESTIONS=30;
const clamp=(x,a,b)=>Math.max(a,Math.min(b,x));
function liquiditySignal(m){const vol=Math.min(1,Math.log10(m.volume+1)/6.0);const liq=Math.min(1,Math.log10(m.liquidity+1)/5.3);return 0.6*vol+0.4*liq;}
function momentumSignal(m){const da=m.volume_1wk?m.volume_1wk/7:0;if(da<=0)return m.volume_24hr>0?0.3:0;return clamp((m.volume_24hr/da-0.5)/2.0,0,1);}
function fairValue(m){const p=m.yes_price,mom=momentumSignal(m),liq=liquiditySignal(m);
if(p>0.92)return Math.min(0.995,p+0.04*liq*(1-p)*4);
if(p<0.08)return Math.max(0.005,p-0.20*p);
return clamp(p+(mom-0.5)*0.06,0.02,0.98);}
function timingSignal(d){if(d==null)return 0.4;if(d<1)return 0.1;if(d<=3)return 0.5;if(d<=90)return 1.0-(d-3)/87.0*0.4;return 0.35;}
function analyzeMarket(m){
if(m.volume<MIN_VOLUME)return null;
const p=m.yes_price,fair=fairValue(m),edgeYes=fair-p,edge=Math.abs(edgeYes);
const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution);
const mis=Math.min(1,edge/0.10);
const conviction=+((W.LIQ*liq+W.MOM*mom+W.MIS*mis+W.TIME*timing)*100).toFixed(1);
let side,entry,rationale;
if(edgeYes>0.015){side="YES";entry=p;rationale=`Model fair value ${pct(fair)} vs market ${pct(p)} — YES looks underpriced by ~${(edgeYes*100).toFixed(1)}c.`;}
else if(edgeYes<-0.015){side="NO";entry=m.no_price;rationale=`Model fair value ${pct(fair)} vs market ${pct(p)} — YES looks overpriced, so NO at ${pct(m.no_price)} has the edge.`;}
else{side="HOLD";entry=p;rationale=`Price ${pct(p)} is close to model fair value ${pct(fair)}; no clear edge — watch only.`;}
const drivers=[];
if(liq>0.6)drivers.push("deep/liquid market");
if(mom>0.6)drivers.push("strong fresh volume (24h surge)");
if(timing>0.8)drivers.push("resolves in a good window");
if(mis>0.4)drivers.push("notable price/value gap");
if(!drivers.length)drivers.push("thin signal");
return {market_id:m.id,question:m.question,event:m.event,url:m.url,category:m.category,tags:m.tags,
clob_yes:(m.clob_token_ids||[])[0]||null,clob_no:(m.clob_token_ids||[])[1]||null,
yes_price:p,no_price:m.no_price,fair_value:+fair.toFixed(4),edge:+edgeYes.toFixed(4),
side,entry_price:+entry.toFixed(4),conviction,volume:m.volume,volume_24hr:m.volume_24hr,
days_to_resolution:m.days_to_resolution!=null?+m.days_to_resolution.toFixed(1):null,drivers,rationale};
}
const pct=(x)=>Math.round(x*100)+"%";
function suggestionBand(s){
if(s.entry_price<0.35)return "longshot";
if(s.entry_price>0.65)return "favorite";
return "balanced";
}
function generateSuggestions(markets,total=SUGGESTION_TOTAL,perCategory=SUGGESTION_PER_CATEGORY){
const actionable=markets.map(analyzeMarket).filter(Boolean).filter(a=>a.side!=="HOLD").sort((a,b)=>b.conviction-a.conviction);
const byCat={},byBucket={},seen=new Set(),out=[];
const add=(a,bucketCap=3)=>{
if(out.length>=total||seen.has(a.market_id))return false;
const c=a.category||"Other",bucket=`${c}:${a.side}:${suggestionBand(a)}`;
if((byCat[c]||0)>=perCategory||(byBucket[bucket]||0)>=bucketCap)return false;
byCat[c]=(byCat[c]||0)+1;byBucket[bucket]=(byBucket[bucket]||0)+1;seen.add(a.market_id);out.push(a);
return true;
};
for(const a of actionable)add(a,3); // first pass: category, side, and price-band diversity
for(const a of actionable)add(a,999); // second pass: fill remaining slots with the best leftovers
return out.sort((a,b)=>b.conviction-a.conviction);
}
/* ---------- Multi-agent store ---------- */
function defaultPortfolio(){return {cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],closed:[],history:[],snapshots:[],stopped:{},lastDecision:null};}
function defaultState(){const agents={};AGENTS.forEach(a=>agents[a.id]=defaultPortfolio());return {date:null,last_run:null,last_cycle_hour:null,agents,whales:{},copycatLeader:null,seeded:false};}
function loadState(){
let st; try{st=JSON.parse(localStorage.getItem(AGENTS_KEY));}catch(e){st=null;}
if(!st||!st.agents)st=defaultState();
AGENTS.forEach(a=>{
if(!st.agents[a.id])st.agents[a.id]=defaultPortfolio();
if(!st.agents[a.id].stopped)st.agents[a.id].stopped={};
if(!("lastDecision" in st.agents[a.id]))st.agents[a.id].lastDecision=null;
});
if(!st.whales)st.whales={};
if(!st.last_cycle_hour&&st.last_run)st.last_cycle_hour=cycleHourFromIso(st.last_run);
return st;
}
function saveState(st){localStorage.setItem(AGENTS_KEY,JSON.stringify(st));}
function loadSuggestions(){try{const s=localStorage.getItem(SUG_KEY);return s?JSON.parse(s):{date:null,suggestions:[]};}catch(e){return {date:null,suggestions:[]};}}
function saveSuggestions(sugs){const p={date:todayStr(),generated_at:nowIso(),suggestions:sugs};localStorage.setItem(SUG_KEY,JSON.stringify(p));return p;}
function collectSyncItems(){const items={};SYNC_KEYS.forEach(k=>{const v=localStorage.getItem(k);if(v!=null)items[k]=v;});return items;}
function applySyncItems(items){if(!items||typeof items!=="object")return false;SYNC_KEYS.forEach(k=>{if(items[k]!=null)localStorage.setItem(k,items[k]);});return true;}
function stateFromSyncItems(items){
try{return items&&items[AGENTS_KEY]?JSON.parse(items[AGENTS_KEY]):null;}
catch(e){return null;}
}
async function pushCloudState(force=false){
try{
const r=await fetch("/api/state",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({items:collectSyncItems(),force})});
if(r.status===409){
const d=await r.json().catch(()=>null);
if(d&&d.state&&d.state.items)applySyncItems(d.state.items);
else await loadAuthoritativeCloudState();
toast("Loaded the newer shared cloud result.");
renderAll();
return false;
}
if(!r.ok)throw new Error("cloud sync unavailable");
return true;
}catch(e){return false;}
}
async function pullCloudState(){
try{
const r=await fetch("/api/state",{cache:"no-store"});
if(!r.ok)return {ok:false,state:null};
const d=await r.json();
return {ok:true,state:(d&&d.state)||null};
}catch(e){}
return {ok:false,state:null};
}
async function loadAuthoritativeCloudState(){
const cloud=await pullCloudState();
if(!cloud.ok)return {ok:false,loaded:false};
const cloudState=cloud.state;
if(cloudState&&cloudState.items&&stateFromSyncItems(cloudState.items)){
applySyncItems(cloudState.items);
return {ok:true,loaded:true};
}
return {ok:true,loaded:false};
}
async function refreshFromCloudAndRender(silent=true){
const cloud=await loadAuthoritativeCloudState();
if(cloud.loaded){
renderAll();
setStatus("cloud synced",false);
if(!silent)toast("Shared cloud state refreshed.");
}else if(!cloud.ok){
setStatus("cloud sync unavailable",false);
if(!silent)toast("Cloud sync is unavailable. Results may be stale until it reconnects.");
}
return cloud;
}
/* ---------- Agent mechanics ---------- */
const equity=(p)=>p.cash+p.positions.reduce((s,x)=>s+x.shares*x.current_price,0);
const hasPosition=(p,id)=>p.positions.some(x=>x.market_id===id);
function recentReturnDelta(p){
const snaps=(p.snapshots||[]).slice(-3);
if(snaps.length<2)return 0;
return (snaps[snaps.length-1].return_pct||0)-(snaps[0].return_pct||0);
}
function adaptiveDecision(cfg,p,rank,total,leaderEq){
const eq=equity(p),ret=(eq/p.starting_balance-1)*100,trail=((leaderEq||eq)-eq)/p.starting_balance*100;
const trend=recentReturnDelta(p);
let minConv=cfg.minConv??0,maxNew=cfg.maxNew??12,maxFrac=cfg.maxFrac??0.34,reserve=0.05,mode="Balanced",reason="following its base strategy while watching the leaderboard.";
if(rank===1){
mode="Defend";minConv+=5;maxNew=Math.max(1,maxNew-1);maxFrac*=0.82;reserve=0.09;
reason="leading the race, so it raises standards and protects cash instead of overtrading.";
}else if(trail>6){
mode="Chase";minConv-=5;maxNew+=1;maxFrac*=1.18;reserve=0.035;
reason="far behind the leader, so it accepts a little more risk to find catch-up trades.";
}else if(trend>1.5){
mode="Press";minConv-=2;maxNew+=1;maxFrac*=1.08;reserve=0.04;
reason="recent momentum is positive, so it leans into its edge while the tape is friendly.";
}
if(ret<-5){
mode="Recover";minConv+=6;maxNew=Math.max(1,maxNew-1);maxFrac*=0.72;reserve=0.08;
reason="in drawdown, so it tightens entries, shrinks bets, and waits for stronger confirmation.";
}
if((p.cash/Math.max(eq,1))<0.12){maxNew=Math.max(1,Math.min(maxNew,2));reason+=" Cash is tight, so new entries are rationed.";}
const maxCap=cfg.kind==="whale"?0.34:0.18;
return {mode,reason,minConv:Math.max(0,Math.round(minConv)),maxNew:Math.max(1,Math.round(maxNew)),maxFrac:+Math.min(maxCap,Math.max(0.02,maxFrac)).toFixed(3),reserve};
}
const stopKey=(posOrId)=>typeof posOrId==="string"?posOrId:String(posOrId.asset||posOrId.market_id||"");
const hasStoppedToday=(p,posOrId)=>p.stopped&&p.stopped[stopKey(posOrId)]===logDay();
function rememberStop(p,pos){
if(!p.stopped)p.stopped={};
const key=stopKey(pos);
if(key)p.stopped[key]=logDay();
}
function doneStopLosses(pos){
return pos.stop_losses&&typeof pos.stop_losses==="object"?Object.assign({},pos.stop_losses):{};
}
function stopLossTarget(pos,tier){
const entry=Number(pos.entry_price);
if(Number.isFinite(entry)&&entry>0)return +(entry*(1-tier.drawdown)).toFixed(4);
if(pos.original_shares>0&&pos.original_cost>0)return +((pos.original_cost/pos.original_shares)*(1-tier.drawdown)).toFixed(4);
if(pos.shares>0&&pos.cost>0)return +((pos.cost/pos.shares)*(1-tier.drawdown)).toFixed(4);
return null;
}
function triggeredStopLosses(pos){
const done=doneStopLosses(pos);
return STOP_LOSS_TIERS.filter(t=>{
if(done[t.id])return false;
const target=stopLossTarget(pos,t);
return target!=null&&(pos.current_price||0)<=target;
});
}
function doneGainStops(pos){
const done=pos.gain_stops&&typeof pos.gain_stops==="object"?Object.assign({},pos.gain_stops):{};
if(pos.took_profit_at&&!done["75"]&&!done["150"]){
done["75"]=pos.took_profit_at;
done["150"]=pos.took_profit_at;
}
return done;
}
function gainStopTarget(pos,tier){
const entry=Number(pos.entry_price);
if(Number.isFinite(entry)&&entry>0)return +(entry*tier.multiple).toFixed(4);
if(pos.shares>0&&pos.cost>0)return +((pos.cost/pos.shares)*tier.multiple).toFixed(4);
return null;
}
function nextGainStop(pos){
if(!pos.cost||pos.cost<=0)return null;
const done=doneGainStops(pos);
return GAIN_STOP_TIERS.find(t=>!done[t.id])||null;
}
function triggeredGainStops(pos){
if(!pos.cost||pos.cost<=0)return [];
const done=doneGainStops(pos);
return GAIN_STOP_TIERS.filter(t=>{
if(done[t.id])return false;
const target=gainStopTarget(pos,t);
return target!=null&&target<=1&&(pos.current_price||0)>=target;
});
}
function gainStopLabel(pos){
const done=doneGainStops(pos),doneCount=GAIN_STOP_TIERS.filter(t=>done[t.id]).length;
const tier=nextGainStop(pos);
if(!tier)return "Gain stop: 75% sold, 25% left riding";
const target=gainStopTarget(pos,tier);
const prefix=doneCount?`Gain stop: ${doneCount*25}% sold; next `:"Gain stop: ";
if(target==null)return `${prefix}waiting for ${tier.label}`;
if(target>1)return `${prefix}${tier.label} unreachable from ${pct(pos.entry_price)} entry`;
const gap=Math.max(0,target-(pos.current_price||0));
return `${prefix}sell 25% at ${pct(target)} (${tier.label})${gap>0?` — ${(gap*100).toFixed(1)}c away`:" — armed"}`;
}
function stopLossLabel(pos){
const done=doneStopLosses(pos),doneCount=STOP_LOSS_TIERS.filter(t=>done[t.id]).length;
const tier=STOP_LOSS_TIERS.find(t=>!done[t.id]);
if(!tier)return "Stop loss: final tier fired";
const target=stopLossTarget(pos,tier);
const sold=doneCount===0?"":`${Math.round(doneCount*33)}% sold; next `;
return `Stop loss: ${sold}${tier.id==="45"?"sell all":"sell 33%"} at ${pct(target)} (${tier.label})`;
}
function stopOutPosition(p,pos){
const proceeds=+(pos.shares*pos.current_price).toFixed(2);
p.cash=+(p.cash+proceeds).toFixed(2);
pos.exit_price=pos.current_price;pos.closed_at=cycleIso();
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
p.closed.push(pos);rememberStop(p,pos);
p.history.push({date:logDay(),action:"STOP",question:pos.question,side:pos.side,
detail:`Stop-loss sold '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
}
function scaleStopLossPosition(p,pos,tier){
const originalShares=+(pos.original_shares||pos.shares).toFixed(2);
const originalCost=+(pos.original_cost||pos.cost).toFixed(2);
pos.original_shares=originalShares;
pos.original_cost=originalCost;
if(tier.sellFrac>=1||tier.id==="45"){
stopOutPosition(p,pos);
pos._closedByStop=true;
return;
}
const soldShares=+Math.min(pos.shares,originalShares*tier.sellFrac).toFixed(2);
if(soldShares<=0)return;
const proceeds=+(soldShares*pos.current_price).toFixed(2);
const realizedCost=+Math.min(pos.cost,originalCost*tier.sellFrac).toFixed(2);
const realizedPnl=+(proceeds-realizedCost).toFixed(2);
p.cash=+(p.cash+proceeds).toFixed(2);
pos.shares=+(pos.shares-soldShares).toFixed(2);
pos.cost=+(pos.cost-realizedCost).toFixed(2);
pos.value=+(pos.shares*pos.current_price).toFixed(2);
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
pos.stop_losses=doneStopLosses(pos);
pos.stop_losses[tier.id]=cycleIso();
p.history.push({date:logDay(),action:"STOP",question:pos.question,side:pos.side,
detail:`Stop-loss ${tier.label} sold 33% of '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (realized ${fmtUSD(realizedPnl)})`});
}
function closePosition(p,pos,reason,action="CLOSE"){
const proceeds=+(pos.shares*pos.current_price).toFixed(2);
p.cash=+(p.cash+proceeds).toFixed(2);
pos.exit_price=pos.current_price;pos.closed_at=cycleIso();
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
p.closed.push(pos);
p.history.push({date:logDay(),action,question:pos.question,side:pos.side,
detail:`${reason} '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
}
function takeProfitPosition(p,pos,tier){
const legacyMultiplier=pos.took_profit_at&&!pos.original_shares?2:1;
const originalShares=+(pos.original_shares||pos.shares*legacyMultiplier).toFixed(2);
const originalCost=+(pos.original_cost||pos.cost*legacyMultiplier).toFixed(2);
pos.original_shares=originalShares;
pos.original_cost=originalCost;
const soldShares=+Math.min(pos.shares,originalShares*tier.sellFrac).toFixed(2);
if(soldShares<=0)return;
const proceeds=+(soldShares*pos.current_price).toFixed(2);
const realizedCost=+Math.min(pos.cost,originalCost*tier.sellFrac).toFixed(2);
const realizedPnl=+(proceeds-realizedCost).toFixed(2);
p.cash=+(p.cash+proceeds).toFixed(2);
pos.shares=+(pos.shares-soldShares).toFixed(2);
pos.cost=+(pos.cost-realizedCost).toFixed(2);
pos.value=+(pos.shares*pos.current_price).toFixed(2);
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
pos.gain_stops=doneGainStops(pos);
pos.gain_stops[tier.id]=cycleIso();
p.history.push({date:logDay(),action:"GAIN",question:pos.question,side:pos.side,
detail:`Gain stop ${tier.label} sold 25% of '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (realized ${fmtUSD(realizedPnl)})`});
}
function daysHeld(pos){
if(!pos.opened_at)return 0;
const start=new Date(pos.opened_at).getTime(),end=new Date(cycleIso()).getTime();
if(!Number.isFinite(start)||!Number.isFinite(end))return 0;
return Math.max(0,(end-start)/86400000);
}
function exitReason(pos,fresh,analysis,cfg){
if(!analysis)return "Model can no longer score";
if(fresh.days_to_resolution!=null&&fresh.days_to_resolution<=EXIT_RESOLUTION_DAYS)return `Close before resolution (${fresh.days_to_resolution.toFixed(1)}d left)`;
if(daysHeld(pos)>=EXIT_STALE_DAYS)return `Stale exit after ${Math.floor(daysHeld(pos))} days`;
if(analysis.side==="HOLD")return "Edge faded";
if(analysis.side&&analysis.side!==pos.side)return `Model flipped to ${analysis.side}`;
const minExit=Math.max(25,(cfg&&cfg.minConv?cfg.minConv:35)-10);
if(analysis.conviction<minExit)return `Conviction faded to ${Math.round(analysis.conviction)}`;
return null;
}
function markToMarket(p,priceMap,cfg=null,{policyExits=false}={}){
const stillOpen=[];
for(const pos of p.positions){
const fresh=priceMap[pos.market_id];
if(fresh==null){
closePosition(p,pos,"Settled/removed market");
continue;
}
const price=pos.side==="YES"?fresh.yes_price:fresh.no_price;
pos.current_price=+price.toFixed(4);pos.value=+(pos.shares*price).toFixed(2);
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
for(const stopTier of triggeredStopLosses(pos))scaleStopLossPosition(p,pos,stopTier);
if(pos._closedByStop){delete pos._closedByStop;continue;}
for(const gainTier of triggeredGainStops(pos))takeProfitPosition(p,pos,gainTier);
if(policyExits){
const reason=exitReason(pos,fresh,analyzeMarket(fresh),cfg);
if(reason){closePosition(p,pos,reason,"EXIT");continue;}
}
stillOpen.push(pos);
}
p.positions=stillOpen;
}
function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds){
const d=decision||{minConv:cfg.minConv,maxNew:cfg.maxNew,maxFrac:cfg.maxFrac,reserve:0.05};
p.lastDecision=decision||null;
const cands=rankedSugs.filter(s=>(s.side==="YES"||s.side==="NO")&&s.conviction>=d.minConv
&&!hasPosition(p,s.market_id)&&!hasStoppedToday(p,s.market_id)&&(focus==="All"||!focus||s.category===focus));
const avoid=avoidMarketIds||new Set();
const ordered=cands.filter(s=>!avoid.has(s.market_id)).concat(cands.filter(s=>avoid.has(s.market_id)));
let opened=0,openedIds=[];
for(const s of ordered){
if(opened>=d.maxNew)break;
const eq=equity(p),investable=p.cash-eq*d.reserve;
if(investable<=50)break;
let frac;
if(cfg.flat){frac=d.maxFrac;}
else{const base=(s.conviction/100)*Math.min(1,Math.abs(s.edge)/0.10);frac=Math.min(d.maxFrac,cfg.kelly*base);}
let stake=Math.min(eq*frac,investable);
if(stake<50)continue;
const entry=s.entry_price; if(entry<=0||entry>=1)continue;
const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
if(cost>p.cash)continue;
p.cash=+(p.cash-cost).toFixed(2);
p.positions.push({market_id:s.market_id,question:s.question,side:s.side,shares,
token_id:(s.side==="YES"?s.clob_yes:s.clob_no)||null,
entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),cost,value:cost,
original_shares:shares,original_cost:cost,unrealized_pnl:0,conviction:s.conviction,category:s.category,opened_at:cycleIso(),url:s.url||"",
gain_stops:{},stop_losses:{}});
p.history.push({date:logDay(),action:"OPEN",question:s.question,side:s.side,
detail:`${decision?decision.mode+" mode — ":""}Bought ${shares} ${s.side} '${s.question.slice(0,40)}' @ ${pct(entry)} for ${fmtUSD(cost)}`});
opened++;openedIds.push(s.market_id);
}
return openedIds;
}
function recordSnapshot(p){
const eq=equity(p);
const snap={date:logDay(),timestamp:(SIM_DAY?new Date(SIM_DAY+"T12:00:00Z").toISOString():(SNAP_TS||nowIso())),cash:+p.cash.toFixed(2),
equity:+eq.toFixed(2),pnl:+(eq-p.starting_balance).toFixed(2),
return_pct:+((eq/p.starting_balance-1)*100).toFixed(2),open_positions:p.positions.length};
p.snapshots=SIM_DAY?p.snapshots.filter(s=>s.date!==snap.date):p.snapshots.filter(s=>s.timestamp!==snap.timestamp);
p.snapshots.push(snap);p.snapshots.sort((a,b)=>new Date(a.timestamp||a.date)-new Date(b.timestamp||b.date));
}
/* ---------- Copy-trading: Polymarket leaders ---------- */
const hasAsset=(p,a)=>p.positions.some(x=>x.asset===a);
async function fetchTokenPrice(asset){
try{const r=await fetch(`${CLOB}/midpoint?token_id=${asset}`);
if(r.ok){const d=await r.json();const m=parseFloat(d.mid);if(m>0&&m<1)return m;}}catch(e){}
return null;
}
async function fetchTraderPositions(wallet){
try{const r=await fetch(`${DATA_API}/positions?user=${wallet}&sizeThreshold=1&limit=80`);
if(!r.ok)return [];
const d=await r.json();
return d.filter(p=>(p.outcome==="Yes"||p.outcome==="No")&&p.curPrice>0.02&&p.curPrice<0.98&&!p.redeemable&&p.size*p.curPrice>=1)
.map(p=>({asset:String(p.asset),conditionId:p.conditionId,outcome:p.outcome,side:String(p.outcome).toUpperCase(),
curPrice:+p.curPrice,title:p.title||"market",value:p.size*p.curPrice,url:rawPositionUrl(p)}));
}catch(e){return [];}
}
function prettyName(n,w){
if(n&&!/^0x/i.test(n))return n; // real display name / pseudonym
return "0x"+String(w).slice(2,6)+"…"+String(w).slice(-4); // anonymous → short wallet
}
function rawPositionUrl(p){
const slug=p.eventSlug||p.event_slug||p.slug||p.marketSlug||p.market_slug;
return slug?`https://polymarket.com/event/${slug}`:marketSearchUrl(p.title||p.question||"");
}
async function discoverWhales(){
const picked=[];
try{
const r=await fetch(`${LB_API}/profit?window=all&limit=40`);
const leaders=r.ok?await r.json():[];
for(const L of leaders){
if(picked.length>=4)break;
const tps=await fetchTraderPositions(L.proxyWallet);
if(tps.length>=1)picked.push({wallet:L.proxyWallet,
name:prettyName(L.name||L.pseudonym,L.proxyWallet),profit:Math.round(L.amount||0)});
}
}catch(e){}
return picked;
}
async function runWhaleAgent(p,wallet,decision){
const d=decision||{maxNew:12,maxFrac:0.34,reserve:0.05};
p.lastDecision=decision||null;
// mark existing copied positions to market via CLOB token price
for(const pos of p.positions){
const pr=await fetchTokenPrice(pos.asset);
if(pr!=null){pos.current_price=+pr.toFixed(4);pos.value=+(pos.shares*pr).toFixed(2);pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);}
}
const tps=await fetchTraderPositions(wallet);
const held=new Set(tps.map(t=>t.asset));
const keep=[];
for(const pos of p.positions){
for(const stopTier of triggeredStopLosses(pos))scaleStopLossPosition(p,pos,stopTier);
if(pos._closedByStop){
delete pos._closedByStop;
}else if(!held.has(pos.asset)){
closePosition(p,pos,"Copied trader exited");
}else{
for(const gainTier of triggeredGainStops(pos))takeProfitPosition(p,pos,gainTier);
keep.push(pos);
}
}
p.positions=keep;
const targets=[...tps].sort((a,b)=>b.value-a.value).slice(0,d.maxNew); // copy their biggest bets, adapted by stance
const book=targets.reduce((s,t)=>s+t.value,0)||1;
for(const t of targets){
if(hasAsset(p,t.asset)||hasStoppedToday(p,t.asset))continue;
const eq=equity(p),investable=p.cash-eq*d.reserve; if(investable<=50)break;
const weight=t.value/book;
const stake=Math.min(eq*Math.min(d.maxFrac,weight),investable);
if(stake<40)continue;
const entry=t.curPrice; const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
if(cost>p.cash)continue;
p.cash=+(p.cash-cost).toFixed(2);
p.positions.push({asset:t.asset,token_id:t.asset,market_id:t.conditionId,question:t.title,side:t.side,category:"Copy",
shares,entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),cost,value:cost,
original_shares:shares,original_cost:cost,unrealized_pnl:0,conviction:null,opened_at:cycleIso(),url:t.url||marketSearchUrl(t.title),
gain_stops:{},stop_losses:{}});
p.history.push({date:logDay(),action:"OPEN",question:t.title,side:t.side,
detail:`Copied trade — ${shares} ${t.side} '${t.title.slice(0,36)}' @ ${pct(entry)} for ${fmtUSD(cost)}`});
}
recordSnapshot(p);
}
/* ---------- Copycat: shadow the leading in-house strategy ---------- */
function runCopycat(p,leader,priceMap){
p.lastDecision={mode:"Adaptive",reason:"watching the strategy leaderboard and rotating toward the in-house agent with the strongest current equity.",minConv:0,maxNew:leader.positions.length,maxFrac:0.2,reserve:0.05};
markToMarket(p,priceMap);
const held=new Set(leader.positions.map(x=>x.market_id));
const keep=[];
for(const pos of p.positions){
if(!held.has(pos.market_id)){
closePosition(p,pos,"Leader exited");
}else keep.push(pos);
}
p.positions=keep;
const book=leader.positions.reduce((s,x)=>s+(x.value||x.cost),0)||1;
for(const lp of [...leader.positions].sort((a,b)=>(b.value||b.cost)-(a.value||a.cost))){
if(hasPosition(p,lp.market_id)||hasStoppedToday(p,lp.market_id))continue;
const eq=equity(p),investable=p.cash-eq*0.05; if(investable<=50)break;
const weight=(lp.value||lp.cost)/book;
const stake=Math.min(eq*Math.min(0.2,weight),investable);
if(stake<50)continue;
const entry=lp.current_price; if(entry<=0||entry>=1)continue;
const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
if(cost>p.cash)continue;
p.cash=+(p.cash-cost).toFixed(2);
p.positions.push({market_id:lp.market_id,token_id:lp.token_id||null,question:lp.question,side:lp.side,category:lp.category,
shares,entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),cost,value:cost,
original_shares:shares,original_cost:cost,unrealized_pnl:0,conviction:lp.conviction,opened_at:cycleIso(),url:lp.url||"",
gain_stops:{},stop_losses:{}});
p.history.push({date:logDay(),action:"OPEN",question:lp.question,side:lp.side,
detail:`Copied leader — ${shares} ${lp.side} '${lp.question.slice(0,36)}' @ ${pct(entry)}`});
}
recordSnapshot(p);
}
/* ---------- Trailing-week backtest seeding ----------
Marks each agent's CURRENT holdings against real Polymarket price history
so the equity curves show a plausible past week instead of a flat line. */
function lastNDates(n){const out=[];const now=new Date();for(let i=n-1;i>=0;i--){const x=new Date(now);x.setUTCDate(now.getUTCDate()-i);out.push(x.toISOString().slice(0,10));}return out;}
async function fetchPriceHistory(tokenId){
try{const r=await fetch(`${CLOB}/prices-history?market=${tokenId}&interval=1w&fidelity=1440`);
if(!r.ok)return null;const d=await r.json();const map={};
(d.history||[]).forEach(h=>{map[new Date(h.t*1000).toISOString().slice(0,10)]=h.p;});
return Object.keys(map).length?map:null;}catch(e){return null;}
}
async function fetchClobTokens(marketId){
try{const r=await fetch(`${GAMMA}/markets/${marketId}`);if(!r.ok)return null;const d=await r.json();
const t=parseJsonField(d.clobTokenIds).map(String);return t.length===2?t:null;}catch(e){return null;}
}
function priceOnDay(map,day,fallback){
if(!map)return fallback;let best=null,bd=null;
for(const dt in map){if(dt<=day&&(bd===null||dt>bd)){bd=dt;best=map[dt];}}
if(best!=null)return best;
const ks=Object.keys(map).sort();return ks.length?map[ks[0]]:fallback;
}
async function fetchTraderActivity(wallet){
try{const r=await fetch(`${DATA_API}/activity?user=${wallet}&limit=500&type=TRADE`);
if(!r.ok)return [];const d=await r.json();
return d.filter(x=>x.asset&&(x.side==="BUY"||x.side==="SELL"))
.map(x=>({asset:String(x.asset),side:x.side,size:+x.size||0,ts:x.timestamp}));}catch(e){return [];}
}
async function fetchTraderPositionsRaw(wallet){
try{const r=await fetch(`${DATA_API}/positions?user=${wallet}&sizeThreshold=1&limit=100`);
if(!r.ok)return [];const d=await r.json();
return d.filter(p=>(p.outcome==="Yes"||p.outcome==="No")&&!p.redeemable&&p.curPrice>0.02&&p.curPrice<0.98)
.map(p=>({asset:String(p.asset),size:+p.size||0,curPrice:+p.curPrice,title:p.title||""}));}catch(e){return [];}
}
/* Replay each whale by reconstructing its past holdings from on-chain trade
activity (current size minus net trades after each day), valued at real
historical token prices — then scaled to the $10k paper bankroll. */
async function backtestWhales(st,dates){
for(const a of AGENTS.filter(x=>x.kind==="whale")){
const p=st.agents[a.id], wh=st.whales[a.id];
const flat=()=>{for(const d of dates){SIM_DAY=d;recordSnapshot(p);}SIM_DAY=null;};
if(!wh){flat();continue;}
setStatus(`backtest: replaying ${wh.name}…`,true);
let cur=[],acts=[];
try{cur=await fetchTraderPositionsRaw(wh.wallet);}catch(e){}
try{acts=await fetchTraderActivity(wh.wallet);}catch(e){}
cur.sort((x,y)=>y.size*y.curPrice-x.size*x.curPrice);
const top=cur.slice(0,12);
if(!top.length){flat();continue;}
const histA={};
for(const t of top)histA[t.asset]=await fetchPriceHistory(t.asset);
const dayVal={};
for(const day of dates){
const endTs=Math.floor(new Date(day+"T23:59:59Z").getTime()/1000);
let v=0;
for(const t of top){
let netAfter=0;
for(const ac of acts)if(ac.asset===t.asset&&ac.ts>endTs)netAfter+=(ac.side==="BUY"?ac.size:-ac.size);
const holdings=Math.max(0,t.size-netAfter);
v+=holdings*priceOnDay(histA[t.asset],day,t.curPrice);
}
dayVal[day]=v;
}
const anchor=dayVal[dates[dates.length-1]]||1;
p.snapshots=dates.map(day=>{
const eq=10000*(dayVal[day]/(anchor||1));
return {date:day,timestamp:new Date(day+"T12:00:00Z").toISOString(),cash:0,
equity:+eq.toFixed(2),pnl:+(eq-10000).toFixed(2),return_pct:+((eq/10000-1)*100).toFixed(2),
open_positions:top.length,seeded:true};
});
}
SIM_DAY=null;
}
/* Full day-by-day backtest: for each of the past 7 days, rebuild every market's
state from that day's real price (+ correct time-to-resolution), re-run the
scoring engine, and step every strategy agent through a day of trading.
NOTE: volume/liquidity signals use current values as a proxy — Polymarket
does not expose historical volume. Price & timing are truly historical. */
async function backtestWeek(st){
const dates=lastNDates(7), start=dates[0];
AGENTS.forEach(a=>{st.agents[a.id]=defaultPortfolio();});
if(AGENTS.some(a=>a.kind==="whale"&&!st.whales[a.id])){
setStatus("backtest: finding top Polymarket traders…",true);
const picked=await discoverWhales();
AGENTS.filter(a=>a.kind==="whale").forEach(a=>{if(!st.whales[a.id]&&picked[a.slot])st.whales[a.id]=picked[a.slot];});
}
setStatus("backtest: loading markets…",true);
const markets=await fetchMarkets(2,100);
const uni=markets.filter(m=>m.clob_token_ids&&m.clob_token_ids.length===2&&(!m.end_date||m.end_date>=start)).slice(0,100);
const hist={}; let n=0;
for(const m of uni){setStatus(`backtest: price history ${++n}/${uni.length}…`,true);hist[m.id]=await fetchPriceHistory(m.clob_token_ids[0]);}
const focus=getFocus();
for(const day of dates){
SIM_DAY=day;
setStatus(`backtest: simulating ${day}…`,true);
const snaps=[];
for(const m of uni){
const yp=priceOnDay(hist[m.id],day,null);
if(yp==null||yp<=0.02||yp>=0.98)continue;
const dtr=m.end_date?(new Date(m.end_date)-new Date(day+"T12:00:00Z"))/86400000:null;
if(dtr!=null&&dtr<-0.5)continue;
snaps.push(Object.assign({},m,{yes_price:+yp.toFixed(4),no_price:+(1-yp).toFixed(4),days_to_resolution:dtr}));
}
const sugs=generateSuggestions(snaps);
const priceMap={}; snaps.forEach(s=>priceMap[s.id]={yes_price:s.yes_price,no_price:s.no_price});
const claimedMarkets=new Set();
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
const p=st.agents[cfg.id];
markToMarket(p,priceMap,cfg,{policyExits:true});
openPositions(p,cfg,cfg.rank(sugs),focus,null,claimedMarkets).forEach(id=>claimedMarkets.add(id));
recordSnapshot(p);
}
const leaderId=AGENTS.filter(a=>a.kind==="strategy").map(a=>({id:a.id,eq:equity(st.agents[a.id])})).sort((x,y)=>y.eq-x.eq)[0].id;
st.copycatLeader=leaderId;
runCopycat(st.agents.copycat,st.agents[leaderId],priceMap);
if(day===dates[dates.length-1])saveSuggestions(sugs); // latest live-ish board for the Suggestions tab
}
SIM_DAY=null;
await backtestWhales(st,dates);
// establish current whale paper positions so live tracking continues
for(const a of AGENTS.filter(x=>x.kind==="whale")){const wh=st.whales[a.id];if(wh){try{await runWhaleAgent(st.agents[a.id],wh.wallet);}catch(e){}}}
st.seeded=true;
}
async function runDailyCycle(){
const cloud=await loadAuthoritativeCloudState();
if(!cloud.ok)throw new Error("Cloud sync unavailable. Refusing to run a local-only cycle.");
let st=loadState();
const hour=currentCycleHour();
if(st.seeded&&st.last_cycle_hour===hour){
setStatus("up to date",false);
return {suggestions:(loadSuggestions().suggestions||[]).length,skipped:true,hour};
}
SNAP_TS=nowIso();
setStatus("fetching markets…",true);
try{
const markets=await fetchMarkets(4,100);
setStatus("analyzing…",true);
const sugs=generateSuggestions(markets);
saveSuggestions(sugs);
st=loadState();
const focus=getFocus();
// Price map for suggestion-market positions (strategy + copycat only).
const ids=new Set();
AGENTS.filter(a=>a.kind==="strategy"||a.kind==="copycat").forEach(a=>
(st.agents[a.id].positions||[]).forEach(pos=>{if(pos.market_id)ids.add(pos.market_id);}));
setStatus("marking positions…",true);
const priceMap={};
for(const id of ids){priceMap[id]=await fetchMarketPrice(id);}
// 1) in-house strategy agents
setStatus("strategy agents trading…",true);
const preBoard=AGENTS.filter(a=>a.kind==="strategy").map(a=>({id:a.id,eq:equity(st.agents[a.id])})).sort((x,y)=>y.eq-x.eq);
const leaderEq=preBoard[0]?preBoard[0].eq:STARTING_BALANCE;
const claimedMarkets=new Set();
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
const p=st.agents[cfg.id];
markToMarket(p,priceMap,cfg,{policyExits:true});
const rank=preBoard.findIndex(x=>x.id===cfg.id)+1||preBoard.length;
const decision=adaptiveDecision(cfg,p,rank,preBoard.length,leaderEq);
openPositions(p,cfg,cfg.rank(sugs),focus,decision,claimedMarkets).forEach(id=>claimedMarkets.add(id));
recordSnapshot(p);
}
// 2) copycat shadows the leading strategy agent
const stratBoard=AGENTS.filter(a=>a.kind==="strategy")
.map(a=>({id:a.id,eq:equity(st.agents[a.id])})).sort((x,y)=>y.eq-x.eq);
st.copycatLeader=stratBoard[0].id;
runCopycat(st.agents["copycat"],st.agents[st.copycatLeader],priceMap);
// 3) whale-copy agents mirror top Polymarket traders
if(AGENTS.some(a=>a.kind==="whale"&&!st.whales[a.id])){
setStatus("finding top Polymarket traders…",true);
const picked=await discoverWhales();
AGENTS.filter(a=>a.kind==="whale").forEach(a=>{if(!st.whales[a.id]&&picked[a.slot])st.whales[a.id]=picked[a.slot];});
}
for(const a of AGENTS.filter(x=>x.kind==="whale")){
const wh=st.whales[a.id];
if(!wh){recordSnapshot(st.agents[a.id]);continue;}
setStatus(`copying ${wh.name}…`,true);
const allBoard=AGENTS.map(c=>({id:c.id,eq:equity(st.agents[c.id])})).sort((x,y)=>y.eq-x.eq);
const rank=allBoard.findIndex(x=>x.id===a.id)+1||allBoard.length;
const decision=adaptiveDecision(a,st.agents[a.id],rank,allBoard.length,allBoard[0]?allBoard[0].eq:STARTING_BALANCE);
try{await runWhaleAgent(st.agents[a.id],wh.wallet,decision);}
catch(e){recordSnapshot(st.agents[a.id]);}
}
st.date=todayStr();st.last_run=SNAP_TS;st.last_cycle_hour=hour;
saveState(st);
const pushed=await pushCloudState();
setStatus("up to date",false);
if(!pushed)return {suggestions:(loadSuggestions().suggestions||[]).length,skipped:true,hour};
return {suggestions:sugs.length};
}finally{SNAP_TS=null;}
}
/* ---------- Leaderboard helpers ---------- */
function board(){
const st=loadState();
return AGENTS.map(c=>{const p=st.agents[c.id]||defaultPortfolio();const eq=equity(p);
return {c,p,eq,pnl:eq-STARTING_BALANCE,ret:(eq/STARTING_BALANCE-1)*100};})
.sort((a,b)=>b.eq-a.eq);
}
const MEDALS=["🥇","🥈","🥉"];
/* ---------- Rendering ---------- */
function renderAll(){renderOverview();renderLeaderboard();renderSuggestions();renderPortfolioTab();renderInvestTab();renderPaperTab();renderAbout();}
function renderOverview(){
const b=board(); const lead=b[0];
const avgRet=b.reduce((s,r)=>s+r.ret,0)/b.length;
const sugCount=(loadSuggestions().suggestions||[]).length;
const stats=[
{ic:lead.c.emoji,label:"Leader",value:lead.c.name.split(" ")[0]},
{ic:"📈",label:"Leader return",value:fmtPct(lead.ret),cls:signClass(lead.pnl)},
{ic:"⚖️",label:"Avg return",value:fmtPct(avgRet),cls:signClass(avgRet)},
{ic:"💡",label:"Ideas this hour",value:sugCount},
{ic:"🏆",label:"Agents",value:AGENTS.length},
];
$("stats").innerHTML=stats.map(s=>`<div class="stat"><div class="ic">${s.ic}</div><div class="label">${s.label}</div><div class="value ${s.cls||""}">${s.value}</div></div>`).join("");
const st=loadState();
if(st.last_run)$("lastRun").textContent="updated "+new Date(st.last_run).toLocaleString();
drawCombo("chart","comboLegend");
renderTopPicks();
}
function renderTopPicks(){
const root=$("topPicks"); if(!root)return;
const top=((loadSuggestions().suggestions)||[]).slice(0,5);
if(!top.length){root.innerHTML=`<div class="empty">Run a cycle to see top picks.</div>`;return;}
root.innerHTML=top.map(s=>`<div class="pos"><div>
<div class="pq">${s.url?`<a class="market-title" href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.question.slice(0,58))}${s.question.length>58?"…":""}</a>`:`${esc(s.question.slice(0,58))}${s.question.length>58?"…":""}`}</div>
<div class="sub"><span class="cat-badge" style="background:${catColor(s.category)}22;color:${catColor(s.category)}">${esc(s.category)}</span> BUY ${s.side} @ ${Math.round(s.entry_price*100)}¢</div>
</div><div class="right"><div style="font-family:'Space Grotesk';font-weight:700">${Math.round(s.conviction)}</div><div class="sub">conviction</div></div></div>`).join("");
}
function miniSpark(snaps,color){
if(!snaps||snaps.length<2)return `<svg width="120" height="34"></svg>`;
const pts=snaps.map(s=>s.equity),W=120,H=34,p=3;
const mn=Math.min(...pts),mx=Math.max(...pts),sp=(mx-mn)||1;
const x=i=>p+(i/(pts.length-1))*(W-2*p),y=v=>H-p-((v-mn)/sp)*(H-2*p);
const d=pts.map((v,i)=>`${i?"L":"M"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
return `<svg width="${W}" height="${H}"><path d="${d}" fill="none" stroke="${color}" stroke-width="2"/></svg>`;
}
function agentBlurb(cfg,st){
if(cfg.kind==="whale"){const w=st.whales[cfg.id];return w?`🐋 Mirrors <b>${esc(w.name)}</b>${w.profit?` · $${(w.profit/1e6).toFixed(1)}M profit`:""}. ${esc(cfg.blurb)}`:`${esc(cfg.blurb)} The trader is selected on the first successful run.`;}
if(cfg.kind==="copycat"){const L=AGENTS.find(a=>a.id===st.copycatLeader);return `🐒 Currently copying <b>${L?esc(L.name):"the leader"}</b>. ${esc(cfg.blurb)}`;}
return cfg.blurb;
}
function agentPlainBlurb(cfg,st){
return agentBlurb(cfg,st).replace(/<[^>]*>/g,"");
}
function bestAndWorst(p){
const open=p.positions||[];
if(!open.length)return {best:"No open trades yet",worst:"No drawdowns yet"};
const sorted=[...open].sort((a,b)=>(b.unrealized_pnl||0)-(a.unrealized_pnl||0));
const label=x=>`${x.question.slice(0,34)}${x.question.length>34?"...":""} (${(x.unrealized_pnl||0)>=0?"+":""}${fmtUSD(x.unrealized_pnl||0)})`;
return {best:label(sorted[0]),worst:label(sorted[sorted.length-1])};
}
function decisionSummary(p){
const d=p.lastDecision;
if(!d)return "No live adaptation yet; this agent is still using its base rulebook.";
return `${d.mode} mode: ${d.reason} Limits now: ${d.maxNew} new trade${d.maxNew===1?"":"s"}, max ${(d.maxFrac*100).toFixed(1)}% per position${d.minConv?`, conviction ${d.minConv}+`:""}.`;
}
function renderAgentBrief(cfg,p,st){
const root=$("agentBrief"); if(!root)return;
const bw=bestAndWorst(p);
const risk=cfg.kind==="whale"?"Mirror risk":cfg.kind==="copycat"?"Adaptive":cfg.maxFrac>=0.12?"Higher risk":cfg.maxFrac<=0.05?"Lower risk":"Balanced";
const cadence=cfg.kind==="whale"?"Copies biggest live holdings":cfg.kind==="copycat"?"Follows current strategy leader":`${cfg.maxNew||0} new trades max`;
const exitRule=cfg.kind==="strategy"?`Exit if edge fades, conviction weakens, <${EXIT_RESOLUTION_DAYS}d to resolve, or ${EXIT_STALE_DAYS}d stale`:cfg.kind==="copycat"?"Exit when leader exits":"Exit when copied trader exits";
const thesis=agentPlainBlurb(cfg,st);
root.innerHTML=`
<div class="brief-main" style="border-top:3px solid ${cfg.color}">
<div class="brief-title"><span>${cfg.emoji}</span><span>${cfg.name}</span></div>
<p>${esc(thesis)}</p>
<p style="margin-top:9px"><b>Adaptation:</b> ${esc(decisionSummary(p))}</p>
</div>
<div class="brief-mini"><div class="k">Style</div><div class="v">${esc(risk)}</div></div>
<div class="brief-mini"><div class="k">Trade Rule</div><div class="v">${esc(cadence)}</div></div>
<div class="brief-mini"><div class="k">Stop Loss</div><div class="v">Sell 33% at -15%, 33% at -30%, all at -45%</div></div>
<div class="brief-mini"><div class="k">Gain Stop</div><div class="v">Sell 25% at +75%, +150%, +300%</div></div>
<div class="brief-mini"><div class="k">Policy Exit</div><div class="v">${esc(exitRule)}</div></div>
<div class="brief-mini"><div class="k">Best / Worst</div><div class="v"><span class="pos-val">${esc(bw.best)}</span><br><span class="neg-val">${esc(bw.worst)}</span></div></div>`;
}
function renderLeaderboard(){
const b=board(); const st=loadState();
$("lbSub").textContent="· "+b[0].c.name+" leads";
$("leaderboard").innerHTML=b.map((r,i)=>`
<div class="lb-card" data-agent="${r.c.id}" style="border-top:3px solid ${r.c.color}">
<span class="rankpill">#${i+1}</span>
<div class="lb-top"><span class="medal">${MEDALS[i]||""}</span><span class="lb-emoji">${r.c.emoji}</span>
<div><div class="lb-name">${r.c.name}</div><div class="lb-blurb">${agentBlurb(r.c,st)}</div></div></div>
<div class="lb-mid"><div class="lb-eq">${fmtUSD(r.eq)}</div><div class="lb-ret ${signClass(r.pnl)}">${fmtPct(r.ret)}</div></div>
<div class="lb-foot"><span class="muted small">${r.p.positions.length} open · ${r.p.closed.length} closed</span>${miniSpark(r.p.snapshots,r.c.color)}</div>
</div>`).join("");
document.querySelectorAll("#leaderboard .lb-card").forEach(el=>{
el.addEventListener("click",()=>{localStorage.setItem(VIEW_KEY,el.dataset.agent);renderPortfolioTab();showTab("portfolio");});
});
drawCombo("chart2","comboLegend2");
}
const CHART_RANGES=[
{id:"1w",label:"1W",days:7},
{id:"1m",label:"1M",days:30},
{id:"3m",label:"3M",days:90},
{id:"6m",label:"6M",days:180},
{id:"1y",label:"1Y",days:365},
{id:"5y",label:"5Y",days:1825},
{id:"all",label:"ALL",days:null},
];
function chartRange(){const r=localStorage.getItem(CHART_RANGE_KEY)||"1w";return CHART_RANGES.some(x=>x.id===r)?r:"1w";}
function renderChartRanges(){
const active=chartRange();
document.querySelectorAll("[data-chart-ranges]").forEach(root=>{
root.innerHTML=CHART_RANGES.map(r=>`<button class="rangebtn ${r.id===active?"active":""}" data-range="${r.id}" type="button">${r.label}</button>`).join("");
});
document.querySelectorAll(".rangebtn").forEach(btn=>btn.onclick=()=>{localStorage.setItem(CHART_RANGE_KEY,btn.dataset.range);renderAll();});
}
const snapTime=(s)=>new Date(s.timestamp||`${s.date}T12:00:00Z`).getTime();
function chartDateLabel(t,compact=false){
const d=new Date(t);
if(isNaN(d))return "";
const sameYear=d.getUTCFullYear()===new Date().getUTCFullYear();
return d.toLocaleDateString("en-US",compact
? {month:"short",day:"numeric"}
: (sameYear?{month:"short",day:"numeric"}:{month:"short",day:"numeric",year:"2-digit"}));
}
function chartTicks(times,maxTicks=5){
if(!times.length)return [];
if(times.length===1)return [{i:0,t:times[0]}];
const count=Math.min(maxTicks,times.length),seen=new Set(),ticks=[];
for(let n=0;n<count;n++){
const i=Math.round(n*(times.length-1)/(count-1));
if(!seen.has(i)){seen.add(i);ticks.push({i,t:times[i]});}
}
return ticks;
}
function niceChartBounds(vals){
let min=Math.min(...vals,0),max=Math.max(...vals,0);
if(min===max){min-=1;max+=1;}
const span=max-min,pad=Math.max(0.5,span*0.12);
min-=pad;max+=pad;
const step=Math.max(0.5,Math.pow(10,Math.floor(Math.log10(max-min)))/2);
min=Math.floor(min/step)*step;max=Math.ceil(max/step)*step;
return {min,max};
}
function smoothPath(points){
if(points.length<2)return points.map((p,i)=>`${i?"L":"M"}${p.x},${p.y}`).join(" ");
let d=`M${points[0].x},${points[0].y}`;
for(let i=1;i<points.length;i++){
const prev=points[i-1],cur=points[i];
const midX=((+prev.x)+(+cur.x))/2;
d+=` C${midX.toFixed(1)},${prev.y} ${midX.toFixed(1)},${cur.y} ${cur.x},${cur.y}`;
}
return d;
}
function rangeFilteredTimes(times){
const range=CHART_RANGES.find(r=>r.id===chartRange())||CHART_RANGES[0];
if(!range.days)return times;
const newest=Math.max(...times);
const cutoff=newest-range.days*86400000;
const filtered=times.filter(t=>t>=cutoff);
return filtered.length?filtered:times.slice(-1);
}
function drawCombo(svgId,legendId){
const st=loadState();
renderChartRanges();
const series=AGENTS.map(c=>({c,snaps:((st.agents[c.id]&&st.agents[c.id].snapshots)||[])
.filter(s=>Number.isFinite(snapTime(s))).sort((a,b)=>snapTime(a)-snapTime(b))}));
const allTimes=rangeFilteredTimes([...new Set(series.flatMap(s=>s.snaps.map(snapTime)))].sort((a,b)=>a-b));
const svg=$(svgId); if(!svg)return;
if(!allTimes.length){svg.innerHTML="";if($(legendId))$(legendId).innerHTML=`<span class="muted small">No history yet — click "Run cycle".</span>`;return;}
const W=960,H=320,padL=72,padR=36,padT=36,padB=54; const vals=[0];
const lines=series.map(s=>{
let last=0,idx=0;
const pts=allTimes.map(t=>{
while(idx<s.snaps.length&&snapTime(s.snaps[idx])<=t){
last=Number.isFinite(s.snaps[idx].return_pct)?s.snaps[idx].return_pct:((s.snaps[idx].equity||STARTING_BALANCE)/STARTING_BALANCE-1)*100;
idx++;
}
vals.push(last);return last;
});
return {c:s.c,pts};
});
const avgPts=allTimes.map((_,i)=>+(lines.reduce((sum,l)=>sum+l.pts[i],0)/Math.max(1,lines.length)).toFixed(2));
vals.push(...avgPts);
const bounds=niceChartBounds(vals),min=bounds.min,max=bounds.max,span=(max-min)||1;
const x=i=>allTimes.length===1?(padL+(W-padR))/2:padL+(i/(allTimes.length-1))*(W-padL-padR);
const y=v=>H-padB-((v-min)/span)*(H-padT-padB);
const yTicks=Array.from({length:5},(_,i)=>+(min+(span*i/4)).toFixed(1));
const zeroY=y(0).toFixed(1);
let inner=`<defs>
<linearGradient id="${svgId}-panel" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="rgba(255,255,255,.055)"/><stop offset="100%" stop-color="rgba(255,255,255,.018)"/>
</linearGradient>
<filter id="${svgId}-soft" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="5" flood-color="rgba(0,0,0,.22)"/>
</filter>
</defs>`;
inner+=`<rect x="${padL}" y="${padT}" width="${W-padL-padR}" height="${H-padT-padB}" rx="12" fill="url(#${svgId}-panel)" stroke="rgba(255,255,255,.08)"/>`;
for(const v of yTicks){
const yy=y(v).toFixed(1);
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${yy}" y2="${yy}" stroke="rgba(255,255,255,.075)" stroke-width="1"/>`;
inner+=`<text x="${padL-12}" y="${(+yy+4).toFixed(1)}" text-anchor="end" fill="rgba(224,234,255,.62)" font-size="11" font-family="Inter, sans-serif">${fmtPct(v)}</text>`;
}
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${zeroY}" y2="${zeroY}" stroke="rgba(255,255,255,.28)" stroke-width="1.2"/>`;
for(const tick of chartTicks(allTimes,5)){
const xx=x(tick.i).toFixed(1);
inner+=`<line x1="${xx}" x2="${xx}" y1="${padT}" y2="${H-padB}" stroke="rgba(255,255,255,.055)" stroke-width="1"/>`;
inner+=`<text x="${xx}" y="${H-18}" text-anchor="middle" fill="rgba(224,234,255,.72)" font-size="11" font-family="Inter, sans-serif">${chartDateLabel(tick.t,true)}</text>`;
}
for(const ln of lines){
const points=ln.pts.map((v,i)=>({x:x(i).toFixed(1),y:y(v).toFixed(1)}));
const d=smoothPath(points);
const last=points[points.length-1];
inner+=`<path d="${d}" fill="none" stroke="${ln.c.color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity=".92" filter="url(#${svgId}-soft)"/>`;
inner+=`<circle cx="${last.x}" cy="${last.y}" r="4.2" fill="${ln.c.color}" stroke="#101827" stroke-width="2"/>`;
}
const avgPoints=avgPts.map((v,i)=>({x:x(i).toFixed(1),y:y(v).toFixed(1)}));
inner+=`<path d="${smoothPath(avgPoints)}" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-dasharray="8 7" stroke-linecap="round" opacity=".82"/>`;
inner+=`<text x="${padL}" y="21" fill="rgba(224,234,255,.64)" font-size="11.5" font-family="Inter, sans-serif">Return %</text>`;
inner+=`<text x="${W-padR}" y="21" text-anchor="end" fill="rgba(224,234,255,.58)" font-size="11.5" font-family="Inter, sans-serif">${chartDateLabel(allTimes[0])} - ${chartDateLabel(allTimes[allTimes.length-1])}</text>`;
svg.innerHTML=inner;
if($(legendId)){
const b=board();const avg=b.reduce((s,r)=>s+r.ret,0)/b.length;
$(legendId).innerHTML=`<span class="leg"><span class="legdot" style="background:#fff"></span>Average <b class="${signClass(avg)}">${fmtPct(avg)}</b></span>`+
b.map(r=>`<span class="leg"><span class="legdot" style="background:${r.c.color}"></span>${r.c.emoji} ${r.c.name} <b class="${signClass(r.pnl)}">${fmtPct(r.ret)}</b></span>`).join("");
}
}
function renderFilterBar(all){
const focus=getFocus();
const counts={};CATEGORIES.forEach(c=>counts[c]=0);
all.forEach(s=>{const c=counts[s.category]!==undefined?s.category:"Other";counts[c]++;});
counts["All"]=all.length;
$("filterbar").innerHTML=CATEGORIES.map(c=>{const n=counts[c]||0;const dim=(c!=="All"&&n===0)?"opacity:.4;":"";
return `<span class="chip ${c===focus?"active":""}" data-cat="${c}" style="${dim}">${c}<span class="ct">${n}</span></span>`;}).join("");
document.querySelectorAll("#filterbar .chip").forEach(el=>el.addEventListener("click",()=>{setFocus(el.dataset.cat);renderSuggestions();}));
}
function renderSuggestions(){
const data=loadSuggestions();
$("sugDate").textContent=data.date?${data.date}`:"";
const all=data.suggestions||[];const root=$("suggestions");
renderFilterBar(all);
if(!all.length){root.innerHTML=`<div class="empty" style="grid-column:1/-1">No suggestions yet. Click "Run cycle".</div>`;$("focusNote").textContent="";return;}
const focus=getFocus();
const filtered=(focus==="All"?all:all.filter(s=>s.category===focus)).slice(0,VISIBLE_SUGGESTIONS);
$("focusNote").textContent=focus==="All"
? "All five agents trade from this wider pool — each now tries fresh markets first to reduce overlap."
: `Filtered to ${focus} — agents will only open new ${focus} positions while this is selected.`;
if(!filtered.length){root.innerHTML=`<div class="empty" style="grid-column:1/-1">No ${esc(focus)} suggestions in this hourly batch.</div>`;return;}
root.innerHTML=filtered.map(s=>{
const col=catColor(s.category);
const drivers=s.drivers.map(d=>`<span class="driver">${d}</span>`).join("");
const days=s.days_to_resolution!=null?`${s.days_to_resolution}d`:"—";
return `<div class="sug" style="border-left-color:${col}">
<div class="sug-row" style="margin-bottom:2px"><span class="cat-badge" style="background:${col}22;color:${col}">${esc(s.category||"Other")}</span></div>
<div class="q">${s.url?`<a class="market-title" href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.question)}</a>`:esc(s.question)}</div>
<div class="event">${esc(s.event||"")}</div>
<div class="sug-row">
<span class="pill ${s.side}">BUY ${s.side}</span>
<span class="muted small">@ ${Math.round(s.entry_price*100)}¢</span>
<div class="conv-bar"><span style="width:${s.conviction}%"></span></div>
<span class="muted small">${Math.round(s.conviction)}</span>
</div>
<div class="rationale">${esc(s.rationale)}</div>
<div class="drivers">${drivers}</div>
<div class="metrics">
<span>Vol <b>${fmtUSD(s.volume)}</b></span><span>24h <b>${fmtUSD(s.volume_24hr)}</b></span><span>Resolves <b>${days}</b></span>
${s.url?`<a class="market-link" href="${esc(s.url)}" target="_blank" rel="noopener">Open on Polymarket ↗</a>`:""}
</div></div>`;
}).join("");
}
function renderPortfolioTab(){
const st=loadState();
let viewId=localStorage.getItem(VIEW_KEY); if(!AGENTS.some(a=>a.id===viewId))viewId="value";
const cfg=agentById(viewId);
$("agentSel").innerHTML=AGENTS.map(a=>{
const active=a.id===viewId;
const style=active?`background:linear-gradient(135deg,${a.color},${a.color});border-color:transparent`:"";
return `<span class="segbtn ${active?"active":""}" data-agent="${a.id}" style="${style}">${a.emoji} ${a.name}</span>`;}).join("");
document.querySelectorAll("#agentSel .segbtn").forEach(el=>el.addEventListener("click",()=>{localStorage.setItem(VIEW_KEY,el.dataset.agent);renderPortfolioTab();}));
const p=st.agents[viewId]||defaultPortfolio();const eq=equity(p),pnl=eq-p.starting_balance;
renderAgentBrief(cfg,p,st);
const stats=[
{ic:"💰",label:"Equity",value:fmtUSD(eq)},
{ic:"💵",label:"Cash",value:fmtUSD(p.cash)},
{ic:"📈",label:"P&L",value:fmtUSD(pnl),cls:signClass(pnl)},
{ic:"🎯",label:"Return",value:fmtPct((eq/p.starting_balance-1)*100),cls:signClass(pnl)},
{ic:"📂",label:"Open",value:p.positions.length},
];
$("statsPf").innerHTML=stats.map(s=>`<div class="stat"><div class="ic">${s.ic}</div><div class="label">${s.label}</div><div class="value ${s.cls||""}">${s.value}</div></div>`).join("");
const sort=$("positionSort");
if(sort){
sort.value=localStorage.getItem(PF_SORT_KEY)||"pnl_desc";
sort.onchange=()=>{localStorage.setItem(PF_SORT_KEY,sort.value);renderPositions(p.positions);};
}
renderPositions(p.positions);
renderHistory((p.history||[]).slice(-50).reverse());
renderAgentReports();
}
function agentCompetitionPlan(cfg,row,rank,leader){
if(rank===1)return "Plan: defend the lead, keep sizing disciplined, and let risk controls protect gains instead of forcing new trades.";
if(cfg.kind==="copycat")return `Plan: keep shadowing ${leader.c.name}, then rotate automatically if another in-house strategy takes the crown.`;
if(cfg.kind==="whale")return "Plan: stay close to the copied trader's largest live positions while using the stop-loss and gain-stop rules to avoid blindly riding every swing.";
if(cfg.id==="value")return "Plan: close the gap by waiting for the cleanest model-versus-market mispricings and avoiding crowded trades without enough edge.";
if(cfg.id==="momentum")return "Plan: attack fast-moving markets where fresh volume confirms attention, hoping speed beats slower value strategies.";
if(cfg.id==="favorite")return "Plan: grind upward through high-probability favorites, aiming to outlast more volatile agents during choppy markets.";
if(cfg.id==="longshot")return "Plan: keep risk small but search for one underpriced outsider that can reprice sharply and leapfrog the leaderboard.";
if(cfg.id==="diversifier")return "Plan: spread bets broadly, reduce single-market damage, and try to win through consistency rather than one heroic call.";
return "Plan: keep following its rulebook and use fresh market data to pressure the leaderboard.";
}
function reportPositionLine(p){
if(!p.positions.length)return "No open positions yet, so this hour's focus is finding entries that fit the strategy.";
const best=[...p.positions].sort((a,b)=>(b.unrealized_pnl||0)-(a.unrealized_pnl||0))[0];
const worst=[...p.positions].sort((a,b)=>(a.unrealized_pnl||0)-(b.unrealized_pnl||0))[0];
if(best===worst)return `Current focus: ${best.question.slice(0,58)}${best.question.length>58?"...":""}, marked ${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}.`;
return `Best mark: ${best.question.slice(0,42)}${best.question.length>42?"...":""} (${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}). Watch item: ${worst.question.slice(0,42)}${worst.question.length>42?"...":""} (${fmtUSD(worst.unrealized_pnl||0)}).`;
}
function renderAgentReports(){
const root=$("agentReports"); if(!root)return;
const b=board(); const st=loadState(); const leader=b[0];
if($("reportDate"))$("reportDate").textContent=`${st.last_run?new Date(st.last_run).toLocaleString():todayStr()}`;
root.innerHTML=b.map((row,i)=>{
const rank=i+1, ret=fmtPct(row.ret), open=row.p.positions.length;
const thesis=agentPlainBlurb(row.c,st);
const plan=agentCompetitionPlan(row.c,row,rank,leader);
const line=reportPositionLine(row.p);
return `<article class="report" style="border-top-color:${row.c.color}">
<div class="report-title"><div class="report-name">${row.c.emoji} ${row.c.name}</div><div class="report-rank">#${rank} · ${ret}</div></div>
<p><b>Strategy:</b> ${esc(thesis)}</p>
<p><b>Adaptation:</b> ${esc(decisionSummary(row.p))}</p>
<p><b>Status:</b> ${open} open position${open===1?"":"s"} with ${fmtUSD(row.eq)} equity. ${esc(line)}</p>
<p><b>${esc(plan)}</b></p>
</article>`;
}).join("");
}
function renderPositions(positions){
const root=$("positions");if(!root)return;
if(!positions.length){
const st=loadState();
const isBacktesting=!st.seeded&&(($("statusText")&&$("statusText").textContent)||"").includes("backtest");
root.innerHTML=isBacktesting
? `<div class="loading-empty"><div class="loading-ring"></div><div><b>Building agent portfolios...</b><div class="empty-copy">The app is replaying the past week, finding top traders, and marking positions to real Polymarket prices. This fills in automatically when the backtest finishes.</div></div></div>`
: `<div class="empty">No open positions yet. Run a cycle to let this agent shop this hour's suggestions.</div>`;
return;
}
const sort=localStorage.getItem(PF_SORT_KEY)||"pnl_desc";
const ordered=[...positions].sort((a,b)=>{
if(sort==="pnl_asc")return (a.unrealized_pnl||0)-(b.unrealized_pnl||0);
if(sort==="value_desc")return (b.value||0)-(a.value||0);
if(sort==="newest")return String(b.opened_at||"").localeCompare(String(a.opened_at||""));
if(sort==="category")return String(a.category||"").localeCompare(String(b.category||""))||((b.unrealized_pnl||0)-(a.unrealized_pnl||0));
return (b.unrealized_pnl||0)-(a.unrealized_pnl||0);
});
root.innerHTML=ordered.map(p=>{const pnl=p.unrealized_pnl||0,col=catColor(p.category);
const title=`<a class="market-title" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">${esc(p.question)}</a>`;
return `<div class="pos"><div>
<div class="pq">${title}</div>
<div class="sub">${p.category?`<span class="cat-badge" style="background:${col}22;color:${col}">${esc(p.category)}</span> `:""}${p.shares} ${p.side} @ ${Math.round(p.entry_price*100)}¢ → ${Math.round(p.current_price*100)}¢</div>
<div class="sub">${esc(stopLossLabel(p))}</div>
<div class="sub">${esc(gainStopLabel(p))}</div>
<div class="sub"><a class="market-link" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">Open exact market</a></div>
</div><div class="right"><div>${fmtUSD(p.value)}</div><div class="${signClass(pnl)}">${pnl>=0?"+":""}${fmtUSD(pnl)}</div></div></div>`;}).join("");
}
function renderHistory(history){
const root=$("history");if(!root)return;
if(!history.length){root.innerHTML=`<div class="empty">No activity yet.</div>`;return;}
root.innerHTML=history.map(h=>`<div class="log-item"><span class="badge ${h.action}">${h.action}</span><span class="log-date">${h.date}</span> — ${esc(h.detail)}</div>`).join("");
}
function defaultPaperAccount(name="Paper Trader"){
return {id:"acct_"+Date.now().toString(36)+Math.random().toString(36).slice(2,6),name,cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],history:[]};
}
function loadPaperStore(){
let store;try{store=JSON.parse(localStorage.getItem(PAPER_KEY));}catch(e){store=null;}
if(!store||!store.accounts){
const acct=defaultPaperAccount("My Paper Account");
store={activeId:acct.id,accounts:{[acct.id]:acct}};
localStorage.setItem(PAPER_KEY,JSON.stringify(store));
}
if(!store.accounts[store.activeId])store.activeId=Object.keys(store.accounts)[0];
return store;
}
function savePaperStore(store,sync=true){
localStorage.setItem(PAPER_KEY,JSON.stringify(store));
if(sync)pushCloudState();
}
function activePaperAccount(store=loadPaperStore()){
return store.accounts[store.activeId]||Object.values(store.accounts)[0];
}
function paperEquity(acct){
return acct.cash+(acct.positions||[]).reduce((s,p)=>s+(p.value||p.shares*p.current_price||0),0);
}
function markPaperToMarket(acct){
const sugs=(loadSuggestions().suggestions)||[];
const byId={};sugs.forEach(s=>byId[s.market_id]=s);
(acct.positions||[]).forEach(pos=>{
const s=byId[pos.market_id];
if(s){
const price=pos.side==="YES"?s.yes_price:s.no_price;
pos.current_price=+price.toFixed(4);
pos.url=s.url||pos.url||"";
}
pos.value=+(pos.shares*pos.current_price).toFixed(2);
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
});
}
function cloneAgentBook(agentId){
const st=loadState(),src=st.agents[agentId],cfg=agentById(agentId);
if(!src)return null;
return {cash:+src.cash.toFixed(2),positions:JSON.parse(JSON.stringify(src.positions||[])),
history:[{date:logDay(),action:"COPY",detail:`Copied ${cfg.name}'s current paper book exactly.`}]};
}
function paperBuy(marketId,side,amount){
const store=loadPaperStore(),acct=activePaperAccount(store),s=((loadSuggestions().suggestions)||[]).find(x=>x.market_id===marketId);
if(!s)return toast("Market not found in current ideas.");
amount=Math.max(0,Math.min(Number(amount||0),acct.cash));
if(amount<1)return toast("Enter a paper amount first.");
const entry=side==="YES"?s.yes_price:s.no_price;
if(entry<=0||entry>=1)return toast("That market price is unavailable.");
const shares=+(amount/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
acct.cash=+(acct.cash-cost).toFixed(2);
acct.positions.push({market_id:s.market_id,question:s.question,side,shares,entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),
cost,value:cost,unrealized_pnl:0,category:s.category,opened_at:cycleIso(),url:s.url||"",paper:true});
acct.history.push({date:logDay(),action:"OPEN",detail:`Paper bought ${shares} ${side} '${s.question.slice(0,40)}' @ ${pct(entry)} for ${fmtUSD(cost)}`});
savePaperStore(store);renderPaperTab();toast("Paper bet placed.");
}
function paperClose(index){
const store=loadPaperStore(),acct=activePaperAccount(store),pos=acct.positions[index];
if(!pos)return;
const proceeds=+(pos.shares*pos.current_price).toFixed(2),realized=+(proceeds-pos.cost).toFixed(2);
acct.cash=+(acct.cash+proceeds).toFixed(2);
acct.history.push({date:logDay(),action:"CLOSE",detail:`Paper sold '${pos.question.slice(0,40)}' for ${fmtUSD(proceeds)} (${fmtUSD(realized)})`});
acct.positions.splice(index,1);
savePaperStore(store);renderPaperTab();
}
function renderExactTrades(){
const root=$("exactTrades");if(!root)return;
const rows=board().flatMap(row=>(row.p.positions||[]).map(pos=>({agent:row.c,pos})))
.sort((a,b)=>(b.pos.value||0)-(a.pos.value||0));
if(!rows.length){root.innerHTML=`<div class="empty">No open agent trades yet.</div>`;return;}
root.innerHTML=rows.slice(0,80).map(({agent,pos})=>`<div class="trade-card">
<div class="trade-head"><div><div class="trade-title">${agent.emoji} ${esc(agent.name)} · <a class="market-title" href="${esc(positionUrl(pos))}" target="_blank" rel="noopener">${esc(pos.question)}</a></div>
<div class="trade-meta"><span>${esc(pos.side)}</span><span>${Number(pos.shares||0).toFixed(2)} shares</span><span>entry ${pct(pos.entry_price)}</span><span>now ${pct(pos.current_price)}</span><span>${fmtUSD(pos.value||0)}</span></div></div>
<a class="btn ghost" href="${esc(positionUrl(pos))}" target="_blank" rel="noopener">Open market</a></div>
</div>`).join("");
}
function loadInvestAllocations(){
try{return JSON.parse(localStorage.getItem(INVEST_KEY))||{};}catch(e){return {};}
}
function saveInvestAllocations(a){
localStorage.setItem(INVEST_KEY,JSON.stringify(a));
pushCloudState();
}
function agentExposureSummary(cfg,p,amount){
const eq=Math.max(equity(p),1);
const open=(p.positions||[]).filter(x=>(x.value||0)>0).slice().sort((a,b)=>(b.value||0)-(a.value||0)).slice(0,3);
if(!amount||!open.length)return `<div class="empty">Add an amount to preview this agent's largest mapped exposures.</div>`;
return open.map(pos=>{
const mapped=amount*((pos.value||0)/eq);
const q=`<a class="market-title" href="${esc(positionUrl(pos))}" target="_blank" rel="noopener">${esc(pos.question.slice(0,52))}${pos.question.length>52?"...":""}</a>`;
return `<div class="exposure-row"><span>${q}<br><span class="muted small">${esc(pos.side)} · ${esc(pos.category||"Other")}</span></span><b>${fmtUSD(mapped)}</b></div>`;
}).join("");
}
function renderInvestTab(){
const root=$("investAgents"); if(!root)return;
const totals=$("investTotals");
const st=loadState(), alloc=loadInvestAllocations();
const rows=board().map(row=>{
const amount=Number(alloc[row.c.id]||0);
return Object.assign({},row,{amount});
});
const total=rows.reduce((s,r)=>s+(Number.isFinite(r.amount)?r.amount:0),0);
const chosen=rows.filter(r=>r.amount>0).length;
const avgReturn=chosen?rows.filter(r=>r.amount>0).reduce((s,r)=>s+r.ret*(r.amount/total),0):0;
if(totals)totals.innerHTML=[
{k:"Planned capital",v:fmtUSD(total)},
{k:"Agents funded",v:String(chosen)},
{k:"Weighted return",v:chosen?fmtPct(avgReturn):"+0.00%"},
].map(x=>`<div class="invest-num"><div class="k">${x.k}</div><div class="v">${x.v}</div></div>`).join("");
root.innerHTML=rows.map(row=>{
const pct=total>0?(row.amount/total*100):0;
return `<div class="invest-agent" style="border-left:3px solid ${row.c.color};padding-left:12px">
<div class="lb-emoji">${row.c.emoji}</div>
<div>
<div class="invest-name">${row.c.name}</div>
<div class="invest-copy">${fmtPct(row.ret)} return · ${row.p.positions.length} open · ${pct.toFixed(1)}% of draft allocation</div>
<div style="margin-top:8px">${agentExposureSummary(row.c,row.p,row.amount)}</div>
</div>
<label class="small muted">Amount<br><input type="number" min="0" step="25" value="${row.amount||""}" placeholder="$0" data-invest-agent="${row.c.id}" aria-label="Allocation for ${esc(row.c.name)}"></label>
</div>`;
}).join("");
document.querySelectorAll("[data-invest-agent]").forEach(input=>{
const saveDraft=()=>{
const next=loadInvestAllocations();
const val=Math.max(0,Number(input.value||0));
if(val)next[input.dataset.investAgent]=val;
else delete next[input.dataset.investAgent];
localStorage.setItem(INVEST_KEY,JSON.stringify(next));
};
input.oninput=saveDraft;
input.onchange=()=>{saveDraft();pushCloudState();renderInvestTab();};
});
renderExactTrades();
}
function renderPaperTab(){
const acctSelect=$("paperAccountSelect"),root=$("paperMarketList");
if(!acctSelect||!root)return;
const store=loadPaperStore(),acct=activePaperAccount(store);
markPaperToMarket(acct);savePaperStore(store,false);
acctSelect.innerHTML=Object.values(store.accounts).map(a=>`<option value="${a.id}" ${a.id===store.activeId?"selected":""}>${esc(a.name)}</option>`).join("");
acctSelect.onchange=()=>{store.activeId=acctSelect.value;savePaperStore(store);renderPaperTab();};
const agentSel=$("copyAgentSelect");
if(agentSel)agentSel.innerHTML=AGENTS.map(a=>`<option value="${a.id}">${a.emoji} ${esc(a.name)}</option>`).join("");
const eq=paperEquity(acct),pnl=eq-acct.starting_balance;
$("paperStats").innerHTML=[
{ic:"💰",label:"Equity",value:fmtUSD(eq)},
{ic:"💵",label:"Cash",value:fmtUSD(acct.cash)},
{ic:"📈",label:"P&L",value:fmtUSD(pnl),cls:signClass(pnl)},
{ic:"🎯",label:"Return",value:fmtPct((eq/acct.starting_balance-1)*100),cls:signClass(pnl)},
].map(s=>`<div class="stat"><div class="ic">${s.ic}</div><div class="label">${s.label}</div><div class="value ${s.cls||""}">${s.value}</div></div>`).join("");
renderPaperPositions(acct);
renderPaperMarkets();
renderPaperHistory(acct);
}
function renderPaperPositions(acct){
const root=$("paperPositions");if(!root)return;
if(!acct.positions.length){root.innerHTML=`<div class="empty">No paper positions yet. Copy an agent or place a paper bet from the market browser.</div>`;return;}
root.innerHTML=acct.positions.map((p,i)=>{const pnl=p.unrealized_pnl||0,col=catColor(p.category);
return `<div class="pos"><div>
<div class="pq"><a class="market-title" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">${esc(p.question)}</a></div>
<div class="sub">${p.category?`<span class="cat-badge" style="background:${col}22;color:${col}">${esc(p.category)}</span> `:""}${p.shares} ${p.side} @ ${Math.round(p.entry_price*100)}¢ → ${Math.round(p.current_price*100)}¢</div>
<div class="sub"><a class="market-link" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">Open market</a></div>
</div><div class="right"><div>${fmtUSD(p.value||0)}</div><div class="${signClass(pnl)}">${pnl>=0?"+":""}${fmtUSD(pnl)}</div><button class="btn ghost" data-paper-close="${i}" style="margin-top:7px">Sell</button></div></div>`;
}).join("");
document.querySelectorAll("[data-paper-close]").forEach(btn=>btn.onclick=()=>paperClose(Number(btn.dataset.paperClose)));
}
function renderPaperMarkets(){
const root=$("paperMarketList"),filter=$("paperFilterbar");if(!root)return;
const all=(loadSuggestions().suggestions)||[],focus=getFocus();
if(filter){
const counts={};CATEGORIES.forEach(c=>counts[c]=0);all.forEach(s=>{counts[counts[s.category]!==undefined?s.category:"Other"]++;});counts.All=all.length;
filter.innerHTML=CATEGORIES.map(c=>`<span class="chip ${focus===c?"active":""}" data-paper-cat="${c}">${c}<span class="ct">${counts[c]||0}</span></span>`).join("");
document.querySelectorAll("[data-paper-cat]").forEach(el=>el.onclick=()=>{setFocus(el.dataset.paperCat);renderPaperTab();});
}
const shown=all.filter(s=>focus==="All"||!focus||s.category===focus).slice(0,24);
if(!shown.length){root.innerHTML=`<div class="empty">Run a cycle to load paper-tradable Polymarket ideas.</div>`;return;}
root.innerHTML=shown.map(s=>`<div class="trade-card">
<div class="trade-head"><div><div class="trade-title"><a class="market-title" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">${esc(s.question)}</a></div>
<div class="trade-meta"><span>${esc(s.category)}</span><span>YES ${pct(s.yes_price)}</span><span>NO ${pct(s.no_price)}</span><span>${Math.round(s.conviction)} conviction</span></div></div>
<a class="btn ghost" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">Open market</a></div>
<div class="trade-actions">
<input class="input" type="number" min="1" step="25" value="100" data-paper-amount="${esc(s.market_id)}" aria-label="Paper amount for ${esc(s.question)}">
<button class="btn" data-paper-buy="${esc(s.market_id)}" data-side="YES">Paper YES</button>
<button class="btn" data-paper-buy="${esc(s.market_id)}" data-side="NO">Paper NO</button>
</div>
</div>`).join("");
document.querySelectorAll("[data-paper-buy]").forEach(btn=>btn.onclick=()=>{
const input=[...document.querySelectorAll("[data-paper-amount]")].find(el=>el.dataset.paperAmount===btn.dataset.paperBuy);
paperBuy(btn.dataset.paperBuy,btn.dataset.side,input&&input.value);
});
}
function renderPaperHistory(acct){
const root=$("paperHistory");if(!root)return;
const hist=(acct.history||[]).slice(-40).reverse();
root.innerHTML=hist.length?hist.map(h=>`<div class="log-item"><span class="badge ${h.action}">${h.action}</span><span class="log-date">${h.date}</span> — ${esc(h.detail)}</div>`).join(""):`<div class="empty">No paper activity yet.</div>`;
}
function renderAbout(){
const root=$("aboutAgents");if(!root)return;
const st=loadState();
root.innerHTML=AGENTS.map(a=>`<div class="feature" style="border-top:3px solid ${a.color}">
<div class="fic">${a.emoji}</div><h4>${a.name}</h4><p>${agentBlurb(a,st)}</p></div>`).join("");
}
function esc(s){return String(s).replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));}
/* ---------- Tab routing ---------- */
const TABS=["overview","leaderboard","suggestions","portfolio","invest","paper","about"];
function showTab(name){
if(!TABS.includes(name))name="overview";
document.querySelectorAll(".tabpanel").forEach(p=>p.classList.toggle("active",p.dataset.tab===name));
document.querySelectorAll(".tab").forEach(t=>t.classList.toggle("active",t.dataset.tab===name));
if(location.hash.slice(1)!==name)history.replaceState(null,"","#"+name);
window.scrollTo({top:0,behavior:"smooth"});
}
document.querySelectorAll(".tab").forEach(t=>t.addEventListener("click",()=>showTab(t.dataset.tab)));
window.addEventListener("hashchange",()=>showTab(location.hash.slice(1)));
/* ---------- Wire up ---------- */
$("runBtn").addEventListener("click",async()=>{
const btn=$("runBtn");btn.disabled=true;btn.textContent="Running…";
try{const r=await runDailyCycle();const b=board();toast(r.skipped?`Already ran this hour · ${b[0].c.emoji} ${b[0].c.name} leads`:`Cycle done · ${r.suggestions} ideas · ${b[0].c.emoji} ${b[0].c.name} leads`);renderAll();}
catch(e){toast("Run failed: "+e.message);setStatus("error",false);}
btn.disabled=false;btn.textContent="Run cycle";
});
const b64Encode=(s)=>btoa(Array.from(new TextEncoder().encode(s),b=>String.fromCharCode(b)).join(""));
const b64Decode=(s)=>new TextDecoder().decode(Uint8Array.from(atob(s),c=>c.charCodeAt(0)));
function buildSyncCode(){
const data={version:1,exported_at:nowIso(),items:collectSyncItems()};
return "PMA1."+b64Encode(JSON.stringify(data));
}
async function readClipboardOrPrompt(){
try{if(navigator.clipboard&&navigator.clipboard.readText)return (await navigator.clipboard.readText()).trim();}
catch(e){}
return (prompt("Paste your Polymarket Arena sync code:")||"").trim();
}
$("copyStateBtn").addEventListener("click",async()=>{
const code=buildSyncCode();
try{await navigator.clipboard.writeText(code);toast("Sync code copied. Paste it on the other device with Import Sync.");}
catch(e){prompt("Copy this sync code:",code);}
});
$("importStateBtn").addEventListener("click",async()=>{
const code=await readClipboardOrPrompt();
if(!code)return;
try{
const payload=JSON.parse(b64Decode(code.replace(/^PMA1\./,"")));
if(!payload||payload.version!==1||!payload.items)throw new Error("Bad sync code");
if(!confirm("Import this sync state and replace this device's current local portfolio data?"))return;
applySyncItems(payload.items);
await pushCloudState(true);
toast("Sync imported. This device now matches the copied state.");
renderAll();showTab(location.hash.slice(1)||"portfolio");
}catch(e){toast("Import failed. Check that the sync code was copied fully.");}
});
$("resetBtn").addEventListener("click",()=>{
if(!confirm("Reset all ten agents to their $10,000 starting balance?"))return;
saveState(defaultState());localStorage.removeItem(SUG_KEY);pushCloudState(true);toast("All agents reset.");renderAll();
});
$("createPaperAccountBtn").addEventListener("click",()=>{
const name=($("paperAccountName").value||"Paper Trader").trim().slice(0,40);
const store=loadPaperStore(),acct=defaultPaperAccount(name||"Paper Trader");
store.accounts[acct.id]=acct;store.activeId=acct.id;
$("paperAccountName").value="";
savePaperStore(store);renderPaperTab();toast("Paper account created.");
});
$("copyAgentBtn").addEventListener("click",()=>{
const agentId=$("copyAgentSelect").value,book=cloneAgentBook(agentId),cfg=agentById(agentId);
if(!book)return toast("Agent book unavailable.");
if(!confirm(`Replace this paper account with ${cfg.name}'s current paper positions?`))return;
const store=loadPaperStore(),acct=activePaperAccount(store);
acct.cash=book.cash;acct.positions=book.positions;acct.history=(acct.history||[]).concat(book.history);
savePaperStore(store);renderPaperTab();showTab("paper");toast(`Copied ${cfg.name} into paper account.`);
});
window.addEventListener("focus",()=>refreshFromCloudAndRender(true));
document.addEventListener("visibilitychange",()=>{
if(!document.hidden)refreshFromCloudAndRender(true);
});
/* On load: first visit runs a 7-day backtest; afterwards the shared portfolio advances once per hour. */
(async function init(){
showTab(location.hash.slice(1)||"overview");
const cloud=await loadAuthoritativeCloudState();
let st=loadState();
if(cloud.loaded){
toast("Cloud state loaded for this device.");
}else if(cloud.ok&&st.seeded){
pushCloudState();
}else if(!cloud.ok){
setStatus("cloud sync unavailable",false);
}
renderAll();
const btn=$("runBtn");
if(!cloud.ok){
toast("Cloud sync is unavailable. Reconnect before running a cycle.");
return;
}
if(!st.seeded){
btn.disabled=true;btn.textContent="Backtesting…";
try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();st.last_cycle_hour=currentCycleHour();saveState(st);if(!(await pushCloudState()))throw new Error("Cloud sync unavailable");renderAll();
toast("7-day backtest complete — live tracking begins today.");}
catch(e){setStatus("backtest error — click Run",false);}
btn.disabled=false;btn.textContent="Run cycle";
}else if(st.last_cycle_hour!==currentCycleHour()){
btn.disabled=true;btn.textContent="Running…";
try{await runDailyCycle();renderAll();toast("Hourly update ready.");}
catch(e){setStatus("error — click Run to retry",false);}
btn.disabled=false;btn.textContent="Run cycle";
}else{setStatus("up to date",false);}
})();
</script>
</body>
</html>