Files
polymarket-analyst_github/index.html
T
2026-07-05 16:58:46 -04:00

1174 lines
70 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="Daily AI analysis of Polymarket with ten competing autonomous paper-trading agents, whale-copy portfolios, and a live leaderboard. Runs entirely in your browser." />
<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,#chart2{width:100%;height:240px;display:block}
.legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:14px}
.leg{display:flex;align-items:center;gap:7px;font-size:12.5px;color:#cdd6e8}
.legdot{width:10px;height:10px;border-radius:3px;display:inline-block}
/* ---------- 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}
.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(4,minmax(110px,.55fr));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)}}
/* ---------- 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}
/* ---------- 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)}
.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:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px}
#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}
.agent-brief{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}
/* ---------- 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="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="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>The Equity Race</h3><span class="small muted" id="lastRun"></span></div>
<svg id="chart" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
<div class="legend" id="comboLegend"></div>
<div class="small muted" style="margin-top:10px">Past week is a day-by-day backtest: agents re-ran their strategy on each day's real historical prices; whales are replayed from their on-chain trade history. (Volume/liquidity signals use current values — Polymarket doesn't expose historical volume.) Live tracking begins today.</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>Equity Race — all agents</h3><span class="small muted">$10,000 start each</span></div>
<svg id="chart2" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
<div class="legend" id="comboLegend2"></div>
<div class="small muted" style="margin-top:10px">Past week is a backtest — current holdings marked against real Polymarket price history. Live tracking takes over from today.</div>
</div>
</section>
<!-- ============ SUGGESTIONS ============ -->
<section class="tabpanel" data-tab="suggestions">
<div class="section-title">💡 Daily 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>
</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 daily suggestions their own way; the <b>Copycat</b> shadows whichever strategy is winning; and the four <b>Whale Shadow</b> agents mirror the live positions of the highest-profit real traders on Polymarket (auto-discovered from its public leaderboard). All mark to market daily 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 daily 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, then every agent snapshots equity for the leaderboard.</li>
</ol>
</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. All portfolios live in this browser's local storage (each device keeps its own).</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 persists in localStorage.
============================================================ */
const GAMMA = "https://gamma-api.polymarket.com";
const STARTING_BALANCE = 10000;
const STOP_LOSS_PCT = 0.18;
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 CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
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 daily suggestions but ranks/sizes them differently. */
const AGENTS = [
{id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy",
blurb:"Backs the biggest model edge — the most mispriced markets.",
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:"Follows the fresh money — favors 24h volume surges.",
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:"Low-variance grinder — backs strong favorites likely to hit.",
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:"High-variance contrarian — hunts cheap, underpriced longshots.",
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:"Spreads many small equal-weight bets across the whole board.",
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:"Shadows whichever in-house strategy is currently winning."},
{id:"whale1", name:"Whale Shadow α", emoji:"🐋", color:"#2dd4bf", kind:"whale", slot:0,
blurb:"Mirrors the live positions of a top Polymarket trader."},
{id:"whale2", name:"Whale Shadow β", emoji:"🐳", color:"#fb923c", kind:"whale", slot:1,
blurb:"Mirrors the live positions of a top Polymarket trader."},
{id:"whale3", name:"Whale Shadow γ", emoji:"🦈", color:"#a3e635", kind:"whale", slot:2,
blurb:"Mirrors the live positions of a top Polymarket trader."},
{id:"whale4", name:"Whale Shadow δ", emoji:"🐡", color:"#60a5fa", kind:"whale", slot:3,
blurb:"Mirrors the live positions of a top Polymarket trader."},
];
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 todayStr = () => new Date().toISOString().slice(0,10);
const nowIso = () => new Date().toISOString();
let SIM_DAY = null; // set during backtest so logs/snapshots use the simulated date
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 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 generateSuggestions(markets,perCategory=12){
const actionable=markets.map(analyzeMarket).filter(Boolean).filter(a=>a.side!=="HOLD").sort((a,b)=>b.conviction-a.conviction);
const byCat={}; const out=[];
for(const a of actionable){const c=a.category||"Other";byCat[c]=(byCat[c]||0)+1;if(byCat[c]<=perCategory)out.push(a);}
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:{}};}
function defaultState(){const agents={};AGENTS.forEach(a=>agents[a.id]=defaultPortfolio());return {date:null,last_run: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(!st.whales)st.whales={};
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;}
/* ---------- 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);
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 shouldStopLoss(pos){
if(!pos.cost||pos.cost<=0)return false;
return (pos.value||0)<=pos.cost*(1-STOP_LOSS_PCT);
}
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=nowIso();
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 markToMarket(p,priceMap){
const stillOpen=[];
for(const pos of p.positions){
const fresh=priceMap[pos.market_id];
if(fresh==null){
const proceeds=pos.shares*pos.current_price; p.cash+=proceeds;
pos.exit_price=pos.current_price;pos.closed_at=nowIso();
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
p.closed.push(pos);
p.history.push({date:logDay(),action:"CLOSE",question:pos.question,side:pos.side,
detail:`Settled '${pos.question.slice(0,40)}' for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
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);
if(shouldStopLoss(pos)){stopOutPosition(p,pos);continue;}
stillOpen.push(pos);
}
p.positions=stillOpen;
}
function openPositions(p,cfg,rankedSugs,focus){
const cands=rankedSugs.filter(s=>(s.side==="YES"||s.side==="NO")&&s.conviction>=cfg.minConv
&&!hasPosition(p,s.market_id)&&!hasStoppedToday(p,s.market_id)&&(focus==="All"||!focus||s.category===focus));
let opened=0;
for(const s of cands){
if(opened>=cfg.maxNew)break;
const eq=equity(p),investable=p.cash-eq*0.05;
if(investable<=50)break;
let frac;
if(cfg.flat){frac=cfg.maxFrac;}
else{const base=(s.conviction/100)*Math.min(1,Math.abs(s.edge)/0.10);frac=Math.min(cfg.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,
unrealized_pnl:0,conviction:s.conviction,category:s.category,opened_at:nowIso(),url:s.url||""});
p.history.push({date:logDay(),action:"OPEN",question:s.question,side:s.side,
detail:`Bought ${shares} ${s.side} '${s.question.slice(0,40)}' @ ${pct(entry)} for ${fmtUSD(cost)}`});
opened++;
}
}
function recordSnapshot(p){
const eq=equity(p);
const snap={date:logDay(),timestamp:(SIM_DAY?new Date(SIM_DAY+"T12:00:00Z").toISOString():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=p.snapshots.filter(s=>s.date!==snap.date);
p.snapshots.push(snap);p.snapshots.sort((a,b)=>a.date<b.date?-1:1);
}
/* ---------- 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}));
}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
}
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){
// 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){
if(shouldStopLoss(pos)){
stopOutPosition(p,pos);
}else if(!held.has(pos.asset)){
const proceeds=pos.shares*pos.current_price;p.cash+=proceeds;
pos.exit_price=pos.current_price;pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);p.closed.push(pos);
p.history.push({date:logDay(),action:"CLOSE",question:pos.question,side:pos.side,
detail:`Trader exited '${pos.question.slice(0,36)}' — settled ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
}else keep.push(pos);
}
p.positions=keep;
const targets=[...tps].sort((a,b)=>b.value-a.value).slice(0,12); // copy their biggest 12 bets
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*0.05; if(investable<=50)break;
const weight=t.value/book;
const stake=Math.min(eq*Math.min(0.34,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,unrealized_pnl:0,conviction:null});
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){
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)){
const proceeds=pos.shares*pos.current_price;p.cash+=proceeds;
pos.exit_price=pos.current_price;pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);p.closed.push(pos);
p.history.push({date:logDay(),action:"CLOSE",question:pos.question,side:pos.side,
detail:`Leader exited '${pos.question.slice(0,36)}' — settled ${fmtUSD(proceeds)}`});
}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,unrealized_pnl:0,conviction:lp.conviction});
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,12);
const priceMap={}; snaps.forEach(s=>priceMap[s.id]={yes_price:s.yes_price,no_price:s.no_price});
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
const p=st.agents[cfg.id];
markToMarket(p,priceMap);
openPositions(p,cfg,cfg.rank(sugs),focus);
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); // today's live-ish board for the Suggestions tab
}
SIM_DAY=null;
await backtestWhales(st,dates);
// establish today's actual 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(){
setStatus("fetching markets…",true);
const markets=await fetchMarkets(4,100);
setStatus("analyzing…",true);
const sugs=generateSuggestions(markets,12);
saveSuggestions(sugs);
const 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);
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
const p=st.agents[cfg.id];
markToMarket(p,priceMap);
openPositions(p,cfg,cfg.rank(sugs),focus);
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);
try{await runWhaleAgent(st.agents[a.id],wh.wallet);}
catch(e){recordSnapshot(st.agents[a.id]);}
}
st.date=todayStr();st.last_run=nowIso();
saveState(st);
setStatus("up to date",false);
return {suggestions:sugs.length};
}
/* ---------- 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();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 today",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">${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`:""}`:"Mirrors a top Polymarket trader (found on first run).";}
if(cfg.kind==="copycat"){const L=AGENTS.find(a=>a.id===st.copycatLeader);return `🐒 Copying <b>${L?esc(L.name):"the leader"}</b>`;}
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 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 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>
</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">${Math.round(STOP_LOSS_PCT*100)}% drawdown</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");
}
function drawCombo(svgId,legendId){
const st=loadState();
const series=AGENTS.map(c=>({c,snaps:(st.agents[c.id]&&st.agents[c.id].snapshots)||[]}));
const allDates=[...new Set(series.flatMap(s=>s.snaps.map(x=>x.date)))].sort();
const svg=$(svgId); if(!svg)return;
if(!allDates.length){svg.innerHTML="";if($(legendId))$(legendId).innerHTML=`<span class="muted small">No history yet — click "Run cycle".</span>`;return;}
const W=600,H=240,pad=10; const vals=[STARTING_BALANCE];
const lines=series.map(s=>{let last=STARTING_BALANCE;const pts=allDates.map(d=>{const snap=s.snaps.find(x=>x.date===d);if(snap)last=snap.equity;vals.push(last);return last;});return {c:s.c,pts};});
const min=Math.min(...vals)*0.999,max=Math.max(...vals)*1.001,span=(max-min)||1;
const x=i=>allDates.length===1?W/2:pad+(i/(allDates.length-1))*(W-2*pad);
const y=v=>H-pad-((v-min)/span)*(H-2*pad);
let inner="";
for(const ln of lines){
const d=ln.pts.map((v,i)=>`${i===0?"M":"L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
const dot=allDates.length===1?`<circle cx="${x(0).toFixed(1)}" cy="${y(ln.pts[0]).toFixed(1)}" r="4" fill="${ln.c.color}"/>`:"";
inner+=`<path d="${d}" fill="none" stroke="${ln.c.color}" stroke-width="2.4" opacity=".95"/>${dot}`;
}
svg.innerHTML=inner;
if($(legendId)){
$(legendId).innerHTML=board().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,12);
$("focusNote").textContent=focus==="All"
? "All five agents trade from this pool — each in its own style."
: `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 today's 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">${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="${s.url}" target="_blank" rel="noopener">View ↗</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());
}
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 today'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);
return `<div class="pos"><div>
<div class="pq">${esc(p.question)}</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><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 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","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(`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";
});
$("resetBtn").addEventListener("click",()=>{
if(!confirm("Reset all ten agents to their $10,000 starting balance?"))return;
saveState(defaultState());localStorage.removeItem(SUG_KEY);toast("All agents reset.");renderAll();
});
/* On load: first visit runs a 7-day backtest; afterwards a live daily update. */
(async function init(){
showTab(location.hash.slice(1)||"overview");
renderAll();
const st=loadState();
const btn=$("runBtn");
if(!st.seeded){
btn.disabled=true;btn.textContent="Backtesting…";
try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();saveState(st);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.date!==todayStr()){
btn.disabled=true;btn.textContent="Running…";
try{await runDailyCycle();renderAll();toast("Daily 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>