Files
polymarket-analyst/index.html
T

780 lines
42 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 — AI Market Intelligence & Paper Trader</title>
<meta name="description" content="Daily AI analysis of Polymarket prediction markets with an autonomous paper-trading agent. 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)}}
/* decorative skyline/chart silhouette pinned to bottom */
.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}
.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)}
.tab{appearance:none;border:0;background:transparent;color:var(--muted);font-family:inherit;
font-size:13.5px;font-weight:600;padding:8px 15px;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}
.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)}
.dot{width:8px;height:8px;border-radius:50%;background:var(--muted);box-shadow:0 0 0 0 rgba(52,211,153,.5)}
.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:640px}
.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:560px}
.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:27px;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)}
/* ---------- equity chart ---------- */
#chart{width:100%;height:230px;display:block}
.chart-meta{display:flex;justify-content:space-between;font-size:12px;color:var(--muted);margin-top:10px}
/* ---------- 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)}
.log-date{color:var(--muted)}
.empty{color:var(--muted);font-size:13.5px;padding:14px 0;text-align:center}
/* ---------- 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 Analyst</div><div class="brand-sub">AI market intelligence · paper trader</div></div>
</div>
<div class="tabs" id="tabs">
<button class="tab" data-tab="overview">Overview</button>
<button class="tab" data-tab="suggestions">Suggestions</button>
<button class="tab" data-tab="portfolio">Portfolio</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="#a9b6ff" stroke-opacity=".5" stroke-width="2">
<path d="M20,250 L120,170 L200,210 L290,90 L380,40"/>
</g>
<path d="M20,250 L120,170 L200,210 L290,90 L380,40 L380,290 L20,290 Z" fill="url(#ha)" opacity=".5"/>
<g fill="#d9b4ff"><circle cx="120" cy="170" r="5"/><circle cx="200" cy="210" r="5"/><circle cx="290" cy="90" r="5"/><circle cx="380" cy="40" r="6"/></g>
<g opacity=".6">
<rect x="60" y="200" width="14" height="50" rx="3" fill="#34d399"/>
<rect x="160" y="220" width="14" height="40" rx="3" fill="#fb7185"/>
<rect x="250" y="150" width="14" height="80" rx="3" fill="#34d399"/>
<rect x="330" y="120" width="14" height="60" rx="3" fill="#34d399"/>
</g>
</svg>
<h1>Trade smarter on <span class="grad">prediction markets</span></h1>
<p>Every day, an AI scores live Polymarket markets for mispricing, momentum and liquidity — then an autonomous agent paper-trades the best ideas and tracks its own P&amp;L.</p>
<div class="badges">
<span class="hbadge">🔴 Live Polymarket data</span>
<span class="hbadge">🧠 Rule-based AI scoring</span>
<span class="hbadge">🤖 Autonomous paper agent</span>
<span class="hbadge">🗂️ Category filters</span>
</div>
</div>
<div class="stats" id="stats"></div>
<div class="grid2">
<div class="card hoverable">
<div class="card-h"><h3>Equity Curve</h3><span class="small muted" id="lastRun"></span></div>
<svg id="chart" viewBox="0 0 600 230" preserveAspectRatio="none"></svg>
<div class="chart-meta" id="chartMeta"></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>
<!-- ============ 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>
<!-- ============ PORTFOLIO ============ -->
<section class="tabpanel" data-tab="portfolio">
<div class="section-title">🤖 Agent Portfolio</div>
<div class="stats" id="statsPf"></div>
<div class="grid2">
<div class="card"><div class="card-h"><h3>Open Positions</h3></div><div id="positions"></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">️ How it works</div>
<div class="about-grid">
<div class="feature"><div class="fic">🔴</div><h4>Live market data</h4><p>Pulls the most active open binary markets straight from Polymarket's public Gamma API — no keys, no backend.</p></div>
<div class="feature"><div class="fic">🧮</div><h4>Four-signal model</h4><p>Each market is scored on liquidity, 24h momentum, a fair-value mispricing estimate, and time-to-resolution.</p></div>
<div class="feature"><div class="fic">🤖</div><h4>Autonomous agent</h4><p>Sizes positions with a capped fractional-Kelly rule, marks them to market daily, and settles resolved ones.</p></div>
<div class="feature"><div class="fic">🗂️</div><h4>Category focus</h4><p>Filter Politics, Sports, Crypto, Economy &amp; more — and steer the agent to trade only your chosen category.</p></div>
</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>Trade</b> — the agent opens sized paper positions, marks existing ones to market, and snapshots equity.</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. Your portfolio lives 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 Analyst — fully client-side.
Fetches live markets, scores them, and runs a paper-trading
agent. State persists in localStorage. No backend needed.
============================================================ */
const GAMMA = "https://gamma-api.polymarket.com";
const STARTING_BALANCE = 10000;
const STORE_KEY = "pma_portfolio_v1";
const SUG_KEY = "pma_suggestions_v1";
const FOCUS_KEY = "pma_focus_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"};
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
const setFocus = (c) => localStorage.setItem(FOCUS_KEY, c);
/* ---------- 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();
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;
}
/* Map Polymarket tag slugs/labels to a top-level category. */
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(),
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 dailyAvg=m.volume_1wk?m.volume_1wk/7:0;
if(dailyAvg<=0) return m.volume_24hr>0?0.3:0;
const ratio=m.volume_24hr/dailyAvg;
return clamp((ratio-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);
const drift=(mom-0.5)*0.06;
return clamp(p+drift,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 score=W.LIQ*liq+W.MOM*mom+W.MIS*mis+W.TIME*timing;
const conviction=+(score*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,
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=10){
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);
}
/* ---------- Portfolio store ---------- */
function defaultPortfolio(){
return {cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,
positions:[],closed:[],history:[],snapshots:[],last_run:null};
}
function loadPortfolio(){
try{const s=localStorage.getItem(STORE_KEY);return s?JSON.parse(s):defaultPortfolio();}
catch(e){return defaultPortfolio();}
}
function savePortfolio(p){localStorage.setItem(STORE_KEY,JSON.stringify(p));}
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 payload={date:todayStr(),generated_at:nowIso(),suggestions:sugs};
localStorage.setItem(SUG_KEY,JSON.stringify(payload));return payload;
}
/* ---------- Agent ---------- */
const MAX_POS_FRAC=0.10, MAX_NEW_PER_DAY=4, MIN_CONV=45, CASH_FLOOR=0.05;
const equity=(p)=>p.cash+p.positions.reduce((s,x)=>s+x.shares*x.current_price,0);
function kellyFraction(conv,edge){
const base=(conv/100)*Math.min(1,Math.abs(edge)/0.10);
return Math.min(MAX_POS_FRAC,0.25*base);
}
const hasPosition=(p,id)=>p.positions.some(x=>x.market_id===id);
async function markToMarket(p){
const notes=[],stillOpen=[];
for(const pos of p.positions){
const fresh=await fetchMarketPrice(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);pos.reason_closed="market resolved/closed";
p.closed.push(pos);
p.history.push({date:todayStr(),action:"CLOSE",market_id:pos.market_id,question:pos.question,
side:pos.side,detail:`Settled for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
notes.push(`Closed '${pos.question.slice(0,50)}' (resolved)`);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);
stillOpen.push(pos);
}
p.positions=stillOpen;return notes;
}
function openPositions(p,sugs,focus){
const notes=[];
const cands=sugs.filter(s=>(s.side==="YES"||s.side==="NO")&&s.conviction>=MIN_CONV
&&!hasPosition(p,s.market_id)&&(focus==="All"||!focus||s.category===focus))
.sort((a,b)=>b.conviction-a.conviction);
let opened=0;
for(const s of cands){
if(opened>=MAX_NEW_PER_DAY) break;
const eq=equity(p), investable=p.cash-eq*CASH_FLOOR;
if(investable<=50) break;
const frac=kellyFraction(s.conviction,s.edge);
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,
entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),cost,value:cost,
unrealized_pnl:0,conviction:s.conviction,rationale:s.rationale,opened_at:nowIso(),url:s.url||"",category:s.category});
p.history.push({date:todayStr(),action:"OPEN",market_id:s.market_id,question:s.question,side:s.side,
detail:`Bought ${shares} ${s.side} @ ${pct(entry)} for ${fmtUSD(cost)} (conviction ${s.conviction})`});
notes.push(`Opened ${s.side} on '${s.question.slice(0,50)}' (${fmtUSD(cost)})`);
opened++;
}
return notes;
}
function recordSnapshot(p){
const eq=equity(p), posVal=+(eq-p.cash).toFixed(2);
const snap={date:todayStr(),timestamp:nowIso(),cash:+p.cash.toFixed(2),positions_value:posVal,
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);
}
async function runDailyCycle(){
setStatus("fetching markets…",true);
const markets=await fetchMarkets(4,100);
setStatus("analyzing…",true);
const sugs=generateSuggestions(markets,12);
saveSuggestions(sugs);
const p=loadPortfolio();
setStatus("agent trading…",true);
const mtm=await markToMarket(p);
const trades=openPositions(p,sugs,getFocus());
recordSnapshot(p);
p.last_run=nowIso();
savePortfolio(p);
setStatus("up to date",false);
return {suggestions:sugs.length,actions:mtm.concat(trades),equity:+equity(p).toFixed(2)};
}
/* ---------- Rendering ---------- */
function renderAll(){renderStats();renderChart(loadPortfolio().snapshots);renderTopPicks();
renderSuggestions();renderPositions(loadPortfolio().positions);
renderHistory(loadPortfolio().history.slice(-50).reverse());}
function statCards(){
const p=loadPortfolio(), eq=equity(p), pnl=eq-p.starting_balance;
return [
{ic:"💰",label:"Equity",value:fmtUSD(eq)},
{ic:"💵",label:"Cash",value:fmtUSD(p.cash)},
{ic:"📈",label:"Total 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 positions",value:p.positions.length},
];
}
function renderStats(){
const html=statCards().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("");
$("stats").innerHTML=html;
const pf=$("statsPf"); if(pf) pf.innerHTML=html;
const p=loadPortfolio();
if(p.last_run){const d=new Date(p.last_run);$("lastRun").textContent="updated "+d.toLocaleString();}
}
function renderTopPicks(){
const root=$("topPicks"); if(!root) return;
const all=(loadSuggestions().suggestions)||[];
const top=all.slice(0,4);
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,60))}${s.question.length>60?"…":""}</div>
<div class="sub"><span class="cat-badge" style="background:${catColor(s.category)}22;color:${catColor(s.category)}">${esc(s.category)}</span>
&nbsp;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 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"
? "The agent trades the strongest picks across all categories."
: `Filtered to ${focus} — the agent 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 renderChart(snaps){
const svg=$("chart");
if(!snaps||!snaps.length){svg.innerHTML="";$("chartMeta").textContent="No history yet — run a cycle.";return;}
const pts=snaps.map(s=>s.equity);
const W=600,H=230,pad=8;
const min=Math.min(...pts,snaps[0].equity)*0.999, max=Math.max(...pts)*1.001, span=(max-min)||1;
const x=(i)=>snaps.length===1?W/2:pad+(i/(snaps.length-1))*(W-2*pad);
const y=(v)=>H-pad-((v-min)/span)*(H-2*pad);
const line=snaps.map((s,i)=>`${i===0?"M":"L"}${x(i).toFixed(1)},${y(s.equity).toFixed(1)}`).join(" ");
const area=`${line} L${x(snaps.length-1).toFixed(1)},${H-pad} L${x(0).toFixed(1)},${H-pad} Z`;
const up=pts[pts.length-1]>=snaps[0].equity, color=up?"#34d399":"#fb7185";
svg.innerHTML=`<defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${color}" stop-opacity="0.35"/>
<stop offset="100%" stop-color="${color}" stop-opacity="0"/></linearGradient></defs>
<path d="${area}" fill="url(#g)"/><path d="${line}" fill="none" stroke="${color}" stroke-width="2.5"/>`;
const last=snaps[snaps.length-1];
$("chartMeta").innerHTML=`<span>${snaps[0].date}</span><span>${snaps.length} day(s) · ${fmtPct(last.return_pct)}</span><span>${last.date}</span>`;
}
function renderPositions(positions){
const root=$("positions"); if(!root) return;
if(!positions.length){root.innerHTML=`<div class="empty">No open positions yet.</div>`;return;}
root.innerHTML=positions.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)}¢ · conv ${p.conviction}</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 esc(s){return String(s).replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));}
/* ---------- Tab routing (deep-linkable via #hash) ---------- */
const TABS=["overview","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();
toast(`Cycle done · ${r.suggestions} suggestions · equity ${fmtUSD(r.equity)}`);
renderAll();
}catch(e){toast("Run failed: "+e.message);setStatus("error",false);}
btn.disabled=false;btn.textContent="Run cycle";
});
$("resetBtn").addEventListener("click",()=>{
if(!confirm("Reset the paper portfolio to its starting balance?")) return;
savePortfolio(defaultPortfolio());localStorage.removeItem(SUG_KEY);
toast("Portfolio reset.");renderAll();
});
/* On load: route to tab, render, then auto-run once per day. */
(async function init(){
showTab(location.hash.slice(1)||"overview");
renderAll();
const sug=loadSuggestions();
if(sug.date!==todayStr()){
const btn=$("runBtn");btn.disabled=true;btn.textContent="Running…";
try{await runDailyCycle();renderAll();toast("Fresh daily analysis 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>