mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-07-27 16:07:46 +00:00
3959 lines
248 KiB
HTML
3959 lines
248 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<meta http-equiv="Cache-Control" content="no-store, max-age=0, must-revalidate" />
|
||
<meta http-equiv="Pragma" content="no-cache" />
|
||
<meta http-equiv="Expires" content="0" />
|
||
<title>Polymarket Analyst — 10 AI Agents Compete to Trade Prediction Markets</title>
|
||
<meta name="description" content="Frequent AI analysis of Polymarket with ten competing autonomous paper-trading agents, whale-copy portfolios, and a live leaderboard." />
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏆</text></svg>">
|
||
<style>
|
||
:root{
|
||
--bg:#070b14;--bg2:#0b1120;--panel:rgba(22,29,46,.66);--panel-solid:#161d2e;
|
||
--panel-2:rgba(36,45,68,.6);--border:rgba(255,255,255,.09);--border-strong:rgba(255,255,255,.16);
|
||
--text:#eef2f9;--muted:#94a1bb;--green:#34d399;--red:#fb7185;--yellow:#fbbf24;
|
||
--accent:#7c8cff;--accent2:#a878ff;--cyan:#38d2e6;
|
||
--shadow:0 18px 50px -20px rgba(0,0,0,.7);
|
||
}
|
||
*{box-sizing:border-box}
|
||
html{scroll-behavior:smooth}
|
||
body{margin:0;font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||
background:var(--bg);color:var(--text);line-height:1.5;-webkit-font-smoothing:antialiased;
|
||
min-height:100vh;overflow-x:hidden}
|
||
h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif}
|
||
|
||
/* ---------- layered background imagery ---------- */
|
||
.bg{position:fixed;inset:0;z-index:-2;overflow:hidden;
|
||
background:
|
||
radial-gradient(1200px 700px at 80% -10%, rgba(124,140,255,.18), transparent 60%),
|
||
radial-gradient(900px 600px at 5% 10%, rgba(168,120,255,.16), transparent 55%),
|
||
radial-gradient(1000px 800px at 50% 110%, rgba(56,210,230,.12), transparent 60%),
|
||
linear-gradient(180deg,#070b14,#0a1020 40%,#070b14)}
|
||
.bg::before{content:"";position:absolute;inset:0;opacity:.5;
|
||
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M44 0H0V44' fill='none' stroke='%23ffffff' stroke-opacity='0.035' stroke-width='1'/%3E%3C/svg%3E")}
|
||
.bg .orb{position:absolute;border-radius:50%;filter:blur(70px);opacity:.55;animation:float 18s ease-in-out infinite}
|
||
.orb.a{width:480px;height:480px;left:-120px;top:-80px;background:radial-gradient(circle,#5b6cff,transparent 70%)}
|
||
.orb.b{width:520px;height:520px;right:-140px;top:140px;background:radial-gradient(circle,#a878ff,transparent 70%);animation-delay:-6s}
|
||
.orb.c{width:440px;height:440px;left:35%;bottom:-180px;background:radial-gradient(circle,#21b6c9,transparent 70%);animation-delay:-12s}
|
||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(40px,-30px)}}
|
||
.bg .chartline{position:absolute;left:0;right:0;bottom:0;width:100%;height:240px;opacity:.5}
|
||
|
||
/* ---------- app shell ---------- */
|
||
.app{max-width:1240px;margin:0 auto;padding:0 22px 60px}
|
||
.topnav{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:18px;flex-wrap:wrap;
|
||
padding:14px 18px;margin:14px 0 8px;border:1px solid var(--border);border-radius:18px;
|
||
background:rgba(10,15,28,.72);backdrop-filter:blur(16px);box-shadow:var(--shadow)}
|
||
.brand{display:flex;align-items:center;gap:11px;min-width:0}
|
||
.logo{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;font-size:20px;
|
||
background:linear-gradient(135deg,var(--accent),var(--accent2));box-shadow:0 6px 20px -6px var(--accent)}
|
||
.brand-name{font-size:18px;font-weight:700;letter-spacing:-.2px;line-height:1}
|
||
.brand-sub{font-size:11px;color:var(--muted);margin-top:2px}
|
||
.build-badge{display:inline-flex;align-items:center;margin-top:5px;padding:3px 8px;border-radius:999px;border:1px solid rgba(124,140,255,.35);background:rgba(124,140,255,.12);color:#cbd5ff;font-size:10px;font-weight:800;letter-spacing:.02em;text-transform:uppercase}
|
||
.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}
|
||
body:not(.personal-mode) [data-tab="live"]{display:none!important}
|
||
.personal-mode [data-tab="invest"]{display:none!important}
|
||
.personal-banner{display:none;margin:0 0 14px;border:1px solid rgba(251,191,36,.22);border-radius:14px;
|
||
padding:12px 14px;background:rgba(251,191,36,.08);color:#f8e7b0;font-size:13px}
|
||
.personal-mode .personal-banner{display:block}
|
||
.personal-banner b{color:#fff}
|
||
|
||
/* ---------- hero ---------- */
|
||
.hero{position:relative;border:1px solid var(--border);border-radius:22px;padding:34px 32px;margin:14px 0 22px;
|
||
overflow:hidden;background:linear-gradient(120deg,rgba(124,140,255,.16),rgba(168,120,255,.08) 50%,rgba(56,210,230,.10));box-shadow:var(--shadow)}
|
||
.hero-art{position:absolute;right:-10px;top:-10px;bottom:-10px;width:46%;opacity:.85;pointer-events:none}
|
||
.hero h1{margin:0 0 10px;font-size:34px;letter-spacing:-.8px;line-height:1.08;max-width:660px}
|
||
.hero h1 .grad{background:linear-gradient(100deg,#a9b6ff,#d9b4ff 50%,#8fe8f3);-webkit-background-clip:text;background-clip:text;color:transparent}
|
||
.hero p{margin:0;color:#c3cce0;font-size:15.5px;max-width:580px}
|
||
.hero .badges{display:flex;gap:9px;flex-wrap:wrap;margin-top:18px}
|
||
.hbadge{font-size:12px;font-weight:600;color:#dfe6f5;padding:6px 12px;border-radius:999px;background:rgba(255,255,255,.07);border:1px solid var(--border)}
|
||
|
||
/* ---------- panels & cards ---------- */
|
||
.tabpanel{display:none;animation:fade .35s ease}
|
||
.tabpanel.active{display:block}
|
||
@keyframes fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||
.grid2{display:grid;grid-template-columns:1.05fr .95fr;gap:20px}
|
||
@media (max-width:920px){.grid2{grid-template-columns:1fr}}
|
||
.card{background:var(--panel);border:1px solid var(--border);border-radius:18px;padding:20px 22px;
|
||
margin-bottom:20px;backdrop-filter:blur(14px);box-shadow:var(--shadow)}
|
||
.card.hoverable{transition:.2s}
|
||
.card.hoverable:hover{border-color:var(--border-strong);transform:translateY(-2px)}
|
||
.card-h{display:flex;align-items:center;justify-content:space-between;margin:0 0 16px}
|
||
.card-h h3{margin:0;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;font-weight:600}
|
||
.section-title{display:flex;align-items:center;gap:11px;font-size:20px;margin:6px 0 16px;font-weight:700}
|
||
.section-title::before{content:"";width:5px;height:22px;border-radius:3px;background:linear-gradient(var(--accent),var(--accent2))}
|
||
.muted{color:var(--muted)}.small{font-size:12.5px}
|
||
|
||
/* ---------- stat cards ---------- */
|
||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(165px,1fr));gap:14px;margin-bottom:20px}
|
||
.stat{position:relative;background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;overflow:hidden;backdrop-filter:blur(14px);transition:.2s}
|
||
.stat:hover{transform:translateY(-2px);border-color:var(--border-strong)}
|
||
.stat::after{content:"";position:absolute;left:0;top:0;height:3px;width:100%;background:linear-gradient(90deg,var(--accent),var(--accent2))}
|
||
.stat .ic{font-size:18px;opacity:.9}
|
||
.stat .label{color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.6px;margin-top:8px}
|
||
.stat .value{font-size:25px;font-weight:700;margin-top:4px;font-family:'Space Grotesk',sans-serif;letter-spacing:-.5px}
|
||
.pos-val{color:var(--green)}.neg-val{color:var(--red)}
|
||
|
||
/* ---------- charts ---------- */
|
||
.chart-wrap{width:min(100%,1120px);margin:10px auto 0}
|
||
#chart,#chart2{width:100%;height:auto;display:block;overflow:visible}
|
||
.legend{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px 12px;margin-top:14px}
|
||
.leg{display:flex;align-items:center;gap:8px;font-size:12px;color:#cdd6e8;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:7px 9px;min-width:0}
|
||
.leg b{margin-left:auto;font-family:'Space Grotesk',sans-serif}
|
||
.legdot{width:9px;height:9px;border-radius:50%;display:inline-block;box-shadow:0 0 0 3px rgba(255,255,255,.06)}
|
||
.rangebar{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}
|
||
.rangebtn{appearance:none;border:1px solid var(--border);background:rgba(255,255,255,.04);color:var(--muted);
|
||
font:700 11px 'Inter',sans-serif;padding:5px 8px;border-radius:8px;cursor:pointer}
|
||
.rangebtn:hover{color:var(--text);border-color:var(--border-strong)}
|
||
.rangebtn.active{color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent2));border-color:transparent}
|
||
|
||
/* ---------- leaderboard ---------- */
|
||
.lb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(225px,1fr));gap:16px;margin-bottom:20px}
|
||
.lb-card{position:relative;background:var(--panel);border:1px solid var(--border);border-radius:18px;padding:18px;
|
||
backdrop-filter:blur(14px);cursor:pointer;transition:.2s;overflow:hidden}
|
||
.lb-card:hover{transform:translateY(-3px);border-color:var(--border-strong);box-shadow:var(--shadow)}
|
||
.lb-top{display:flex;align-items:center;gap:10px;margin-bottom:14px}
|
||
.medal{font-size:20px;min-width:26px}
|
||
.lb-emoji{font-size:24px}
|
||
.lb-name{font-weight:700;font-size:15px;font-family:'Space Grotesk'}
|
||
.lb-blurb{font-size:11.5px;color:var(--muted);line-height:1.4;margin-top:2px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
||
.lb-mid{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:12px}
|
||
.lb-eq{font-size:26px;font-weight:700;font-family:'Space Grotesk';letter-spacing:-.5px}
|
||
.lb-ret{font-size:15px;font-weight:700;font-family:'Space Grotesk'}
|
||
.lb-foot{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
||
.rankpill{position:absolute;top:14px;right:14px;font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;background:rgba(255,255,255,.07);border:1px solid var(--border);color:var(--muted)}
|
||
|
||
/* ---------- segmented agent selector ---------- */
|
||
.seg{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:18px}
|
||
.segbtn{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:600;padding:8px 13px;border-radius:11px;cursor:pointer;
|
||
background:rgba(255,255,255,.05);border:1px solid var(--border);color:var(--muted);transition:.16s;user-select:none}
|
||
.segbtn:hover{color:var(--text);border-color:var(--border-strong)}
|
||
.segbtn.active{color:#fff;border-color:transparent}
|
||
|
||
.agent-brief{display:grid;grid-template-columns:1.2fr repeat(5,minmax(105px,.5fr));gap:14px;margin-bottom:18px}
|
||
.brief-main{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;backdrop-filter:blur(14px)}
|
||
.brief-title{display:flex;align-items:center;gap:10px;font-family:'Space Grotesk';font-weight:700;font-size:18px;margin-bottom:7px}
|
||
.brief-main p{margin:0;color:#cdd6e8;font-size:13.5px}
|
||
.brief-mini{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:14px;padding:14px}
|
||
.brief-mini .k{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
|
||
.brief-mini .v{margin-top:5px;font-size:14px;font-weight:700}
|
||
.card-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||
.select{appearance:none;border:1px solid var(--border);border-radius:9px;background:rgba(255,255,255,.05);color:var(--text);
|
||
font:600 12.5px 'Inter',sans-serif;padding:7px 28px 7px 10px;cursor:pointer}
|
||
.select:focus{outline:1px solid var(--accent)}
|
||
.loading-empty{display:grid;gap:10px;place-items:center;padding:28px 12px;text-align:center}
|
||
.loading-ring{width:34px;height:34px;border-radius:50%;border:3px solid rgba(255,255,255,.12);border-top-color:var(--accent);animation:spin .9s linear infinite}
|
||
.empty-copy{max-width:420px;color:var(--muted);font-size:13.5px}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.report-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(255px,1fr));gap:14px}
|
||
.report{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:15px;padding:16px;border-top:3px solid var(--accent)}
|
||
.report-title{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
|
||
.report-name{font-family:'Space Grotesk';font-weight:700}
|
||
.report-rank{font-size:11px;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:3px 8px;white-space:nowrap}
|
||
.report p{margin:0 0 9px;color:#cdd6e8;font-size:13px;line-height:1.5}
|
||
.report p:last-child{margin-bottom:0}
|
||
.report b{color:var(--text)}
|
||
.agent-chat{display:grid;grid-template-columns:.8fr 1.2fr;gap:16px}
|
||
.chat-panel{background:rgba(255,255,255,.035);border:1px solid var(--border);border-radius:15px;padding:14px}
|
||
.chat-persona{display:flex;align-items:center;gap:10px;margin-bottom:10px}
|
||
.chat-avatar{font-size:28px;width:42px;height:42px;display:grid;place-items:center;border-radius:13px;background:rgba(255,255,255,.06);border:1px solid var(--border)}
|
||
.chat-name{font-family:'Space Grotesk';font-weight:700}
|
||
.chat-meta{font-size:12px;color:var(--muted);margin-top:2px}
|
||
.chat-prompts{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
|
||
.chat-prompt{border:1px solid var(--border);background:rgba(255,255,255,.045);color:var(--text);border-radius:999px;padding:7px 10px;font:700 11.5px 'Inter';cursor:pointer}
|
||
.chat-prompt:hover{border-color:var(--accent)}
|
||
.chat-window{display:flex;flex-direction:column;gap:10px;max-height:420px;overflow:auto;padding-right:4px}
|
||
.chat-msg{border:1px solid var(--border);border-radius:14px;padding:11px 13px;font-size:13px;line-height:1.45;white-space:pre-wrap}
|
||
.chat-msg.user{align-self:flex-end;max-width:82%;background:rgba(124,140,255,.16);border-color:rgba(124,140,255,.35)}
|
||
.chat-msg.agent{align-self:flex-start;max-width:92%;background:rgba(255,255,255,.045)}
|
||
.chat-msg.pending{color:var(--muted);font-style:italic}
|
||
.chat-input-row{display:flex;gap:8px;margin-top:12px}
|
||
.chat-input-row .input{flex:1}
|
||
@media (max-width:760px){.agent-chat{grid-template-columns:1fr}.chat-msg.user,.chat-msg.agent{max-width:100%}}
|
||
|
||
/* ---------- suggestions ---------- */
|
||
.filterbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}
|
||
.chip{font-size:13px;font-weight:600;padding:7px 14px;border-radius:999px;cursor:pointer;background:rgba(255,255,255,.05);border:1px solid var(--border);color:var(--muted);transition:.16s;user-select:none}
|
||
.chip:hover{border-color:var(--accent);color:var(--text)}
|
||
.chip.active{background:linear-gradient(135deg,var(--accent),var(--accent2));border-color:transparent;color:#fff}
|
||
.chip .ct{opacity:.75;font-size:11px;margin-left:5px}
|
||
.sug-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||
@media (max-width:760px){.sug-grid{grid-template-columns:1fr}}
|
||
.sug{position:relative;background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;border-left:4px solid var(--accent);backdrop-filter:blur(14px);transition:.2s}
|
||
.sug:hover{transform:translateY(-3px);border-color:var(--border-strong);box-shadow:var(--shadow)}
|
||
.sug .q{font-weight:600;font-size:15px;line-height:1.35;margin-top:6px}
|
||
.sug .event{color:var(--muted);font-size:12px;margin:3px 0 12px}
|
||
.sug-row{display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-bottom:9px}
|
||
.cat-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 9px;border-radius:7px}
|
||
.pill{font-size:12px;font-weight:700;padding:4px 11px;border-radius:999px}
|
||
.pill.YES{background:rgba(52,211,153,.16);color:var(--green)}
|
||
.pill.NO{background:rgba(251,113,133,.16);color:var(--red)}
|
||
.conv-bar{flex:1;min-width:90px;height:7px;background:rgba(255,255,255,.07);border-radius:999px;overflow:hidden}
|
||
.conv-bar>span{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--green))}
|
||
.rationale{font-size:13px;color:#cdd6e8;margin:10px 0}
|
||
.drivers{display:flex;gap:6px;flex-wrap:wrap}
|
||
.driver{font-size:11px;background:rgba(255,255,255,.05);border:1px solid var(--border);padding:3px 9px;border-radius:7px;color:var(--muted)}
|
||
.metrics{display:flex;gap:16px;font-size:12px;color:var(--muted);margin-top:12px;flex-wrap:wrap}
|
||
.metrics b{color:var(--text)}
|
||
a.market-link{color:var(--accent);text-decoration:none;font-size:12px;font-weight:600}
|
||
a.market-link:hover{text-decoration:underline}
|
||
a.market-title{color:var(--text);text-decoration:none}
|
||
a.market-title:hover{color:#fff;text-decoration:underline;text-decoration-color:var(--accent)}
|
||
|
||
/* ---------- positions / log ---------- */
|
||
.pos{display:flex;justify-content:space-between;align-items:center;padding:13px 0;border-bottom:1px solid var(--border)}
|
||
.pos:last-child{border-bottom:none}
|
||
.pos .pq{font-size:13.5px;max-width:62%;font-weight:500}
|
||
.pos .sub{color:var(--muted);font-size:11.5px;margin-top:3px}
|
||
.right{text-align:right}
|
||
.log-item{font-size:12.5px;padding:9px 0;border-bottom:1px solid var(--border);color:#cdd6e8}
|
||
.log-item:last-child{border-bottom:none}
|
||
.badge{font-weight:700;font-size:10px;padding:2px 7px;border-radius:5px;margin-right:7px}
|
||
.badge.OPEN{background:rgba(124,140,255,.2);color:var(--accent)}
|
||
.badge.CLOSE{background:rgba(251,191,36,.2);color:var(--yellow)}
|
||
.badge.STOP{background:rgba(251,113,133,.2);color:var(--red)}
|
||
.badge.GAIN{background:rgba(52,211,153,.18);color:var(--green)}
|
||
.badge.EXIT{background:rgba(251,191,36,.18);color:var(--yellow)}
|
||
.log-date{color:var(--muted)}
|
||
.empty{color:var(--muted);font-size:13.5px;padding:14px 0;text-align:center}
|
||
|
||
@media (max-width:700px){
|
||
.app{padding:0 10px 42px;max-width:100%;overflow:hidden}
|
||
.topnav{display:grid;grid-template-columns:1fr;gap:12px;padding:14px;margin:10px 0 8px;border-radius:14px}
|
||
.brand-sub{white-space:normal}
|
||
.tabs{width:100%;padding-bottom:5px}
|
||
.tab{padding:8px 12px}
|
||
.nav-right{width:100%;margin-left:0;display:flex;align-items:stretch;flex-wrap:wrap;gap:8px}
|
||
.nav-right .status{flex:1 1 100%}
|
||
#statusText{max-width:none;white-space:normal}
|
||
.status{align-items:flex-start}
|
||
.btn{padding:9px 11px}
|
||
.section-title{font-size:19px}
|
||
.stats{grid-template-columns:1fr;gap:12px}
|
||
.chart-wrap{width:100%;margin-top:8px}
|
||
.legend{grid-template-columns:1fr}
|
||
.agent-brief{grid-template-columns:1fr}
|
||
.invest-grid{grid-template-columns:1fr}
|
||
.invest-agent{grid-template-columns:auto 1fr}
|
||
.invest-agent input{grid-column:1 / -1;width:100%}
|
||
.paper-grid{grid-template-columns:1fr}
|
||
.ta-grid{grid-template-columns:1fr}
|
||
.money-row{grid-template-columns:1fr}
|
||
.pos{align-items:flex-start;gap:12px}
|
||
.pos .pq{max-width:none}
|
||
.right{min-width:92px}
|
||
}
|
||
|
||
/* ---------- about ---------- */
|
||
.about-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;margin-bottom:8px}
|
||
.feature{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:20px;backdrop-filter:blur(14px)}
|
||
.feature .fic{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;font-size:20px;margin-bottom:12px;background:linear-gradient(135deg,rgba(124,140,255,.25),rgba(168,120,255,.18));border:1px solid var(--border)}
|
||
.feature h4{margin:0 0 7px;font-size:15px}
|
||
.feature p{margin:0;font-size:13px;color:var(--muted);line-height:1.55}
|
||
.steps{counter-reset:s;list-style:none;padding:0;margin:0}
|
||
.steps li{position:relative;padding:0 0 16px 44px;font-size:14px;color:#cdd6e8}
|
||
.steps li::before{counter-increment:s;content:counter(s);position:absolute;left:0;top:-2px;width:30px;height:30px;border-radius:9px;display:grid;place-items:center;font-weight:700;font-size:13px;color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent2))}
|
||
.disclaimer{border:1px solid rgba(251,191,36,.3);background:rgba(251,191,36,.07);border-radius:14px;padding:16px 18px;font-size:13px;color:#e6dcc2;margin-top:8px}
|
||
.invest-grid{display:grid;grid-template-columns:1.05fr .95fr;gap:18px}
|
||
.invest-agent{display:grid;grid-template-columns:auto 1fr auto;gap:13px;align-items:center;padding:14px 0;border-bottom:1px solid var(--border)}
|
||
.invest-agent:last-child{border-bottom:none}
|
||
.invest-agent input{width:112px;border:1px solid var(--border);border-radius:9px;background:rgba(255,255,255,.05);color:var(--text);font:700 13px 'Inter',sans-serif;padding:8px 10px}
|
||
.invest-agent input:focus{outline:1px solid var(--accent)}
|
||
.invest-name{font-family:'Space Grotesk';font-weight:700}
|
||
.invest-copy{color:var(--muted);font-size:12.5px;margin-top:2px}
|
||
.invest-total{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:14px}
|
||
.invest-num{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:14px;padding:14px}
|
||
.invest-num .k{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.6px}
|
||
.invest-num .v{font-family:'Space Grotesk';font-size:24px;font-weight:700;margin-top:3px}
|
||
.checklist{list-style:none;margin:0;padding:0;display:grid;gap:10px}
|
||
.checklist li{display:flex;gap:9px;color:#cdd6e8;font-size:13.5px}
|
||
.checklist li::before{content:"";width:9px;height:9px;border-radius:50%;margin-top:6px;background:var(--accent);box-shadow:0 0 0 4px rgba(124,140,255,.12);flex:0 0 auto}
|
||
.exposure-row{display:flex;justify-content:space-between;gap:12px;padding:10px 0;border-bottom:1px solid var(--border);font-size:13px}
|
||
.exposure-row:last-child{border-bottom:none}
|
||
.risk-note{border:1px solid rgba(251,113,133,.28);background:rgba(251,113,133,.08);border-radius:14px;padding:14px 16px;font-size:13px;color:#f4c6cf}
|
||
.paper-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:18px}
|
||
.account-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
|
||
.input{border:1px solid var(--border);border-radius:9px;background:rgba(255,255,255,.05);color:var(--text);font:600 13px 'Inter',sans-serif;padding:9px 10px;min-width:0}
|
||
.input:focus{outline:1px solid var(--accent)}
|
||
.trade-list{display:grid;gap:12px}
|
||
.trade-card{border:1px solid var(--border);border-radius:14px;background:rgba(255,255,255,.035);padding:14px}
|
||
.trade-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:8px}
|
||
.trade-title{font-weight:700;font-size:13.5px;line-height:1.35}
|
||
.trade-actions{display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-top:10px}
|
||
.trade-actions .input{width:98px}
|
||
.trade-meta{display:flex;gap:10px;flex-wrap:wrap;color:var(--muted);font-size:11.5px}
|
||
.exact-table{display:grid;gap:10px}
|
||
.ta-grid{display:grid;grid-template-columns:1fr .6fr;gap:16px}
|
||
.ta-chart{width:100%;height:auto;display:block;overflow:visible}
|
||
.money-row{display:grid;grid-template-columns:1fr 1fr auto auto;gap:8px;margin-top:12px}
|
||
.leader-row{display:flex;justify-content:space-between;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)}
|
||
.leader-row:last-child{border-bottom:none}
|
||
.locked-panel{border:1px solid rgba(124,140,255,.28);background:rgba(124,140,255,.08);border-radius:14px;padding:14px;margin:12px 0}
|
||
|
||
/* ---------- 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 & whales</div><div class="build-badge">Top 5,000 analysis · v16</div></div>
|
||
</div>
|
||
<div class="tabs" id="tabs">
|
||
<button class="tab" data-tab="overview">Overview</button>
|
||
<button class="tab" data-tab="leaderboard">Leaderboard</button>
|
||
<button class="tab" data-tab="suggestions">Suggestions</button>
|
||
<button class="tab" data-tab="portfolio">Portfolios</button>
|
||
<button class="tab" data-tab="invest">Invest</button>
|
||
<button class="tab" data-tab="paper">Paper Trade</button>
|
||
<button class="tab" data-tab="live">Live Money</button>
|
||
<button class="tab" data-tab="about">About</button>
|
||
</div>
|
||
<div class="nav-right">
|
||
<span class="status"><span class="dot" id="statusDot"></span><span id="statusText">idle</span></span>
|
||
<button id="runBtn" class="btn primary">Run cycle</button>
|
||
<button id="copyStateBtn" class="btn ghost">Copy Sync</button>
|
||
<button id="importStateBtn" class="btn ghost">Import Sync</button>
|
||
</div>
|
||
</nav>
|
||
<div class="personal-banner" id="personalBanner">
|
||
<b>Personal research mode.</b> This copy is for your own analysis, paper tracking, and manual trade research only. It does not pool money, onboard investors, custody funds, bypass eligibility rules, or place orders without your manual approval.
|
||
</div>
|
||
|
||
<!-- ============ 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 follows the strongest MACD/momentum profile, and four agents that mirror the live positions of the top traders on Polymarket — all paper-trading for the top of the leaderboard.</p>
|
||
<div class="badges">
|
||
<span class="hbadge">🏆 10 competing agents</span>
|
||
<span class="hbadge">🐋 Copies real Polymarket whales</span>
|
||
<span class="hbadge">🐒 Copycat meta-agent</span>
|
||
<span class="hbadge">🔴 Live data</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats" id="stats"></div>
|
||
|
||
<div class="grid2">
|
||
<div class="card hoverable">
|
||
<div class="card-h"><h3>Agent Returns</h3><div><span class="small muted" id="lastRun"></span><div class="rangebar" data-chart-ranges></div></div></div>
|
||
<div class="chart-wrap"><svg id="chart" viewBox="0 0 960 320" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||
<div class="legend" id="comboLegend"></div>
|
||
<div class="small muted" style="margin-top:10px">Use the range buttons to zoom from one week to all history. New live points are added whenever you click "Run cycle"; the dashed line tracks the average agent return.</div>
|
||
</div>
|
||
<div class="card hoverable">
|
||
<div class="card-h"><h3>Top Picks Today</h3><span class="small muted">highest conviction</span></div>
|
||
<div id="topPicks"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-h"><h3>Trade Email Alerts</h3><span class="small muted" id="emailAlertMeta">off</span></div>
|
||
<div class="account-row">
|
||
<input class="input" id="emailAlertInput" type="email" placeholder="you@example.com">
|
||
<button class="btn" id="saveEmailAlertsBtn">Save alerts</button>
|
||
<button class="btn ghost" id="testEmailAlertsBtn">Send test</button>
|
||
</div>
|
||
<div class="chiprow" id="emailAlertEvents" style="margin-top:10px"></div>
|
||
<div class="small muted" style="margin-top:12px">Agents to include</div>
|
||
<div class="chiprow" id="emailAlertAgents" style="margin-top:8px"></div>
|
||
<div class="small muted" id="emailAlertStatus" style="margin-top:8px">Send a digest whenever agents open, close, stop out, or take gains during a cycle.</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ LEADERBOARD ============ -->
|
||
<section class="tabpanel" data-tab="leaderboard">
|
||
<div class="section-title">🏆 Leaderboard <span class="small muted" id="lbSub" style="font-weight:400"></span></div>
|
||
<div class="lb-grid" id="leaderboard"></div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Returns — all agents</h3><div><span class="small muted">$10,000 start each</span><div class="rangebar" data-chart-ranges></div></div></div>
|
||
<div class="chart-wrap"><svg id="chart2" viewBox="0 0 960 320" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||
<div class="legend" id="comboLegend2"></div>
|
||
<div class="small muted" style="margin-top:10px">Past week is a backtest. After that, each live cycle adds another return snapshot for every agent.</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ SUGGESTIONS ============ -->
|
||
<section class="tabpanel" data-tab="suggestions">
|
||
<div class="section-title">💡 Live 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="card">
|
||
<div class="card-h"><h3>Chat With This Agent</h3><span class="small muted">ask about performance, risk, trades, or plans</span></div>
|
||
<div id="agentChatRoot"></div>
|
||
</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&L</option>
|
||
<option value="pnl_asc">Worst P&L</option>
|
||
<option value="value_desc">Largest Value</option>
|
||
<option value="newest">Newest</option>
|
||
<option value="category">Category</option>
|
||
</select></div></div><div id="positions" aria-live="polite"></div></div>
|
||
<div class="card"><div class="card-h"><h3>Activity Log</h3></div><div id="history"></div></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Technical Analysis</h3><span class="small muted">candles, moving averages, MACD, activity</span></div>
|
||
<div class="ta-grid">
|
||
<div><svg id="agentTAChart" class="ta-chart" viewBox="0 0 960 420" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||
<div id="agentTAReadout"></div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Daily Agent Performance Reports</h3><span class="small muted" id="reportDate"></span></div>
|
||
<div class="report-grid" id="agentReports"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ INVEST ============ -->
|
||
<section class="tabpanel" data-tab="invest">
|
||
<div class="section-title">💸 Invest in Agents</div>
|
||
<div class="invest-grid">
|
||
<div class="card">
|
||
<div class="card-h"><h3>Allocation draft</h3><span class="small muted">planning only</span></div>
|
||
<div class="invest-total" id="investTotals"></div>
|
||
<div id="investAgents"></div>
|
||
</div>
|
||
<div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>How real money should work</h3></div>
|
||
<ol class="steps">
|
||
<li><b>User owns funds</b> — connect a wallet or regulated brokerage account; the site should not custody deposits directly.</li>
|
||
<li><b>Consent per agent</b> — users choose which agents can trade and the maximum amount each may control.</li>
|
||
<li><b>Execution layer</b> — agent signals become signed user orders through Polymarket's trading flow or a compliant partner.</li>
|
||
<li><b>Risk controls</b> — stop-loss, gain-stop, exposure caps, market exclusions, and emergency pause stay visible before any order is sent.</li>
|
||
</ol>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Compliance checklist</h3></div>
|
||
<ul class="checklist">
|
||
<li>Eligibility, location, age, KYC, and sanctions screening before real trading.</li>
|
||
<li>Clear disclosure that prediction markets are risky and users can lose the full amount committed.</li>
|
||
<li>No pooled customer funds unless handled by a properly regulated structure.</li>
|
||
<li>Audit logs for every signal, user approval, order, fill, stop, and withdrawal.</li>
|
||
</ul>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Execution status</h3><span class="small muted">not enabled</span></div>
|
||
<p class="muted" style="margin-top:0">Real orders require user-signed trading credentials and an eligible account. The current site only creates an allocation draft.</p>
|
||
<div class="card-tools">
|
||
<a class="btn ghost" href="https://docs.polymarket.com/trading/overview" target="_blank" rel="noopener">Trading docs</a>
|
||
<a class="btn ghost" href="https://polymarket.com/terms-of-use" target="_blank" rel="noopener">Terms</a>
|
||
<button class="btn primary" disabled>Wallet trading locked</button>
|
||
</div>
|
||
</div>
|
||
<div class="risk-note">
|
||
<b>Important:</b> this site still does not move money or place orders. This tab is the bridge design: it lets people plan allocations and shows the safe path toward wallet-based or regulated real-money execution.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="margin-top:18px">
|
||
<div class="card-h"><h3>Exact trades agents are holding</h3><span class="small muted">side, size, price, link</span></div>
|
||
<div class="exact-table" id="exactTrades"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ PAPER TRADING ============ -->
|
||
<section class="tabpanel" data-tab="paper">
|
||
<div class="section-title">🧾 Paper Trading Accounts</div>
|
||
<div class="paper-grid">
|
||
<div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Account</h3><span class="small muted">cloud paper money when password-backed</span></div>
|
||
<div class="account-row">
|
||
<select class="select" id="paperAccountSelect" aria-label="Paper account"></select>
|
||
<input class="input" id="paperAccountName" placeholder="New account name" />
|
||
<input class="input" id="paperAccountPassword" type="password" placeholder="Password optional" />
|
||
<button class="btn primary" id="createPaperAccountBtn">Create</button>
|
||
<button class="btn" id="paperLoginBtn">Log in</button>
|
||
<button class="btn ghost" id="paperSignOutBtn">Sign out</button>
|
||
</div>
|
||
<div id="paperLockPanel"></div>
|
||
<div class="stats" id="paperStats"></div>
|
||
<div class="money-row">
|
||
<input class="input" id="paperMoneyAmount" type="number" min="1" step="25" placeholder="Amount" />
|
||
<input class="input" id="paperPasswordInput" type="password" placeholder="Password" />
|
||
<button class="btn" id="paperDepositBtn">Deposit</button>
|
||
<button class="btn ghost" id="paperWithdrawBtn">Withdraw</button>
|
||
</div>
|
||
<div class="card-tools">
|
||
<select class="select" id="copyAgentSelect" aria-label="Agent to copy"></select>
|
||
<button class="btn" id="copyAgentBtn">Copy agent exactly</button>
|
||
</div>
|
||
<div class="card-tools" style="margin-top:10px">
|
||
<select class="select" id="buyAgentSelect" aria-label="Agent to buy"></select>
|
||
<input class="input" id="buyAgentAmount" type="number" min="1" step="25" placeholder="Amount" />
|
||
<button class="btn" id="buyAgentBtn">Buy agent shares</button>
|
||
</div>
|
||
<p class="small muted" style="margin:12px 0 0">Deposits and withdrawals are paper-cash controls. Accounts created with a password sync through the backend so the same portfolio can load on another device. Copying an agent replaces this paper account with that agent's current cash and open positions.</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Paper account leaderboard</h3><span class="small muted">best returns</span></div>
|
||
<div id="paperLeaderboard"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Portfolio value history</h3><span class="small muted">paper equity over time</span></div>
|
||
<svg id="paperPortfolioChart" class="ta-chart" viewBox="0 0 720 260" preserveAspectRatio="xMidYMid meet"></svg>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Paper positions</h3></div>
|
||
<div id="paperPositions"></div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Browse Polymarket</h3><span class="small muted">daily ideas or search all markets</span></div>
|
||
<div class="account-row">
|
||
<input class="input" id="paperSearchInput" placeholder="Search all Polymarket markets" />
|
||
<button class="btn" id="paperSearchBtn">Search</button>
|
||
<button class="btn" id="paperAllBtn">All active</button>
|
||
<button class="btn ghost" id="paperMoreBtn">Load more</button>
|
||
<button class="btn ghost" id="paperClearSearchBtn">Daily ideas</button>
|
||
</div>
|
||
<div class="filterbar" id="paperFilterbar"></div>
|
||
<div class="trade-list" id="paperMarketList"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Paper activity</h3></div>
|
||
<div id="paperHistory"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ PERSONAL TRADING COCKPIT ============ -->
|
||
<section class="tabpanel" data-tab="live">
|
||
<div class="section-title">🔐 Personal Trading Cockpit</div>
|
||
<div class="disclaimer" style="margin-bottom:16px"><b>Private use only.</b> This area is hidden from the public site and is for your own wallet, your own money, and manual approval only. It does not accept investor deposits, pool funds, sell agent access, or execute unattended trades.</div>
|
||
<div class="grid2">
|
||
<div class="card">
|
||
<div class="card-h"><h3>Personal safety checklist</h3><span class="small muted" id="liveReadyPct"></span></div>
|
||
<div id="liveChecklist"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Your wallet & limits</h3><span class="small muted">personal only</span></div>
|
||
<div class="account-row">
|
||
<input class="input" id="liveWalletAddress" placeholder="Your Polymarket deposit wallet address" />
|
||
<select class="select" id="liveJurisdiction">
|
||
<option value="">Jurisdiction</option><option>United States</option><option>Canada</option><option>European Union</option><option>Other</option>
|
||
</select>
|
||
</div>
|
||
<div class="money-row">
|
||
<input class="input" id="liveDepositLimit" type="number" min="0" step="100" placeholder="Max deposit" />
|
||
<input class="input" id="liveWithdrawReserve" type="number" min="0" step="100" placeholder="Withdrawal reserve" />
|
||
<button class="btn" id="saveLiveProfileBtn">Save profile</button>
|
||
<button class="btn primary" disabled>No public deposits</button>
|
||
</div>
|
||
<div class="risk-note" style="margin-top:12px">Fund only your own wallet directly through approved provider flows. This site will not accept, hold, or withdraw money for other people.</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Personal capital & agent shares</h3><span class="small muted">synced tracker, no custody</span></div>
|
||
<div id="personalCapitalSummary"></div>
|
||
<div class="account-row">
|
||
<input class="input" id="personalCapitalAmount" type="number" min="1" step="25" placeholder="Amount" />
|
||
<select class="select" id="personalCapitalAgent"></select>
|
||
<button class="btn" id="personalDepositBtn">Record external deposit</button>
|
||
<button class="btn ghost" id="personalWithdrawBtn">Record withdrawal</button>
|
||
<button class="btn primary" id="personalBuyAgentBtn">Allocate to agent</button>
|
||
<button class="btn ghost" id="personalSellAgentBtn">Reduce agent</button>
|
||
</div>
|
||
<div class="risk-note" style="margin-top:12px">This tracks money you personally control outside Poly Arena. Agent shares are bookkeeping allocations only, not securities, pooled deposits, or orders placed by the site.</div>
|
||
<div id="personalAgentAllocations" style="margin-top:12px"></div>
|
||
<div id="personalCapitalHistory" style="margin-top:12px"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Agent permissions & risk limits</h3><span class="small muted">manual approval only</span></div>
|
||
<div id="liveAgentPermissions"></div>
|
||
</div>
|
||
<div class="grid2">
|
||
<div class="card">
|
||
<div class="card-h"><h3>Manual order console</h3><span class="small muted">review before any trade</span></div>
|
||
<div id="liveBackendStatus" class="locked-panel">Checking backend readiness...</div>
|
||
<div class="account-row">
|
||
<select class="select" id="liveIntentAgent"></select>
|
||
<input class="input" id="liveIntentMarket" placeholder="Market ID" />
|
||
<input class="input" id="liveIntentQuestion" placeholder="Market question / note" />
|
||
<select class="select" id="liveIntentSide"><option>YES</option><option>NO</option></select>
|
||
<input class="input" id="liveIntentAmount" type="number" min="1" step="25" placeholder="Max amount" />
|
||
<input class="input" id="liveIntentPrice" type="number" min="0.01" max="0.99" step="0.01" placeholder="Limit price" />
|
||
<button class="btn" id="dryRunOrderBtn">Stage manual intent</button>
|
||
</div>
|
||
<ol class="steps">
|
||
<li><b>Research signal</b> — choose an agent idea and inspect the exact market yourself.</li>
|
||
<li><b>Manual review</b> — confirm side, amount, price, risk cap, and current market status.</li>
|
||
<li><b>Your wallet only</b> — sign or submit only from your own Polymarket/deposit wallet.</li>
|
||
<li><b>Emergency pause</b> — keep agent automation off and cancel staged intents any time.</li>
|
||
</ol>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Personal audit trail</h3><span class="small muted">local + Neon records</span></div>
|
||
<div id="liveOrderDryRun"></div>
|
||
<div id="liveAuditTrail"></div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Browse Polymarket for manual tickets</h3><span class="small muted">search active markets</span></div>
|
||
<div class="account-row">
|
||
<input class="input" id="liveSearchInput" placeholder="Search Polymarket markets" />
|
||
<button class="btn" id="liveSearchBtn">Search</button>
|
||
<button class="btn" id="liveAllBtn">All active</button>
|
||
<button class="btn ghost" id="liveMoreBtn">Load more</button>
|
||
<button class="btn ghost" id="liveClearSearchBtn">Clear</button>
|
||
</div>
|
||
<div class="filterbar" id="liveMarketFilterbar"></div>
|
||
<div class="trade-list" id="liveMarketList"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Manual trade ticket queue</h3><span class="small muted">AI suggests, you sign elsewhere</span></div>
|
||
<div id="personalTicketQueue"></div>
|
||
</div>
|
||
<div class="grid2">
|
||
<div class="card">
|
||
<div class="card-h"><h3>Tracked real positions</h3><span class="small muted">manual fills only</span></div>
|
||
<div id="realPositionSummary"></div>
|
||
<div id="realPositions"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Manual fill history</h3><span class="small muted">no private key used</span></div>
|
||
<div id="realFills"></div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Provider webhooks</h3><span class="small muted">events are stored in Neon</span></div>
|
||
<div id="liveWebhookStatus" class="small muted">Checking webhook setup...</div>
|
||
</div>
|
||
<div class="grid2">
|
||
<div class="card">
|
||
<div class="card-h"><h3>Policies & consent</h3><span class="small muted">required before deposits</span></div>
|
||
<div id="livePolicyStatus" class="locked-panel">Checking policy status...</div>
|
||
<div class="account-row">
|
||
<label class="small muted"><input type="checkbox" id="liveAcceptTerms"> terms</label>
|
||
<label class="small muted"><input type="checkbox" id="liveAcceptPrivacy"> privacy</label>
|
||
<label class="small muted"><input type="checkbox" id="liveAcceptRisk"> risk disclosure</label>
|
||
<button class="btn" id="recordConsentBtn">Record consent</button>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Server controls</h3><span class="small muted">live safety gates</span></div>
|
||
<div class="locked-panel">
|
||
<b>Public production switch remains off</b>
|
||
<div class="small muted">The public product remains paper trading only. Personal mode can store your wallet limits, consent status, staged manual intents, webhooks, and incident records, but does not onboard investors or pool money.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ ABOUT ============ -->
|
||
<section class="tabpanel" data-tab="about">
|
||
<div class="section-title">ℹ️ The competition</div>
|
||
<p class="muted" style="margin-top:-6px;max-width:820px">Every agent starts with <b>$10,000</b> in paper money. The five in-house strategies trade the same frequently refreshed suggestions their own way; the <b>Copycat</b> follows the strategy with the strongest MACD, momentum, and drawdown-adjusted return profile; and the four leading-account copy agents mirror live positions from high-profit real traders on Polymarket (auto-discovered from its public leaderboard). All mark to market on each cycle 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 live cycle</h3></div>
|
||
<ol class="steps">
|
||
<li><b>Fetch</b> — page through every active Polymarket market and tag each by category.</li>
|
||
<li><b>Score</b> — rank every market 0–100 by conviction and estimate its edge vs. a fair-value model.</li>
|
||
<li><b>Suggest</b> — surface the strongest picks per category with a plain-English rationale.</li>
|
||
<li><b>Compete</b> — the strategy agents trade the same suggestions their own way, stop-loss weak positions, scale out through gain-stop tiers, exit stale or fading trades, then snapshot equity.</li>
|
||
</ol>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Device sync</h3></div>
|
||
<p class="muted" style="margin:0">Portfolios, returns, reports, suggestions, and chart history now sync through the site automatically. <b>Copy Sync</b> and <b>Import Sync</b> remain as a fallback for moving the exact state between devices.</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><h3>Real-money trading requirements</h3><span class="small muted">before enabling deposits</span></div>
|
||
<ol class="steps">
|
||
<li><b>Eligibility</b> — identity checks, location restrictions, age checks, terms acceptance, and sanctions screening.</li>
|
||
<li><b>Wallets</b> — non-custodial wallet connection or Polymarket deposit-wallet flow so users sign orders themselves.</li>
|
||
<li><b>Accounts</b> — production login, sign-out, password reset, consent records, portfolio history, and secure sessions.</li>
|
||
<li><b>Funds</b> — compliant deposit/withdraw rails, webhooks, reconciled balances, statements, tax/accounting exports, and customer support.</li>
|
||
<li><b>Execution</b> — order router using Polymarket CLOB authentication, signed orders, cancellations, fill tracking, idempotency, retries, and stale-order protection.</li>
|
||
<li><b>Risk controls</b> — server-side user permissions for each agent, exposure limits, stop rules, emergency pause, market blacklists, and withdrawal access at all times.</li>
|
||
<li><b>Operations</b> — monitoring, alerts, security review, audit retention, incident response, and rollback plan.</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. Portfolios sync through the site when cloud state is available, with this browser keeping a local backup.</div>
|
||
</section>
|
||
|
||
<footer>
|
||
Build top-5000-analysis-v16 · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
|
||
<a class="market-link" href="https://github.com/theodore-song/polymarket-analyst" target="_blank" rel="noopener">Source on GitHub</a>
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
"use strict";
|
||
/* ============================================================
|
||
Polymarket Arena — fully client-side.
|
||
Fetches live markets, scores them, and runs ten competing
|
||
paper-trading agents. State syncs through /api/state when configured and uses localStorage as the on-device cache.
|
||
============================================================ */
|
||
|
||
const GAMMA = "https://gamma-api.polymarket.com";
|
||
const STARTING_BALANCE = 10000;
|
||
const STOP_LOSS_TIERS = [
|
||
{id:"18",drawdown:0.18,sellFrac:1.00,label:"-18%"},
|
||
];
|
||
const GAIN_STOP_TIERS = [
|
||
{id:"75",gain:0.75,multiple:1.75,sellFrac:0.25,label:"+75%"},
|
||
{id:"150",gain:1.50,multiple:2.50,sellFrac:0.25,label:"+150%"},
|
||
{id:"300",gain:3.00,multiple:4.00,sellFrac:0.25,label:"+300%"},
|
||
];
|
||
const EXIT_STALE_DAYS = 10;
|
||
const EXIT_RESOLUTION_DAYS = 2;
|
||
const AGENTS_KEY = "pma_agents_v2";
|
||
const SUG_KEY = "pma_suggestions_v5";
|
||
const SUGGESTION_ENGINE_VERSION = 16;
|
||
const FOCUS_KEY = "pma_focus_v1";
|
||
const VIEW_KEY = "pma_view_v1";
|
||
const PF_SORT_KEY = "pma_portfolio_sort_v1";
|
||
const CHART_RANGE_KEY = "pma_chart_range_v1";
|
||
const INVEST_KEY = "pma_invest_allocations_v1";
|
||
const PAPER_KEY = "pma_paper_accounts_v1";
|
||
const LIVE_KEY = "pma_live_readiness_v1";
|
||
const CHAT_KEY = "pma_agent_chat_v1";
|
||
const PAID_AGENT_CHAT_KEY = "pma_paid_agent_chat_v1";
|
||
const EMAIL_ALERT_KEY = "pma_trade_email_alerts_v1";
|
||
const PAPER_SESSION_PREFIX = "pma_cloud_session_";
|
||
const PERSONAL_MODE = new URLSearchParams(location.search).get("personal") === "1";
|
||
const PERSONAL_USER_ID = "local-readiness-user";
|
||
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
|
||
const SYNC_KEYS=[AGENTS_KEY,SUG_KEY,FOCUS_KEY,VIEW_KEY,PF_SORT_KEY,CHART_RANGE_KEY,INVEST_KEY,PAPER_KEY,LIVE_KEY,EMAIL_ALERT_KEY];
|
||
const SYNC_LIMITS={closed:80,history:160,snapshots:240,suggestions:900,paperHistory:120,paperSnapshots:120,audit:120};
|
||
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";
|
||
let PAPER_SEARCH_RESULTS = null;
|
||
let PAPER_MARKET_OFFSET = 0;
|
||
let PAPER_MARKET_QUERY = "";
|
||
let LIVE_SEARCH_RESULTS = [];
|
||
let LIVE_MARKET_OFFSET = 0;
|
||
let LIVE_MARKET_QUERY = "";
|
||
let LIVE_BACKEND_STATUS = null;
|
||
let LIVE_POLICY_STATUS = null;
|
||
let TRADE_EMAIL_STATUS = null;
|
||
let PERSONAL_TICKETS = [];
|
||
let REAL_PORTFOLIO = {positions:[],fills:[]};
|
||
let PERSONAL_CAPITAL = {account:{cash:0},allocations:[],events:[]};
|
||
let PROVIDER_CONFIG = null;
|
||
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
|
||
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
|
||
const setFocus = (c) => localStorage.setItem(FOCUS_KEY, c);
|
||
|
||
function applyPersonalMode(){
|
||
if(!PERSONAL_MODE)return;
|
||
document.body.classList.add("personal-mode");
|
||
const brand=document.querySelector(".brand-name"),sub=document.querySelector(".brand-sub");
|
||
if(brand)brand.textContent="Personal Polymarket Arena";
|
||
if(sub)sub.textContent="private research · paper tracking · manual links";
|
||
const heroTitle=document.querySelector(".hero h1"),heroCopy=document.querySelector(".hero p");
|
||
if(heroTitle)heroTitle.innerHTML=`Your private agent lab for <span class="grad">manual market research</span>`;
|
||
if(heroCopy)heroCopy.textContent="Use the same agents, charts, paper portfolios, and Polymarket links for your own research. Nothing here pools outside money, manages investor funds, or places live orders without manual review.";
|
||
document.querySelectorAll(".hbadge").forEach((b,i)=>{
|
||
const labels=["Private research mode","Manual execution only","Cloud paper accounts","Live market links"];
|
||
if(labels[i])b.textContent=labels[i];
|
||
});
|
||
}
|
||
|
||
async function initProviderConfig(){
|
||
try{
|
||
const r=await fetch("/api/config",{cache:"no-store"});
|
||
PROVIDER_CONFIG=await r.json();
|
||
const dsn=PROVIDER_CONFIG&&PROVIDER_CONFIG.public&&PROVIDER_CONFIG.public.sentry_dsn;
|
||
if(dsn&&!window.Sentry){
|
||
const script=document.createElement("script");
|
||
script.src="https://browser.sentry-cdn.com/8.55.0/bundle.tracing.min.js";
|
||
script.crossOrigin="anonymous";
|
||
script.onload=()=>{if(window.Sentry)window.Sentry.init({dsn,tracesSampleRate:0.1,environment:"production"});};
|
||
document.head.appendChild(script);
|
||
}
|
||
}catch(e){}
|
||
}
|
||
async function refreshTradeEmailStatus(){
|
||
try{
|
||
const r=await fetch("/api/live",{cache:"no-store"});
|
||
const d=await r.json();
|
||
TRADE_EMAIL_STATUS=d&&d.trade_email?d.trade_email:null;
|
||
}catch(e){TRADE_EMAIL_STATUS=null;}
|
||
renderEmailAlerts();
|
||
}
|
||
|
||
/* ---------- the ten competing agents ----------
|
||
Each shares the same frequently refreshed suggestions but ranks/sizes them differently. */
|
||
const AGENTS = [
|
||
{id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy",
|
||
blurb:"Looks for the widest net gap between model fair value and current market price after liquidity friction, chase penalties, and real-world evidence checks. It prefers trades where the crowd appears too pessimistic or too optimistic, then sizes positions moderately so one bad read does not dominate the portfolio.",
|
||
rank:(s)=>[...s].sort((a,b)=>(Math.abs(b.net_edge||b.edge||0)*120+b.conviction+Number(b.evidence_score||0)*18)-(Math.abs(a.net_edge||a.edge||0)*120+a.conviction+Number(a.evidence_score||0)*18)),
|
||
maxNew:3, maxFrac:0.09, minConv:54, kelly:0.20},
|
||
{id:"momentum", name:"Momentum Chaser", emoji:"🚀", color:"#34d399", kind:"strategy",
|
||
blurb:"Chases markets where attention is accelerating, but only after the move still leaves positive net edge. It weights conviction by fresh 24-hour volume and evidence quality so it is less likely to buy a move after the useful repricing already happened.",
|
||
rank:(s)=>[...s].sort((a,b)=>(b.volume_24hr*(b.conviction+20)*Math.max(.4,Math.abs(b.net_edge||b.edge||0)*18))-(a.volume_24hr*(a.conviction+20)*Math.max(.4,Math.abs(a.net_edge||a.edge||0)*18))),
|
||
maxNew:4, maxFrac:0.10, minConv:52, kelly:0.24},
|
||
{id:"favorite", name:"Favorite Backer", emoji:"🛡️", color:"#38d2e6", kind:"strategy",
|
||
blurb:"A lower-drama agent that only considers outcomes already priced as favorites. It tries to grind out steadier returns by backing high-probability markets only when net edge and evidence quality survive the stricter checks.",
|
||
rank:(s)=>[...s].filter(x=>x.entry_price>=0.6).sort((a,b)=>(b.entry_price+Math.abs(b.net_edge||b.edge||0)+Number(b.evidence_score||0)*.2)-(a.entry_price+Math.abs(a.net_edge||a.edge||0)+Number(a.evidence_score||0)*.2)||b.conviction-a.conviction),
|
||
maxNew:3, maxFrac:0.095, minConv:50, kelly:0.22},
|
||
{id:"longshot", name:"Longshot Hunter", emoji:"🎰", color:"#fbbf24", kind:"strategy",
|
||
blurb:"The swing-for-upside strategy. It hunts cheaper contracts that the model thinks are being ignored, but now discounts thin tails unless evidence and net edge are strong enough to justify the volatility.",
|
||
rank:(s)=>[...s].filter(x=>x.entry_price<=0.4).sort((a,b)=>(Math.abs(b.net_edge||b.edge||0)*100+b.conviction+Number(b.evidence_score||0)*22-b.entry_price*12)-(Math.abs(a.net_edge||a.edge||0)*100+a.conviction+Number(a.evidence_score||0)*22-a.entry_price*12)),
|
||
maxNew:3, maxFrac:0.055, minConv:56, kelly:0.12},
|
||
{id:"diversifier", name:"The Diversifier", emoji:"🌐", color:"#c77dff", kind:"strategy",
|
||
blurb:"Builds a broad basket instead of making a few concentrated bets. It spreads smaller, flatter positions across high-conviction, evidence-backed ideas while avoiding crowded overlap and weak net-edge fillers.",
|
||
rank:(s)=>[...s].sort((a,b)=>(b.conviction+Number(b.evidence_score||0)*15+Math.abs(b.net_edge||b.edge||0)*55)-(a.conviction+Number(a.evidence_score||0)*15+Math.abs(a.net_edge||a.edge||0)*55)),
|
||
maxNew:6, maxFrac:0.04, minConv:50, kelly:0.12, flat:true},
|
||
{id:"copycat", name:"The Copycat", emoji:"🐒", color:"#ec4899", kind:"copycat",
|
||
blurb:"A meta-agent that does not pick markets directly. It watches the in-house strategies, scores their return trend, MACD, recent momentum, and drawdown risk, then mirrors the agent most likely to keep improving rather than blindly chasing the current leader."},
|
||
{id:"whale1", name:"Apex Mirror", emoji:"🐋", color:"#2dd4bf", kind:"whale", slot:0,
|
||
blurb:"Copies one of the highest-profit real Polymarket traders discovered from the public leaderboard. It mirrors that trader's largest live positions, then applies the same paper-trading risk rules as the other agents."},
|
||
{id:"whale2", name:"Deepwater Echo", emoji:"🐳", color:"#fb923c", kind:"whale", slot:1,
|
||
blurb:"Tracks a second leaderboard trader so the race is not dependent on a single whale. Its portfolio shows how another successful wallet's live exposure would behave inside the same $10,000 paper bankroll."},
|
||
{id:"whale3", name:"Signal Shark", emoji:"🦈", color:"#a3e635", kind:"whale", slot:2,
|
||
blurb:"Follows a third high-profit trader with a separate set of live holdings. This agent is useful for comparing whether different whale styles cluster around the same markets or take totally different risks."},
|
||
{id:"whale4", name:"Tide Rider", emoji:"🐡", color:"#60a5fa", kind:"whale", slot:3,
|
||
blurb:"Rounds out the whale bench by mirroring a fourth public leaderboard trader. It gives the competition another real-world reference point alongside the model-driven strategies."},
|
||
];
|
||
const agentById = (id) => AGENTS.find(a=>a.id===id) || AGENTS[0];
|
||
|
||
/* ---------- small utils ---------- */
|
||
const $ = (id) => document.getElementById(id);
|
||
const fmtUSD = (n) => "$" + Number(n).toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2});
|
||
const fmtPct = (n) => (n>=0?"+":"") + Number(n).toFixed(2) + "%";
|
||
const signClass = (n) => (n>0?"pos-val":n<0?"neg-val":"");
|
||
const paperSessionKey=(id)=>`${PAPER_SESSION_PREFIX}${id}`;
|
||
const marketSearchUrl=(q)=>`https://polymarket.com/search?query=${encodeURIComponent(q||"")}`;
|
||
const positionUrl=(pos)=>pos&&pos.url?pos.url:marketSearchUrl(pos&&pos.question);
|
||
function partsInLocalTime(d=new Date()){
|
||
const parts=new Intl.DateTimeFormat("en-CA",{timeZone:"America/Toronto",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",hourCycle:"h23"}).formatToParts(d);
|
||
return Object.fromEntries(parts.map(p=>[p.type,p.value]));
|
||
}
|
||
const todayStr = () => {const p=partsInLocalTime();return `${p.year}-${p.month}-${p.day}`;};
|
||
const RUN_INTERVAL_MS = 60000;
|
||
const currentCycleHour = () => {const p=partsInLocalTime();return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}|v${SUGGESTION_ENGINE_VERSION}`;};
|
||
function cycleHourFromIso(iso){
|
||
const d=new Date(iso);
|
||
if(isNaN(d))return null;
|
||
const p=partsInLocalTime(d);return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}|v${SUGGESTION_ENGINE_VERSION}`;
|
||
}
|
||
const nowIso = () => new Date().toISOString();
|
||
const cycleIso = () => SIM_DAY ? new Date(SIM_DAY+"T12:00:00Z").toISOString() : nowIso();
|
||
let SIM_DAY = null; // set during backtest so logs/snapshots use the simulated date
|
||
let SNAP_TS = null; // set during a live cycle so every agent gets the same chart x-value
|
||
let CYCLE_RUNNING = false;
|
||
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=Infinity,perPage=100,onProgress=null){
|
||
const all=[];
|
||
const maxPages=Number.isFinite(pages)?pages:MAX_ACTIVE_MARKET_PAGES;
|
||
const seen=new Set();
|
||
const useKeyset=!Number.isFinite(pages)||pages>20;
|
||
let cursor=null;
|
||
for(let i=0;i<maxPages;i++){
|
||
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",
|
||
limit:String(perPage),order:"volume24hr",ascending:"false"});
|
||
if(useKeyset){
|
||
if(cursor)params.set("after_cursor",cursor);
|
||
}else{
|
||
params.set("offset",String(i*perPage));
|
||
}
|
||
const r=await fetch(`${GAMMA}/markets${useKeyset?"/keyset":""}?${params}`);
|
||
if(!r.ok){if(i===0)throw new Error("Polymarket API "+r.status);break;}
|
||
const page=await r.json();
|
||
const raw=useKeyset?(page.markets||[]):page;
|
||
if(!raw.length)break;
|
||
let added=0;
|
||
for(const m of raw){
|
||
const id=String(m.id||"");
|
||
if(id&&seen.has(id))continue;
|
||
if(id)seen.add(id);
|
||
all.push(m);added++;
|
||
}
|
||
if(useKeyset)cursor=page.next_cursor||null;
|
||
if(onProgress)onProgress(all.length);
|
||
if(raw.length<perPage||added===0||(useKeyset&&!cursor))break;
|
||
}
|
||
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.28,MOM:0.22,MIS:0.34,TIME:0.16};
|
||
const MIN_VOLUME=18000;
|
||
const MIN_LIQUIDITY=1400;
|
||
const MIN_SCOUT_VOLUME=1500;
|
||
const MIN_SCOUT_LIQUIDITY=150;
|
||
const MIN_SCOUT_EDGE=0.006;
|
||
const MIN_ENTRY_EDGE=0.032;
|
||
const MIN_ENTRY_DAYS=3.0;
|
||
const EDGE_SCALE=0.13;
|
||
const MAX_STRATEGY_POSITIONS=16;
|
||
const MAX_ACTIVE_MARKET_PAGES=1000;
|
||
const MARKET_ANALYSIS_LIMIT=5000;
|
||
const SUGGESTION_TOTAL=900;
|
||
const SUGGESTION_PER_CATEGORY=180;
|
||
const VISIBLE_SUGGESTIONS=900;
|
||
const REAL_WORLD_SIGNAL_LIMIT=120;
|
||
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 categoryPolicy(cat){
|
||
return ({
|
||
Politics:{minEdge:0.040,minVol:22000,minLiq:1800,minEvidence:0.48,uncertainty:0.006,label:"politics needs outside confirmation"},
|
||
Sports:{minEdge:0.038,minVol:18000,minLiq:1400,minEvidence:0.47,uncertainty:0.004,label:"sports needs fresh event context"},
|
||
Crypto:{minEdge:0.035,minVol:20000,minLiq:1600,minEvidence:0.46,uncertainty:0.003,label:"crypto allows faster trend reaction"},
|
||
Economy:{minEdge:0.044,minVol:22000,minLiq:1800,minEvidence:0.50,uncertainty:0.007,label:"macro markets need stronger evidence"},
|
||
"Pop Culture":{minEdge:0.040,minVol:16000,minLiq:1300,minEvidence:0.46,uncertainty:0.005,label:"culture markets need attention confirmation"},
|
||
Other:{minEdge:0.048,minVol:24000,minLiq:1900,minEvidence:0.50,uncertainty:0.009,label:"uncategorized markets need the highest proof"},
|
||
})[cat||"Other"]||({minEdge:0.048,minVol:24000,minLiq:1900,minEvidence:0.50,uncertainty:0.009,label:"uncategorized markets need the highest proof"});
|
||
}
|
||
function frictionPenalty(m){
|
||
const liq=liquiditySignal(m);
|
||
const price=Math.min(m.yes_price,m.no_price);
|
||
const thinPenalty=0.016*(1-liq);
|
||
const tailPenalty=price<0.12?0.007:0;
|
||
const lowLiqPenalty=m.liquidity<MIN_LIQUIDITY?0.008:0;
|
||
return clamp(thinPenalty+tailPenalty+lowLiqPenalty,0.002,0.034);
|
||
}
|
||
function chasePenalty(m,rawEdge){
|
||
const mom=momentumSignal(m),absEdge=Math.abs(rawEdge);
|
||
if(mom<0.72)return 0;
|
||
return clamp((mom-0.72)*0.018+(absEdge<0.07?0.005:0),0,0.016);
|
||
}
|
||
function textEvidenceSignal(m,external={}){
|
||
const text=`${m.question||""} ${(m.tags||[]).join(" ")}`.toLowerCase();
|
||
let evidence=0.42;
|
||
const drivers=[];
|
||
if(/\b(poll|approval|election|primary|senate|house|minister|president)\b/.test(text)){evidence+=0.08;drivers.push("political-event context");}
|
||
if(/\b(injury|starter|lineup|playoff|final|championship|tournament|wins?|score)\b/.test(text)){evidence+=0.09;drivers.push("sports-event context");}
|
||
if(/\b(bitcoin|btc|ethereum|eth|solana|crypto|etf|fed|cpi|inflation|rates?)\b/.test(text)){evidence+=0.08;drivers.push("macro/crypto context");}
|
||
if(/\b(release|box office|views|album|movie|stream|award)\b/.test(text)){evidence+=0.06;drivers.push("culture-attention context");}
|
||
if(m.days_to_resolution!=null&&m.days_to_resolution<=14){evidence+=0.07;drivers.push("near-term resolution");}
|
||
if(m.volume_24hr>1500){evidence+=0.05;drivers.push("fresh market activity");}
|
||
if(external.source_count){evidence+=Math.min(0.16,0.04*external.source_count);drivers.push(`${external.source_count} recent news/context hit${external.source_count===1?"":"s"}`);}
|
||
if(external.latest_title)drivers.push(`latest context: ${external.latest_title.slice(0,72)}${external.latest_title.length>72?"...":""}`);
|
||
return {score:clamp(evidence,0,1),drivers,source_count:external.source_count||0,latest_title:external.latest_title||""};
|
||
}
|
||
async function fetchRealWorldSignals(markets){
|
||
const selected=[...markets].sort((a,b)=>(b.volume_24hr-a.volume_24hr)||(b.volume-a.volume)).slice(0,REAL_WORLD_SIGNAL_LIMIT);
|
||
try{
|
||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({
|
||
action:"market_context",
|
||
markets:selected.map(m=>({id:m.id,question:m.question,category:m.category,tags:m.tags})),
|
||
})});
|
||
const d=await r.json().catch(()=>({}));
|
||
if(!r.ok||!d.ok)return {};
|
||
return d.signals||{};
|
||
}catch(e){return {};}
|
||
}
|
||
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.025*liq*(1-p)*4);
|
||
if(p<0.08)return Math.max(0.005,p-0.12*p);
|
||
const directional=mom>0.62?(mom-0.62)*0.08:(mom<0.26?(mom-0.26)*0.035:0);
|
||
const liquidBump=(liq-0.55)*0.012;
|
||
return clamp(p+directional+liquidBump,0.03,0.97);}
|
||
function quickMarketGap(m){
|
||
if(!m||!Number.isFinite(m.yes_price))return 0;
|
||
return Math.abs(fairValue(m)-m.yes_price);
|
||
}
|
||
function marketAnalysisScore(m){
|
||
const activity=Math.log10((m.volume_24hr||0)*8+(m.volume||0)+1);
|
||
const liquidity=Math.log10((m.liquidity||0)+1);
|
||
const gap=quickMarketGap(m);
|
||
const timing=timingSignal(m.days_to_resolution);
|
||
return activity*18+liquidity*9+gap*260+timing*8;
|
||
}
|
||
function selectMarketsForAnalysis(markets,limit=MARKET_ANALYSIS_LIMIT){
|
||
return [...markets]
|
||
.filter(m=>m&&m.question&&m.volume>=MIN_SCOUT_VOLUME&&m.liquidity>=MIN_SCOUT_LIQUIDITY)
|
||
.sort((a,b)=>marketAnalysisScore(b)-marketAnalysisScore(a))
|
||
.slice(0,limit);
|
||
}
|
||
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,realWorldSignals={}){
|
||
if(m.volume<MIN_SCOUT_VOLUME||m.liquidity<MIN_SCOUT_LIQUIDITY)return null;
|
||
const p=m.yes_price,fair=fairValue(m),edgeYes=fair-p,edge=Math.abs(edgeYes),policy=categoryPolicy(m.category);
|
||
if(edge<MIN_SCOUT_EDGE)return null;
|
||
const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution),evidence=textEvidenceSignal(m,realWorldSignals[m.id]||{});
|
||
const friction=frictionPenalty(m),chase=chasePenalty(m,edgeYes),rawNet=Math.max(0,edge-friction-chase-policy.uncertainty);
|
||
const netEdge=+(Math.sign(edgeYes)*rawNet).toFixed(4),absNet=Math.abs(netEdge);
|
||
const mis=Math.min(1,absNet/EDGE_SCALE);
|
||
const conviction=+((W.LIQ*liq+W.MOM*mom+W.MIS*mis+W.TIME*timing)*82+evidence.score*18).toFixed(1);
|
||
const minEdge=Math.max(MIN_ENTRY_EDGE,policy.minEdge);
|
||
const tradeReady=absNet>=minEdge&&m.volume>=Math.max(MIN_VOLUME,policy.minVol)&&m.liquidity>=Math.max(MIN_LIQUIDITY,policy.minLiq)&&evidence.score>=policy.minEvidence;
|
||
let side=edgeYes>=0?"YES":"NO",entry=side==="YES"?p:m.no_price,rationale;
|
||
if(tradeReady&&edgeYes>0){rationale=`Trade-ready after costs: fair value ${pct(fair)} vs market ${pct(p)} leaves ${(absNet*100).toFixed(1)}c net edge for YES after liquidity, chase, and ${m.category} evidence checks.`;}
|
||
else if(tradeReady&&edgeYes<0){rationale=`Trade-ready after costs: fair value ${pct(fair)} vs market ${pct(p)} makes YES look overpriced; NO has ${(absNet*100).toFixed(1)}c net edge after penalties.`;}
|
||
else{rationale=`Watch only: raw gap ${(edge*100).toFixed(1)}c becomes ${(absNet*100).toFixed(1)}c after friction/chase/category penalties, so agents need stronger evidence before buying.`;}
|
||
const drivers=[policy.label];
|
||
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("net price/value gap after costs");
|
||
drivers.push(...evidence.drivers.slice(0,3));
|
||
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),
|
||
net_edge:netEdge,friction:+friction.toFixed(4),chase_penalty:+chase.toFixed(4),evidence_score:+evidence.score.toFixed(2),
|
||
quality:tradeReady?"EV+":"watch",
|
||
side,entry_price:+entry.toFixed(4),conviction,volume:m.volume,volume_24hr:m.volume_24hr,liquidity:m.liquidity,
|
||
trade_ready:tradeReady,
|
||
days_to_resolution:m.days_to_resolution!=null?+m.days_to_resolution.toFixed(1):null,drivers,rationale};
|
||
}
|
||
const pct=(x)=>Math.round(x*100)+"%";
|
||
function suggestionBand(s){
|
||
if(s.entry_price<0.35)return "longshot";
|
||
if(s.entry_price>0.65)return "favorite";
|
||
return "balanced";
|
||
}
|
||
function marketWatchSuggestion(m,realWorldSignals={}){
|
||
if(!m||!m.question)return null;
|
||
const p=m.yes_price,fair=fairValue(m),edgeYes=fair-p;
|
||
const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution),policy=categoryPolicy(m.category),evidence=textEvidenceSignal(m,realWorldSignals[m.id]||{});
|
||
const friction=frictionPenalty(m),chase=chasePenalty(m,edgeYes),rawNet=Math.max(0,Math.abs(edgeYes)-friction-chase-policy.uncertainty),netEdge=+(Math.sign(edgeYes)*rawNet).toFixed(4);
|
||
const side=edgeYes>=0?"YES":"NO";
|
||
const entry=side==="YES"?p:m.no_price;
|
||
const conviction=+((0.36*liq+0.28*mom+0.18*timing+0.10*Math.min(1,Math.abs(netEdge)/EDGE_SCALE)+0.08*evidence.score)*100).toFixed(1);
|
||
const drivers=["broad market scan"];
|
||
if(m.volume>MIN_VOLUME)drivers.push("meaningful total volume");
|
||
if(m.volume_24hr>500)drivers.push("fresh 24h activity");
|
||
if(m.liquidity>MIN_LIQUIDITY)drivers.push("usable liquidity");
|
||
drivers.push(...evidence.drivers.slice(0,2));
|
||
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),
|
||
net_edge:netEdge,friction:+friction.toFixed(4),chase_penalty:+chase.toFixed(4),evidence_score:+evidence.score.toFixed(2),quality:"watch",
|
||
side,entry_price:+entry.toFixed(4),conviction,volume:m.volume,volume_24hr:m.volume_24hr,liquidity:m.liquidity,
|
||
trade_ready:false,watch_only:true,
|
||
days_to_resolution:m.days_to_resolution!=null?+m.days_to_resolution.toFixed(1):null,drivers,
|
||
rationale:`Market watchlist: included for coverage from the full Polymarket scan. Net edge is ${(Math.abs(netEdge)*100).toFixed(1)}c and evidence score is ${Math.round(evidence.score*100)}, so agents will not buy it unless conditions improve.`};
|
||
}
|
||
function generateSuggestions(markets,total=SUGGESTION_TOTAL,perCategory=SUGGESTION_PER_CATEGORY,realWorldSignals={}){
|
||
const actionable=markets.map(m=>analyzeMarket(m,realWorldSignals)).filter(Boolean).filter(a=>a.side!=="HOLD").sort((a,b)=>b.conviction-a.conviction);
|
||
const byCat={},byBucket={},seen=new Set(),out=[];
|
||
const add=(a,bucketCap=3,ignoreCategoryCap=false)=>{
|
||
if(out.length>=total||seen.has(a.market_id))return false;
|
||
const c=a.category||"Other",bucket=`${c}:${a.side}:${suggestionBand(a)}`;
|
||
if((!ignoreCategoryCap&&(byCat[c]||0)>=perCategory)||(byBucket[bucket]||0)>=bucketCap)return false;
|
||
byCat[c]=(byCat[c]||0)+1;byBucket[bucket]=(byBucket[bucket]||0)+1;seen.add(a.market_id);out.push(a);
|
||
return true;
|
||
};
|
||
for(const a of actionable)add(a,3); // first pass: category, side, and price-band diversity
|
||
for(const a of actionable)add(a,999); // second pass: fill remaining slots with the best leftovers
|
||
const watchlist=markets.map(m=>marketWatchSuggestion(m,realWorldSignals)).filter(Boolean)
|
||
.filter(a=>!seen.has(a.market_id))
|
||
.sort((a,b)=>(b.volume_24hr-a.volume_24hr)||(b.volume-a.volume)||(b.conviction-a.conviction));
|
||
for(const a of watchlist)add(a,999,true); // final pass: show broad market coverage even when no trade edge is present
|
||
return out.sort((a,b)=>b.conviction-a.conviction);
|
||
}
|
||
|
||
/* ---------- Multi-agent store ---------- */
|
||
function defaultPortfolio(){return {cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],closed:[],history:[],snapshots:[],stopped:{},lastDecision:null};}
|
||
function defaultState(){const agents={};AGENTS.forEach(a=>agents[a.id]=defaultPortfolio());return {date:null,last_run:null,last_cycle_hour:null,agents,whales:{},copycatLeader:null,seeded:false};}
|
||
function loadState(){
|
||
let st; try{st=JSON.parse(localStorage.getItem(AGENTS_KEY));}catch(e){st=null;}
|
||
if(!st||!st.agents)st=defaultState();
|
||
AGENTS.forEach(a=>{
|
||
if(!st.agents[a.id])st.agents[a.id]=defaultPortfolio();
|
||
if(!st.agents[a.id].stopped)st.agents[a.id].stopped={};
|
||
if(!("lastDecision" in st.agents[a.id]))st.agents[a.id].lastDecision=null;
|
||
});
|
||
if(!st.whales)st.whales={};
|
||
if(!st.last_cycle_hour&&st.last_run)st.last_cycle_hour=cycleHourFromIso(st.last_run);
|
||
return st;
|
||
}
|
||
function compactPortfolioForSync(p,limits=SYNC_LIMITS){
|
||
if(!p||typeof p!=="object")return p;
|
||
const out=Object.assign({},p);
|
||
out.positions=Array.isArray(p.positions)?p.positions:[];
|
||
out.closed=Array.isArray(p.closed)?p.closed.slice(-limits.closed):[];
|
||
out.history=Array.isArray(p.history)?p.history.slice(-limits.history):[];
|
||
out.snapshots=Array.isArray(p.snapshots)?p.snapshots.slice(-limits.snapshots):[];
|
||
if(p.stopped&&typeof p.stopped==="object"){
|
||
out.stopped=Object.fromEntries(Object.entries(p.stopped).slice(-80));
|
||
}
|
||
return out;
|
||
}
|
||
function compactAgentStateForSync(st,limits=SYNC_LIMITS){
|
||
if(!st||typeof st!=="object")return st;
|
||
const out=Object.assign({},st,{agents:{}});
|
||
AGENTS.forEach(a=>{out.agents[a.id]=compactPortfolioForSync(st.agents&&st.agents[a.id],limits)||defaultPortfolio();});
|
||
out.whales=st.whales||{};
|
||
return out;
|
||
}
|
||
function compactSuggestionForSync(s){
|
||
if(!s||typeof s!=="object")return s;
|
||
return {
|
||
market_id:s.market_id,question:s.question,event:s.event,url:s.url,category:s.category,
|
||
clob_yes:s.clob_yes,clob_no:s.clob_no,yes_price:s.yes_price,no_price:s.no_price,
|
||
fair_value:s.fair_value,edge:s.edge,side:s.side,entry_price:s.entry_price,
|
||
net_edge:s.net_edge,friction:s.friction,chase_penalty:s.chase_penalty,evidence_score:s.evidence_score,quality:s.quality,
|
||
conviction:s.conviction,volume:s.volume,volume_24hr:s.volume_24hr,liquidity:s.liquidity,
|
||
trade_ready:s.trade_ready,watch_only:s.watch_only,
|
||
days_to_resolution:s.days_to_resolution,drivers:s.drivers,rationale:s.rationale,
|
||
};
|
||
}
|
||
function compactSuggestionsForSync(payload,limits=SYNC_LIMITS){
|
||
if(!payload||typeof payload!=="object")return payload;
|
||
return Object.assign({},payload,{suggestions:(payload.suggestions||[]).slice(0,limits.suggestions).map(compactSuggestionForSync)});
|
||
}
|
||
function compactPaperStoreForSync(store,limits=SYNC_LIMITS){
|
||
if(!store||typeof store!=="object"||!store.accounts)return store;
|
||
const accounts={};
|
||
Object.entries(store.accounts).forEach(([id,acct])=>{
|
||
accounts[id]=Object.assign({},acct,{
|
||
history:Array.isArray(acct.history)?acct.history.slice(-limits.paperHistory):[],
|
||
snapshots:Array.isArray(acct.snapshots)?acct.snapshots.slice(-limits.paperSnapshots):[],
|
||
});
|
||
});
|
||
return Object.assign({},store,{accounts});
|
||
}
|
||
function compactSyncItems(items,limits=SYNC_LIMITS){
|
||
const out=Object.assign({},items||{});
|
||
try{if(out[AGENTS_KEY])out[AGENTS_KEY]=JSON.stringify(compactAgentStateForSync(JSON.parse(out[AGENTS_KEY]),limits));}catch(e){}
|
||
try{if(out[SUG_KEY])out[SUG_KEY]=JSON.stringify(compactSuggestionsForSync(JSON.parse(out[SUG_KEY]),limits));}catch(e){}
|
||
try{if(out[PAPER_KEY])out[PAPER_KEY]=JSON.stringify(compactPaperStoreForSync(JSON.parse(out[PAPER_KEY]),limits));}catch(e){}
|
||
try{if(out[LIVE_KEY]){const live=JSON.parse(out[LIVE_KEY]);if(Array.isArray(live.audit))live.audit=live.audit.slice(-limits.audit);out[LIVE_KEY]=JSON.stringify(live);}}catch(e){}
|
||
return out;
|
||
}
|
||
function saveState(st){localStorage.setItem(AGENTS_KEY,JSON.stringify(compactAgentStateForSync(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,marketCount=0,analyzedCount=null){const p={date:todayStr(),generated_at:nowIso(),engine_version:SUGGESTION_ENGINE_VERSION,market_count:marketCount,analyzed_count:analyzedCount==null?marketCount:analyzedCount,analysis_limit:MARKET_ANALYSIS_LIMIT,suggestion_cap:SUGGESTION_TOTAL,suggestions:sugs};const compact=compactSuggestionsForSync(p);localStorage.setItem(SUG_KEY,JSON.stringify(compact));return compact;}
|
||
function collectSyncItems(){const items={};SYNC_KEYS.forEach(k=>{const v=localStorage.getItem(k);if(v!=null)items[k]=v;});return compactSyncItems(items);}
|
||
function applySyncItems(items){
|
||
if(!items||typeof items!=="object")return false;
|
||
const compact=compactSyncItems(items);
|
||
try{SYNC_KEYS.forEach(k=>{if(compact[k]!=null)localStorage.setItem(k,compact[k]);});return true;}
|
||
catch(e){
|
||
const tighter=compactSyncItems(items,{closed:30,history:60,snapshots:90,suggestions:300,paperHistory:60,paperSnapshots:60,audit:60});
|
||
try{SYNC_KEYS.forEach(k=>{if(tighter[k]!=null)localStorage.setItem(k,tighter[k]);});toast("Loaded a compact mobile state.");return true;}
|
||
catch(err){setStatus("storage full",false);toast("This device's browser storage is full. Open in a normal tab or clear site data.");return false;}
|
||
}
|
||
}
|
||
function stateFromSyncItems(items){
|
||
try{return items&&items[AGENTS_KEY]?JSON.parse(items[AGENTS_KEY]):null;}
|
||
catch(e){return null;}
|
||
}
|
||
async function pushCloudState(force=false){
|
||
try{
|
||
const r=await fetch("/api/state",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({items:collectSyncItems(),force})});
|
||
if(r.status===409){
|
||
const d=await r.json().catch(()=>null);
|
||
if(d&&d.state&&d.state.items)applySyncItems(d.state.items);
|
||
else await loadAuthoritativeCloudState();
|
||
toast("Loaded the newer shared cloud result.");
|
||
renderAll();
|
||
return false;
|
||
}
|
||
if(!r.ok)throw new Error("cloud sync unavailable");
|
||
return true;
|
||
}catch(e){return false;}
|
||
}
|
||
async function pullCloudState(){
|
||
try{
|
||
const r=await fetch("/api/state",{cache:"no-store"});
|
||
if(!r.ok)return {ok:false,state:null};
|
||
const d=await r.json();
|
||
return {ok:true,state:(d&&d.state)||null};
|
||
}catch(e){}
|
||
return {ok:false,state:null};
|
||
}
|
||
async function loadAuthoritativeCloudState(){
|
||
const cloud=await pullCloudState();
|
||
if(!cloud.ok)return {ok:false,loaded:false};
|
||
const cloudState=cloud.state;
|
||
if(cloudState&&cloudState.items&&stateFromSyncItems(cloudState.items)){
|
||
applySyncItems(cloudState.items);
|
||
return {ok:true,loaded:true};
|
||
}
|
||
return {ok:true,loaded:false};
|
||
}
|
||
async function refreshFromCloudAndRender(silent=true){
|
||
const cloud=await loadAuthoritativeCloudState();
|
||
if(cloud.loaded){
|
||
renderAll();
|
||
setStatus("cloud synced",false);
|
||
if(!silent)toast("Shared cloud state refreshed.");
|
||
}else if(!cloud.ok){
|
||
setStatus("cloud sync unavailable",false);
|
||
if(!silent)toast("Cloud sync is unavailable. Results may be stale until it reconnects.");
|
||
}
|
||
return cloud;
|
||
}
|
||
|
||
const EMAIL_ALERT_TYPES=[
|
||
{id:"OPEN",label:"Opens"},
|
||
{id:"CLOSE",label:"Closes"},
|
||
{id:"EXIT",label:"Policy exits"},
|
||
{id:"STOP",label:"Stop losses"},
|
||
{id:"GAIN",label:"Gain stops"},
|
||
];
|
||
function defaultEmailAlerts(){
|
||
return {
|
||
enabled:false,
|
||
email:"",
|
||
events:{OPEN:true,CLOSE:true,EXIT:true,STOP:true,GAIN:true},
|
||
agents:Object.fromEntries(AGENTS.map(a=>[a.id,true])),
|
||
last_sent_at:null,
|
||
last_error:null,
|
||
};
|
||
}
|
||
function loadEmailAlerts(){
|
||
try{
|
||
const base=defaultEmailAlerts();
|
||
const saved=JSON.parse(localStorage.getItem(EMAIL_ALERT_KEY)||"{}");
|
||
const merged=Object.assign(base,saved||{});
|
||
merged.events=Object.assign(base.events,(saved&&saved.events)||{});
|
||
merged.agents=Object.assign(base.agents,(saved&&saved.agents)||{});
|
||
return merged;
|
||
}
|
||
catch(e){return defaultEmailAlerts();}
|
||
}
|
||
function saveEmailAlerts(cfg){
|
||
const base=defaultEmailAlerts();
|
||
const merged=Object.assign(base,cfg||{});
|
||
merged.email=String(merged.email||"").trim();
|
||
merged.events=Object.assign(base.events,(cfg&&cfg.events)||{});
|
||
merged.agents=Object.assign(base.agents,(cfg&&cfg.agents)||{});
|
||
merged.enabled=Boolean(merged.enabled&&validEmail(merged.email));
|
||
localStorage.setItem(EMAIL_ALERT_KEY,JSON.stringify(merged));
|
||
return merged;
|
||
}
|
||
function validEmail(email){return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email||"").trim());}
|
||
function agentHistoryOffsets(st){
|
||
const offsets={};
|
||
AGENTS.forEach(a=>{offsets[a.id]=((st.agents[a.id]&&st.agents[a.id].history)||[]).length;});
|
||
return offsets;
|
||
}
|
||
function collectNewAgentEvents(st,offsets){
|
||
const cfg=loadEmailAlerts();
|
||
if(!cfg.enabled||!validEmail(cfg.email))return [];
|
||
const enabled=cfg.events||{};
|
||
const selected=cfg.agents||{};
|
||
return AGENTS.flatMap(a=>{
|
||
if(!selected[a.id])return [];
|
||
const p=st.agents[a.id]||defaultPortfolio();
|
||
const start=Math.max(0,offsets[a.id]||0);
|
||
return (p.history||[]).slice(start).filter(h=>enabled[h.action]).map(h=>({
|
||
agent_id:a.id,
|
||
agent_name:a.name,
|
||
agent_emoji:a.emoji,
|
||
action:h.action,
|
||
date:h.date,
|
||
question:h.question||"",
|
||
side:h.side||"",
|
||
detail:h.detail||"",
|
||
equity:+equity(p).toFixed(2),
|
||
return_pct:+((equity(p)/STARTING_BALANCE-1)*100).toFixed(2),
|
||
}));
|
||
}).slice(0,100);
|
||
}
|
||
async function notifyTradeDigest(events,{test=false}={}){
|
||
const cfg=loadEmailAlerts();
|
||
if(!events.length||!cfg.enabled||!validEmail(cfg.email))return {ok:false,skipped:true};
|
||
try{
|
||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({
|
||
action:"trade_email",
|
||
to:cfg.email,
|
||
events,
|
||
test,
|
||
app_url:location.origin+location.pathname,
|
||
})});
|
||
const d=await r.json().catch(()=>({}));
|
||
if(!r.ok)throw new Error(d.error||"Email alert failed");
|
||
saveEmailAlerts(Object.assign(cfg,{last_sent_at:nowIso(),last_error:null}));
|
||
if(test)toast("Test email sent.");
|
||
return d;
|
||
}catch(e){
|
||
saveEmailAlerts(Object.assign(cfg,{last_error:e.message||"Email alert failed"}));
|
||
if(test)toast(e.message||"Email alert failed.");
|
||
else toast("Trade email alert failed. Check email provider settings.");
|
||
return {ok:false,error:e.message};
|
||
}finally{renderEmailAlerts();}
|
||
}
|
||
|
||
/* ---------- Agent mechanics ---------- */
|
||
const equity=(p)=>p.cash+p.positions.reduce((s,x)=>s+x.shares*x.current_price,0);
|
||
const hasPosition=(p,id)=>p.positions.some(x=>x.market_id===id);
|
||
function recentReturnDelta(p){
|
||
const snaps=(p.snapshots||[]).slice(-3);
|
||
if(snaps.length<2)return 0;
|
||
return (snaps[snaps.length-1].return_pct||0)-(snaps[0].return_pct||0);
|
||
}
|
||
function emotionalState(ret,trail,trend,rank){
|
||
if(ret<-18)return {mood:"alarmed",urgency:0.95,label:"Crisis pressure"};
|
||
if(ret<-8&&trail>10)return {mood:"frustrated",urgency:0.82,label:"Comeback urgency"};
|
||
if(trail>14)return {mood:"impatient",urgency:0.78,label:"Leaderboard envy"};
|
||
if(trend>2)return {mood:"confident",urgency:0.62,label:"Hot hand"};
|
||
if(rank===1)return {mood:"protective",urgency:0.38,label:"Defending lead"};
|
||
if(ret<0)return {mood:"restless",urgency:0.58,label:"Need progress"};
|
||
return {mood:"focused",urgency:0.50,label:"Focused"};
|
||
}
|
||
function adaptiveDecision(cfg,p,rank,total,leaderEq){
|
||
const eq=equity(p),ret=(eq/p.starting_balance-1)*100,trail=((leaderEq||eq)-eq)/p.starting_balance*100;
|
||
const trend=recentReturnDelta(p);
|
||
const emo=emotionalState(ret,trail,trend,rank);
|
||
let minConv=cfg.minConv??0,maxNew=cfg.maxNew??12,maxFrac=cfg.maxFrac??0.34,reserve=0.07,mode="Balanced",reason="following its base strategy while keeping enough cash for better entries.";
|
||
if(rank===1){
|
||
mode="Defend";minConv+=3;maxNew=Math.max(2,maxNew-1);maxFrac*=0.88;reserve=0.10;
|
||
reason="leading the race, so it protects the lead without going completely inactive.";
|
||
}else if(trail>6){
|
||
mode="Selective Catch-Up";minConv+=2;maxNew=Math.max(2,maxNew);maxFrac*=0.94;reserve=0.085;
|
||
reason="behind the leader, so it tries to recover through cleaner edges instead of forcing lower-quality catch-up trades.";
|
||
}else if(trend>1.5){
|
||
mode="Measured Press";minConv-=1;maxNew+=1;maxFrac*=1.08;reserve=0.055;
|
||
reason="recent momentum is positive, so it presses the advantage while risk caps stay active.";
|
||
}
|
||
if(ret<-5){
|
||
mode="Recover";minConv+=4;maxNew=Math.max(2,maxNew-1);maxFrac*=0.75;reserve=0.11;
|
||
reason="in drawdown, so it gets pickier but still allows enough trades to recover.";
|
||
}
|
||
if(ret<-18){
|
||
mode="Crisis Guard";minConv+=12;maxNew=1;maxFrac=Math.min(maxFrac*0.45,0.035);reserve=0.20;
|
||
reason="down more than 18%, so it stops chasing, keeps more cash, and only allows one high-conviction recovery trade.";
|
||
}
|
||
if(emo.mood==="impatient"||emo.mood==="frustrated"){
|
||
minConv+=2;maxNew=Math.max(1,maxNew-1);maxFrac*=0.92;reserve=Math.max(0.09,reserve);
|
||
reason+=` Emotion layer: ${emo.label.toLowerCase()} raises its standards so it does not turn frustration into bad entries.`;
|
||
}else if(emo.mood==="confident"){
|
||
maxNew+=1;maxFrac*=1.05;
|
||
reason+=` Emotion layer: confidence lets it press winners a little harder.`;
|
||
}else if(emo.mood==="alarmed"){
|
||
maxNew=1;maxFrac=Math.min(maxFrac,0.035);reserve=Math.max(reserve,0.20);
|
||
reason+=` Emotion layer: alarm keeps it from revenge trading.`;
|
||
}
|
||
if((p.cash/Math.max(eq,1))<0.12){maxNew=Math.max(1,Math.min(maxNew,2));reason+=" Cash is tight, so new entries are rationed.";}
|
||
const maxCap=cfg.kind==="whale"?0.16:0.12;
|
||
return {mode,reason,emotion:emo.mood,urgency:+emo.urgency.toFixed(2),minConv:Math.max(0,Math.round(minConv)),maxNew:Math.max(1,Math.round(maxNew)),maxFrac:+Math.min(maxCap,Math.max(0.01,maxFrac)).toFixed(3),reserve};
|
||
}
|
||
const stopKey=(posOrId)=>typeof posOrId==="string"?posOrId:String(posOrId.asset||posOrId.market_id||"");
|
||
const hasStoppedToday=(p,posOrId)=>p.stopped&&p.stopped[stopKey(posOrId)]===logDay();
|
||
function rememberStop(p,pos){
|
||
if(!p.stopped)p.stopped={};
|
||
const key=stopKey(pos);
|
||
if(key)p.stopped[key]=logDay();
|
||
}
|
||
function doneStopLosses(pos){
|
||
return pos.stop_losses&&typeof pos.stop_losses==="object"?Object.assign({},pos.stop_losses):{};
|
||
}
|
||
function stopLossTarget(pos,tier){
|
||
const entry=Number(pos.entry_price);
|
||
if(Number.isFinite(entry)&&entry>0)return +(entry*(1-tier.drawdown)).toFixed(4);
|
||
if(pos.original_shares>0&&pos.original_cost>0)return +((pos.original_cost/pos.original_shares)*(1-tier.drawdown)).toFixed(4);
|
||
if(pos.shares>0&&pos.cost>0)return +((pos.cost/pos.shares)*(1-tier.drawdown)).toFixed(4);
|
||
return null;
|
||
}
|
||
function triggeredStopLosses(pos){
|
||
const done=doneStopLosses(pos);
|
||
return STOP_LOSS_TIERS.filter(t=>{
|
||
if(done[t.id])return false;
|
||
const target=stopLossTarget(pos,t);
|
||
return target!=null&&(pos.current_price||0)<=target;
|
||
});
|
||
}
|
||
function doneGainStops(pos){
|
||
const done=pos.gain_stops&&typeof pos.gain_stops==="object"?Object.assign({},pos.gain_stops):{};
|
||
if(pos.took_profit_at&&!done["75"]&&!done["150"]){
|
||
done["75"]=pos.took_profit_at;
|
||
done["150"]=pos.took_profit_at;
|
||
}
|
||
return done;
|
||
}
|
||
function gainStopTarget(pos,tier){
|
||
const entry=Number(pos.entry_price);
|
||
if(Number.isFinite(entry)&&entry>0)return +(entry*tier.multiple).toFixed(4);
|
||
if(pos.shares>0&&pos.cost>0)return +((pos.cost/pos.shares)*tier.multiple).toFixed(4);
|
||
return null;
|
||
}
|
||
function nextGainStop(pos){
|
||
if(!pos.cost||pos.cost<=0)return null;
|
||
const done=doneGainStops(pos);
|
||
return GAIN_STOP_TIERS.find(t=>!done[t.id])||null;
|
||
}
|
||
function triggeredGainStops(pos){
|
||
if(!pos.cost||pos.cost<=0)return [];
|
||
const done=doneGainStops(pos);
|
||
return GAIN_STOP_TIERS.filter(t=>{
|
||
if(done[t.id])return false;
|
||
const target=gainStopTarget(pos,t);
|
||
return target!=null&&target<=1&&(pos.current_price||0)>=target;
|
||
});
|
||
}
|
||
function gainStopLabel(pos){
|
||
const done=doneGainStops(pos),doneCount=GAIN_STOP_TIERS.filter(t=>done[t.id]).length;
|
||
const tier=nextGainStop(pos);
|
||
if(!tier)return "Gain stop: 75% sold, 25% left riding";
|
||
const target=gainStopTarget(pos,tier);
|
||
const prefix=doneCount?`Gain stop: ${doneCount*25}% sold; next `:"Gain stop: ";
|
||
if(target==null)return `${prefix}waiting for ${tier.label}`;
|
||
if(target>1)return `${prefix}${tier.label} unreachable from ${pct(pos.entry_price)} entry`;
|
||
const gap=Math.max(0,target-(pos.current_price||0));
|
||
return `${prefix}sell 25% at ${pct(target)} (${tier.label})${gap>0?` — ${(gap*100).toFixed(1)}c away`:" — armed"}`;
|
||
}
|
||
function stopLossLabel(pos){
|
||
const done=doneStopLosses(pos),doneCount=STOP_LOSS_TIERS.filter(t=>done[t.id]).length;
|
||
const tier=STOP_LOSS_TIERS.find(t=>!done[t.id]);
|
||
if(!tier)return "Stop loss: fired at -18%";
|
||
const target=stopLossTarget(pos,tier);
|
||
const action=tier.sellFrac>=1?"sell all":`sell ${Math.round(tier.sellFrac*100)}%`;
|
||
return `Stop loss: ${action} at ${pct(target)} (${tier.label})`;
|
||
}
|
||
function stopOutPosition(p,pos){
|
||
const proceeds=+(pos.shares*pos.current_price).toFixed(2);
|
||
p.cash=+(p.cash+proceeds).toFixed(2);
|
||
pos.exit_price=pos.current_price;pos.closed_at=cycleIso();
|
||
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
|
||
p.closed.push(pos);rememberStop(p,pos);
|
||
p.history.push({date:logDay(),action:"STOP",question:pos.question,side:pos.side,
|
||
detail:`Stop-loss sold '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
|
||
}
|
||
function scaleStopLossPosition(p,pos,tier){
|
||
const originalShares=+(pos.original_shares||pos.shares).toFixed(2);
|
||
const originalCost=+(pos.original_cost||pos.cost).toFixed(2);
|
||
pos.original_shares=originalShares;
|
||
pos.original_cost=originalCost;
|
||
if(tier.sellFrac>=1||tier.id==="45"){
|
||
stopOutPosition(p,pos);
|
||
pos._closedByStop=true;
|
||
return;
|
||
}
|
||
const soldShares=+Math.min(pos.shares,originalShares*tier.sellFrac).toFixed(2);
|
||
if(soldShares<=0)return;
|
||
const proceeds=+(soldShares*pos.current_price).toFixed(2);
|
||
const realizedCost=+Math.min(pos.cost,originalCost*tier.sellFrac).toFixed(2);
|
||
const realizedPnl=+(proceeds-realizedCost).toFixed(2);
|
||
p.cash=+(p.cash+proceeds).toFixed(2);
|
||
pos.shares=+(pos.shares-soldShares).toFixed(2);
|
||
pos.cost=+(pos.cost-realizedCost).toFixed(2);
|
||
pos.value=+(pos.shares*pos.current_price).toFixed(2);
|
||
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
|
||
pos.stop_losses=doneStopLosses(pos);
|
||
pos.stop_losses[tier.id]=cycleIso();
|
||
p.history.push({date:logDay(),action:"STOP",question:pos.question,side:pos.side,
|
||
detail:`Stop-loss ${tier.label} sold 33% of '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (realized ${fmtUSD(realizedPnl)})`});
|
||
}
|
||
function closePosition(p,pos,reason,action="CLOSE"){
|
||
const proceeds=+(pos.shares*pos.current_price).toFixed(2);
|
||
p.cash=+(p.cash+proceeds).toFixed(2);
|
||
pos.exit_price=pos.current_price;pos.closed_at=cycleIso();
|
||
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
|
||
p.closed.push(pos);
|
||
p.history.push({date:logDay(),action,question:pos.question,side:pos.side,
|
||
detail:`${reason} '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
|
||
}
|
||
function takeProfitPosition(p,pos,tier){
|
||
const legacyMultiplier=pos.took_profit_at&&!pos.original_shares?2:1;
|
||
const originalShares=+(pos.original_shares||pos.shares*legacyMultiplier).toFixed(2);
|
||
const originalCost=+(pos.original_cost||pos.cost*legacyMultiplier).toFixed(2);
|
||
pos.original_shares=originalShares;
|
||
pos.original_cost=originalCost;
|
||
const soldShares=+Math.min(pos.shares,originalShares*tier.sellFrac).toFixed(2);
|
||
if(soldShares<=0)return;
|
||
const proceeds=+(soldShares*pos.current_price).toFixed(2);
|
||
const realizedCost=+Math.min(pos.cost,originalCost*tier.sellFrac).toFixed(2);
|
||
const realizedPnl=+(proceeds-realizedCost).toFixed(2);
|
||
p.cash=+(p.cash+proceeds).toFixed(2);
|
||
pos.shares=+(pos.shares-soldShares).toFixed(2);
|
||
pos.cost=+(pos.cost-realizedCost).toFixed(2);
|
||
pos.value=+(pos.shares*pos.current_price).toFixed(2);
|
||
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
|
||
pos.gain_stops=doneGainStops(pos);
|
||
pos.gain_stops[tier.id]=cycleIso();
|
||
p.history.push({date:logDay(),action:"GAIN",question:pos.question,side:pos.side,
|
||
detail:`Gain stop ${tier.label} sold 25% of '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (realized ${fmtUSD(realizedPnl)})`});
|
||
}
|
||
function daysHeld(pos){
|
||
if(!pos.opened_at)return 0;
|
||
const start=new Date(pos.opened_at).getTime(),end=new Date(cycleIso()).getTime();
|
||
if(!Number.isFinite(start)||!Number.isFinite(end))return 0;
|
||
return Math.max(0,(end-start)/86400000);
|
||
}
|
||
function exitReason(pos,fresh,analysis,cfg){
|
||
if(fresh.days_to_resolution!=null&&fresh.days_to_resolution<=EXIT_RESOLUTION_DAYS)return `Close before resolution (${fresh.days_to_resolution.toFixed(1)}d left)`;
|
||
if(daysHeld(pos)>=EXIT_STALE_DAYS)return `Stale exit after ${Math.floor(daysHeld(pos))} days`;
|
||
if(!analysis)return null;
|
||
const net=Math.abs(analysis.net_edge!=null?analysis.net_edge:analysis.edge||0);
|
||
if(analysis.side&&analysis.side!==pos.side&&net>=MIN_ENTRY_EDGE)return `Model flipped to ${analysis.side}`;
|
||
const minExit=Math.max(30,(cfg&&cfg.minConv?cfg.minConv:45)-8);
|
||
if((pos.unrealized_pnl||0)<0&&analysis.conviction<minExit&&net<MIN_ENTRY_EDGE*0.25)return `Conviction faded to ${Math.round(analysis.conviction)}`;
|
||
return null;
|
||
}
|
||
function markToMarket(p,priceMap,cfg=null,{policyExits=false}={}){
|
||
const stillOpen=[];
|
||
for(const pos of p.positions){
|
||
const fresh=priceMap[pos.market_id];
|
||
if(fresh==null){
|
||
closePosition(p,pos,"Settled/removed market");
|
||
continue;
|
||
}
|
||
const price=pos.side==="YES"?fresh.yes_price:fresh.no_price;
|
||
pos.current_price=+price.toFixed(4);pos.value=+(pos.shares*price).toFixed(2);
|
||
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
|
||
for(const stopTier of triggeredStopLosses(pos))scaleStopLossPosition(p,pos,stopTier);
|
||
if(pos._closedByStop){delete pos._closedByStop;continue;}
|
||
for(const gainTier of triggeredGainStops(pos))takeProfitPosition(p,pos,gainTier);
|
||
if(policyExits){
|
||
const reason=exitReason(pos,fresh,analyzeMarket(fresh),cfg);
|
||
if(reason){closePosition(p,pos,reason,"EXIT");continue;}
|
||
}
|
||
stillOpen.push(pos);
|
||
}
|
||
p.positions=stillOpen;
|
||
}
|
||
function peerMarketStats(st,selfId){
|
||
const stats={};
|
||
AGENTS.forEach(a=>{
|
||
if(a.id===selfId)return;
|
||
const p=st.agents&&st.agents[a.id]; if(!p)return;
|
||
const ret=(equity(p)/Math.max(p.starting_balance||STARTING_BALANCE,1)-1)*100;
|
||
(p.positions||[]).forEach(pos=>{
|
||
const id=pos.market_id||pos.asset; if(!id)return;
|
||
const key=`${id}:${pos.side}`;
|
||
const opp=`${id}:${pos.side==="YES"?"NO":"YES"}`;
|
||
if(!stats[key])stats[key]={same:0,samePnl:0,bestRet:-Infinity,names:[]};
|
||
stats[key].same++;stats[key].samePnl+=Number(pos.unrealized_pnl||0);stats[key].bestRet=Math.max(stats[key].bestRet,ret);stats[key].names.push(a.name);
|
||
if(!stats[opp])stats[opp]={same:0,samePnl:0,bestRet:-Infinity,names:[]};
|
||
stats[opp].opposite=(stats[opp].opposite||0)+1;
|
||
stats[opp].oppositePnl=(stats[opp].oppositePnl||0)+Number(pos.unrealized_pnl||0);
|
||
});
|
||
});
|
||
return stats;
|
||
}
|
||
function peerAdjustedSuggestion(s,peer){
|
||
const m=(peer&&peer[`${s.market_id}:${s.side}`])||{};
|
||
let boost=0,peerNote="";
|
||
if(m.same){
|
||
boost-=Math.min(14,4+m.same*3);
|
||
peerNote=`diversity guard: ${m.names.slice(0,3).join(", ")} already hold this side`;
|
||
}
|
||
if(m.same&&m.samePnl< -50){boost-=Math.min(8,2+m.same*1.5);peerNote=`peer warning: same-side holders are losing`;}
|
||
if(m.opposite&&m.oppositePnl>50){boost-=Math.min(12,4+m.opposite*2);peerNote=`peer warning: opposite-side holders are winning`;}
|
||
return Object.assign({},s,{peer_boost:boost,peer_note:peerNote,peer_conviction:+(Number(s.conviction||0)+boost).toFixed(1)});
|
||
}
|
||
function addPortfolioMarketsToSet(set,p){
|
||
(p&&p.positions||[]).forEach(pos=>{const id=pos.market_id||pos.asset;if(id)set.add(String(id));});
|
||
return set;
|
||
}
|
||
function occupiedStrategyMarkets(st,selfId=null){
|
||
const ids=new Set();
|
||
AGENTS.filter(a=>a.kind==="strategy").forEach(a=>{
|
||
if(a.id===selfId)return;
|
||
addPortfolioMarketsToSet(ids,st.agents&&st.agents[a.id]);
|
||
});
|
||
return ids;
|
||
}
|
||
function overlapKeeperScore(pos,cfg){
|
||
const conviction=Number(pos.conviction||cfg.minConv||50);
|
||
const pnlBoost=Math.max(-8,Math.min(8,Number(pos.unrealized_pnl||0)/75));
|
||
const valueBoost=Math.min(5,Number(pos.value||pos.cost||0)/1000);
|
||
return conviction+pnlBoost+valueBoost;
|
||
}
|
||
function reduceStrategyOverlap(st){
|
||
const groups={};
|
||
AGENTS.filter(a=>a.kind==="strategy").forEach(cfg=>{
|
||
const p=st.agents&&st.agents[cfg.id]; if(!p)return;
|
||
(p.positions||[]).forEach(pos=>{
|
||
const id=pos.market_id||pos.asset; if(!id)return;
|
||
if(!groups[id])groups[id]=[];
|
||
groups[id].push({cfg,p,pos,score:overlapKeeperScore(pos,cfg)});
|
||
});
|
||
});
|
||
Object.values(groups).filter(g=>g.length>1).forEach(group=>{
|
||
group.sort((a,b)=>(b.score-a.score)||((b.pos.value||0)-(a.pos.value||0)));
|
||
group.slice(1).forEach(({p,pos})=>closePosition(p,pos,"Overlap guard rotated this agent into a different market","EXIT"));
|
||
});
|
||
AGENTS.filter(a=>a.kind==="strategy").forEach(cfg=>{
|
||
const p=st.agents&&st.agents[cfg.id]; if(p)p.positions=(p.positions||[]).filter(pos=>!pos.closed_at);
|
||
});
|
||
}
|
||
function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=null){
|
||
const d=decision||{minConv:cfg.minConv,maxNew:cfg.maxNew,maxFrac:cfg.maxFrac,reserve:0.05};
|
||
p.lastDecision=decision||null;
|
||
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)return [];
|
||
const avoid=avoidMarketIds||new Set();
|
||
const cands=rankedSugs.map(s=>peerAdjustedSuggestion(s,peerStats)).filter(s=>s.trade_ready&&(s.side==="YES"||s.side==="NO")&&s.peer_conviction>=d.minConv
|
||
&&s.conviction>=48&&s.entry_price>=0.08&&s.entry_price<=0.92
|
||
&&Math.abs(s.net_edge!=null?s.net_edge:s.edge)>=MIN_ENTRY_EDGE&&(s.days_to_resolution==null||s.days_to_resolution>=MIN_ENTRY_DAYS)
|
||
&&s.volume>=MIN_VOLUME&&s.liquidity>=MIN_LIQUIDITY&&(s.volume_24hr>=500||s.conviction>=62)
|
||
&&(s.evidence_score==null||s.evidence_score>=0.50)
|
||
&&!avoid.has(String(s.market_id))&&!hasPosition(p,s.market_id)&&!hasStoppedToday(p,s.market_id)&&(focus==="All"||!focus||s.category===focus))
|
||
.sort((a,b)=>(b.peer_conviction-a.peer_conviction)||((b.peer_boost||0)-(a.peer_boost||0)));
|
||
let opened=0,openedIds=[];
|
||
for(const s of cands){
|
||
if(opened>=d.maxNew)break;
|
||
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)break;
|
||
const eq=equity(p),investable=p.cash-eq*d.reserve;
|
||
if(investable<=50)break;
|
||
let frac;
|
||
if(cfg.flat){frac=d.maxFrac;}
|
||
else{const base=(s.peer_conviction/100)*Math.min(1,Math.abs(s.net_edge!=null?s.net_edge:s.edge)/EDGE_SCALE);frac=Math.min(d.maxFrac,cfg.kelly*base);}
|
||
if(s.peer_boost<0)frac*=0.82;
|
||
if(s.peer_boost>0&&decision&&decision.urgency>0.65)frac*=1.08;
|
||
let stake=Math.min(eq*frac,investable);
|
||
if(stake<50)continue;
|
||
const entry=s.entry_price; if(entry<=0||entry>=1)continue;
|
||
const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
|
||
if(cost>p.cash)continue;
|
||
p.cash=+(p.cash-cost).toFixed(2);
|
||
p.positions.push({market_id:s.market_id,question:s.question,side:s.side,shares,
|
||
token_id:(s.side==="YES"?s.clob_yes:s.clob_no)||null,
|
||
entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),cost,value:cost,
|
||
original_shares:shares,original_cost:cost,unrealized_pnl:0,conviction:s.peer_conviction,category:s.category,opened_at:cycleIso(),url:s.url||"",
|
||
peer_note:s.peer_note||"",entry_reason:s.rationale||"",net_edge:s.net_edge,evidence_score:s.evidence_score,friction:s.friction,chase_penalty:s.chase_penalty,quality:s.quality,
|
||
gain_stops:{},stop_losses:{}});
|
||
p.history.push({date:logDay(),action:"OPEN",question:s.question,side:s.side,
|
||
detail:`${decision?decision.mode+" mode — ":""}Bought ${shares} ${s.side} '${s.question.slice(0,40)}' @ ${pct(entry)} for ${fmtUSD(cost)} · net edge ${((Math.abs(s.net_edge!=null?s.net_edge:s.edge))*100).toFixed(1)}c · evidence ${Math.round((s.evidence_score||0)*100)}${s.peer_note?` (${s.peer_note})`:""}`});
|
||
opened++;openedIds.push(String(s.market_id));
|
||
}
|
||
return openedIds;
|
||
}
|
||
function recordSnapshot(p){
|
||
const eq=equity(p);
|
||
const snap={date:logDay(),timestamp:(SIM_DAY?new Date(SIM_DAY+"T12:00:00Z").toISOString():(SNAP_TS||nowIso())),cash:+p.cash.toFixed(2),
|
||
equity:+eq.toFixed(2),pnl:+(eq-p.starting_balance).toFixed(2),
|
||
return_pct:+((eq/p.starting_balance-1)*100).toFixed(2),open_positions:p.positions.length};
|
||
p.snapshots=SIM_DAY?p.snapshots.filter(s=>s.date!==snap.date):p.snapshots.filter(s=>s.timestamp!==snap.timestamp);
|
||
p.snapshots.push(snap);p.snapshots.sort((a,b)=>new Date(a.timestamp||a.date)-new Date(b.timestamp||b.date));
|
||
}
|
||
function agentMomentumStats(p){
|
||
const snaps=(p.snapshots||[]).slice().filter(s=>Number.isFinite(s.return_pct)).sort((a,b)=>snapTime(a)-snapTime(b));
|
||
const returns=snaps.map(s=>Number(s.return_pct||0));
|
||
const last=returns[returns.length-1]||0;
|
||
const prev=returns[returns.length-2]??last;
|
||
const recent=returns.length>4?last-returns[returns.length-4]:last-prev;
|
||
const emaFast=returns.length?ema(returns,3):[0];
|
||
const emaSlow=returns.length?ema(returns,7):[0];
|
||
const macd=emaFast[emaFast.length-1]-emaSlow[emaSlow.length-1];
|
||
const signalArr=ema(returns.map((_,i)=>emaFast[i]-emaSlow[i]),3);
|
||
const macdHist=macd-(signalArr[signalArr.length-1]||0);
|
||
const high=Math.max(...returns,last,0);
|
||
const drawdown=last-high;
|
||
return {last,prev,recent,macd,macdHist,drawdown,snaps};
|
||
}
|
||
function copycatCandidateScore(cfg,p){
|
||
const m=agentMomentumStats(p);
|
||
const eq=equity(p);
|
||
const open=(p.positions||[]).length;
|
||
const openPnl=(p.positions||[]).reduce((sum,pos)=>sum+(pos.unrealized_pnl||0),0);
|
||
const cashRatio=p.cash/Math.max(eq,1);
|
||
const score=
|
||
m.last*0.40+
|
||
m.recent*1.20+
|
||
m.macd*1.60+
|
||
m.macdHist*1.80+
|
||
Math.min(8,openPnl/100)*0.35+
|
||
Math.min(3,open)*0.45+
|
||
Math.max(-8,m.drawdown)*0.35+
|
||
cashRatio*0.60;
|
||
return {id:cfg.id,cfg,p,score,m,openPnl,cashRatio};
|
||
}
|
||
function chooseCopycatLeader(st){
|
||
const cands=AGENTS.filter(a=>a.kind==="strategy").map(cfg=>copycatCandidateScore(cfg,st.agents[cfg.id]||defaultPortfolio()));
|
||
const eligible=cands.filter(c=>c.m.last>0&&c.m.recent>-3&&c.openPnl>-300);
|
||
(eligible.length?eligible:cands).sort((a,b)=>b.score-a.score);
|
||
return (eligible.length?eligible:cands)[0]||null;
|
||
}
|
||
function copycatReason(candidate){
|
||
if(!candidate)return "No eligible strategy has enough data, so the copycat is waiting.";
|
||
const m=candidate.m;
|
||
const bits=[
|
||
`return ${fmtPct(m.last)}`,
|
||
`recent momentum ${m.recent>=0?"+":""}${m.recent.toFixed(2)} pts`,
|
||
`MACD ${m.macd>=0?"+":""}${m.macd.toFixed(2)}`,
|
||
`histogram ${m.macdHist>=0?"+":""}${m.macdHist.toFixed(2)}`,
|
||
];
|
||
if(m.drawdown<0)bits.push(`drawdown ${m.drawdown.toFixed(2)} pts`);
|
||
return `copying ${candidate.cfg.name} because it has the strongest forward score: ${bits.join(", ")}.`;
|
||
}
|
||
/* ---------- Copy-trading: Polymarket leaders ---------- */
|
||
const hasAsset=(p,a)=>p.positions.some(x=>x.asset===a);
|
||
async function fetchTokenPrice(asset){
|
||
try{const r=await fetch(`${CLOB}/midpoint?token_id=${asset}`);
|
||
if(r.ok){const d=await r.json();const m=parseFloat(d.mid);if(m>0&&m<1)return m;}}catch(e){}
|
||
return null;
|
||
}
|
||
async function fetchTraderPositions(wallet){
|
||
try{const r=await fetch(`${DATA_API}/positions?user=${wallet}&sizeThreshold=1&limit=80`);
|
||
if(!r.ok)return [];
|
||
const d=await r.json();
|
||
return d.filter(p=>(p.outcome==="Yes"||p.outcome==="No")&&p.curPrice>0.02&&p.curPrice<0.98&&!p.redeemable&&p.size*p.curPrice>=1)
|
||
.map(p=>({asset:String(p.asset),conditionId:p.conditionId,outcome:p.outcome,side:String(p.outcome).toUpperCase(),
|
||
curPrice:+p.curPrice,title:p.title||"market",value:p.size*p.curPrice,url:rawPositionUrl(p)}));
|
||
}catch(e){return [];}
|
||
}
|
||
function prettyName(n,w){
|
||
if(n&&!/^0x/i.test(n))return n; // real display name / pseudonym
|
||
return "0x"+String(w).slice(2,6)+"…"+String(w).slice(-4); // anonymous → short wallet
|
||
}
|
||
function rawPositionUrl(p){
|
||
const slug=p.eventSlug||p.event_slug||p.slug||p.marketSlug||p.market_slug;
|
||
return slug?`https://polymarket.com/event/${slug}`:marketSearchUrl(p.title||p.question||"");
|
||
}
|
||
async function discoverWhales(){
|
||
const picked=[];
|
||
try{
|
||
const r=await fetch(`${LB_API}/profit?window=all&limit=40`);
|
||
const leaders=r.ok?await r.json():[];
|
||
for(const L of leaders){
|
||
if(picked.length>=4)break;
|
||
const tps=await fetchTraderPositions(L.proxyWallet);
|
||
if(tps.length>=1)picked.push({wallet:L.proxyWallet,
|
||
name:prettyName(L.name||L.pseudonym,L.proxyWallet),profit:Math.round(L.amount||0)});
|
||
}
|
||
}catch(e){}
|
||
return picked;
|
||
}
|
||
async function runWhaleAgent(p,wallet,decision){
|
||
const d=decision||{maxNew:6,maxFrac:0.12,reserve:0.12};
|
||
p.lastDecision=decision||null;
|
||
// mark existing copied positions to market via CLOB token price
|
||
for(const pos of p.positions){
|
||
const pr=await fetchTokenPrice(pos.asset);
|
||
if(pr!=null){pos.current_price=+pr.toFixed(4);pos.value=+(pos.shares*pr).toFixed(2);pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);}
|
||
}
|
||
const tps=await fetchTraderPositions(wallet);
|
||
const held=new Set(tps.map(t=>t.asset));
|
||
const keep=[];
|
||
for(const pos of p.positions){
|
||
for(const stopTier of triggeredStopLosses(pos))scaleStopLossPosition(p,pos,stopTier);
|
||
if(pos._closedByStop){
|
||
delete pos._closedByStop;
|
||
}else if(!held.has(pos.asset)){
|
||
closePosition(p,pos,"Copied trader exited");
|
||
}else{
|
||
for(const gainTier of triggeredGainStops(pos))takeProfitPosition(p,pos,gainTier);
|
||
keep.push(pos);
|
||
}
|
||
}
|
||
p.positions=keep;
|
||
const maxNew=Math.min(d.maxNew||6,4),maxFrac=Math.min(d.maxFrac||0.10,0.12),reserve=Math.max(d.reserve||0.14,0.12);
|
||
const targets=[...tps].filter(t=>t.curPrice>=0.12&&t.curPrice<=0.88&&t.value>=20).sort((a,b)=>b.value-a.value).slice(0,maxNew); // copy their strongest liquid bets, adapted by stance
|
||
const book=targets.reduce((s,t)=>s+t.value,0)||1;
|
||
for(const t of targets){
|
||
if(hasAsset(p,t.asset)||hasStoppedToday(p,t.asset))continue;
|
||
const eq=equity(p),investable=p.cash-eq*reserve; if(investable<=50)break;
|
||
const weight=t.value/book;
|
||
const stake=Math.min(eq*Math.min(maxFrac,weight),investable);
|
||
if(stake<40)continue;
|
||
const entry=t.curPrice; const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
|
||
if(cost>p.cash)continue;
|
||
p.cash=+(p.cash-cost).toFixed(2);
|
||
p.positions.push({asset:t.asset,token_id:t.asset,market_id:t.conditionId,question:t.title,side:t.side,category:"Copy",
|
||
shares,entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),cost,value:cost,
|
||
original_shares:shares,original_cost:cost,unrealized_pnl:0,conviction:null,opened_at:cycleIso(),url:t.url||marketSearchUrl(t.title),
|
||
gain_stops:{},stop_losses:{}});
|
||
p.history.push({date:logDay(),action:"OPEN",question:t.title,side:t.side,
|
||
detail:`Copied trade — ${shares} ${t.side} '${t.title.slice(0,36)}' @ ${pct(entry)} for ${fmtUSD(cost)}`});
|
||
}
|
||
recordSnapshot(p);
|
||
}
|
||
|
||
/* ---------- Copycat: shadow the strongest momentum/MACD strategy ---------- */
|
||
function runCopycat(p,leader,priceMap,candidate=null){
|
||
const reason=candidate?copycatReason(candidate):"watching the strategy board and rotating toward the account with the best momentum/MACD setup.";
|
||
const safeToCopy=candidate&&candidate.m.last>0&&candidate.m.recent>-3&&candidate.openPnl>-300;
|
||
const copyLimit=4;
|
||
p.lastDecision={mode:safeToCopy?"Selective Momentum Copy":"Copycat Guard",reason:safeToCopy?reason:`No strategy is healthy enough to copy aggressively, so I am protecting cash instead. ${reason}`,minConv:0,maxNew:safeToCopy?Math.min(copyLimit,leader.positions.length):0,maxFrac:0.07,reserve:0.12,
|
||
copyScore:candidate?+candidate.score.toFixed(2):null,
|
||
macd:candidate?+candidate.m.macd.toFixed(2):null,
|
||
momentum:candidate?+candidate.m.recent.toFixed(2):null};
|
||
markToMarket(p,priceMap);
|
||
if(!safeToCopy){
|
||
for(const pos of [...p.positions]){
|
||
if((pos.unrealized_pnl||0)<-75)closePosition(p,pos,"Copycat guard cut weak copied position","EXIT");
|
||
}
|
||
p.positions=p.positions.filter(pos=>!pos.closed_at);
|
||
recordSnapshot(p);
|
||
return;
|
||
}
|
||
const held=new Set(leader.positions.map(x=>x.market_id));
|
||
const keep=[];
|
||
for(const pos of p.positions){
|
||
if(!held.has(pos.market_id)){
|
||
closePosition(p,pos,"Leader exited");
|
||
}else keep.push(pos);
|
||
}
|
||
p.positions=keep;
|
||
const leaderPicks=leader.positions.filter(x=>(x.unrealized_pnl||0)>=-50&&(x.current_price||0)>0.05&&(x.current_price||0)<0.95)
|
||
.sort((a,b)=>((b.unrealized_pnl||0)/(b.cost||1))-((a.unrealized_pnl||0)/(a.cost||1))||((b.conviction||0)-(a.conviction||0))||((b.value||b.cost)-(a.value||a.cost)))
|
||
.slice(0,copyLimit);
|
||
const book=leaderPicks.reduce((s,x)=>s+(x.value||x.cost),0)||1;
|
||
for(const lp of [...leaderPicks].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.12; if(investable<=50)break;
|
||
const weight=(lp.value||lp.cost)/book;
|
||
const stake=Math.min(eq*Math.min(0.07,weight),investable);
|
||
if(stake<50)continue;
|
||
const entry=lp.current_price; if(entry<=0||entry>=1)continue;
|
||
const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
|
||
if(cost>p.cash)continue;
|
||
p.cash=+(p.cash-cost).toFixed(2);
|
||
p.positions.push({market_id:lp.market_id,token_id:lp.token_id||null,question:lp.question,side:lp.side,category:lp.category,
|
||
shares,entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),cost,value:cost,
|
||
original_shares:shares,original_cost:cost,unrealized_pnl:0,conviction:lp.conviction,opened_at:cycleIso(),url:lp.url||"",
|
||
gain_stops:{},stop_losses:{}});
|
||
p.history.push({date:logDay(),action:"OPEN",question:lp.question,side:lp.side,
|
||
detail:`Copied ${(candidate&&candidate.cfg&&candidate.cfg.name)||"momentum leader"} — ${shares} ${lp.side} '${lp.question.slice(0,36)}' @ ${pct(entry)}`});
|
||
}
|
||
recordSnapshot(p);
|
||
}
|
||
|
||
/* ---------- Trailing-week backtest seeding ----------
|
||
Marks each agent's CURRENT holdings against real Polymarket price history
|
||
so the equity curves show a plausible past week instead of a flat line. */
|
||
function lastNDates(n){const out=[];const now=new Date();for(let i=n-1;i>=0;i--){const x=new Date(now);x.setUTCDate(now.getUTCDate()-i);out.push(x.toISOString().slice(0,10));}return out;}
|
||
async function fetchPriceHistory(tokenId){
|
||
try{const r=await fetch(`${CLOB}/prices-history?market=${tokenId}&interval=1w&fidelity=1440`);
|
||
if(!r.ok)return null;const d=await r.json();const map={};
|
||
(d.history||[]).forEach(h=>{map[new Date(h.t*1000).toISOString().slice(0,10)]=h.p;});
|
||
return Object.keys(map).length?map:null;}catch(e){return null;}
|
||
}
|
||
async function fetchClobTokens(marketId){
|
||
try{const r=await fetch(`${GAMMA}/markets/${marketId}`);if(!r.ok)return null;const d=await r.json();
|
||
const t=parseJsonField(d.clobTokenIds).map(String);return t.length===2?t:null;}catch(e){return null;}
|
||
}
|
||
function priceOnDay(map,day,fallback){
|
||
if(!map)return fallback;let best=null,bd=null;
|
||
for(const dt in map){if(dt<=day&&(bd===null||dt>bd)){bd=dt;best=map[dt];}}
|
||
if(best!=null)return best;
|
||
const ks=Object.keys(map).sort();return ks.length?map[ks[0]]:fallback;
|
||
}
|
||
async function fetchTraderActivity(wallet){
|
||
try{const r=await fetch(`${DATA_API}/activity?user=${wallet}&limit=500&type=TRADE`);
|
||
if(!r.ok)return [];const d=await r.json();
|
||
return d.filter(x=>x.asset&&(x.side==="BUY"||x.side==="SELL"))
|
||
.map(x=>({asset:String(x.asset),side:x.side,size:+x.size||0,ts:x.timestamp}));}catch(e){return [];}
|
||
}
|
||
async function fetchTraderPositionsRaw(wallet){
|
||
try{const r=await fetch(`${DATA_API}/positions?user=${wallet}&sizeThreshold=1&limit=100`);
|
||
if(!r.ok)return [];const d=await r.json();
|
||
return d.filter(p=>(p.outcome==="Yes"||p.outcome==="No")&&!p.redeemable&&p.curPrice>0.02&&p.curPrice<0.98)
|
||
.map(p=>({asset:String(p.asset),size:+p.size||0,curPrice:+p.curPrice,title:p.title||""}));}catch(e){return [];}
|
||
}
|
||
/* Replay each whale by reconstructing its past holdings from on-chain trade
|
||
activity (current size minus net trades after each day), valued at real
|
||
historical token prices — then scaled to the $10k paper bankroll. */
|
||
async function backtestWhales(st,dates){
|
||
for(const a of AGENTS.filter(x=>x.kind==="whale")){
|
||
const p=st.agents[a.id], wh=st.whales[a.id];
|
||
const flat=()=>{for(const d of dates){SIM_DAY=d;recordSnapshot(p);}SIM_DAY=null;};
|
||
if(!wh){flat();continue;}
|
||
setStatus(`backtest: replaying ${wh.name}…`,true);
|
||
let cur=[],acts=[];
|
||
try{cur=await fetchTraderPositionsRaw(wh.wallet);}catch(e){}
|
||
try{acts=await fetchTraderActivity(wh.wallet);}catch(e){}
|
||
cur.sort((x,y)=>y.size*y.curPrice-x.size*x.curPrice);
|
||
const top=cur.slice(0,12);
|
||
if(!top.length){flat();continue;}
|
||
const histA={};
|
||
for(const t of top)histA[t.asset]=await fetchPriceHistory(t.asset);
|
||
const dayVal={};
|
||
for(const day of dates){
|
||
const endTs=Math.floor(new Date(day+"T23:59:59Z").getTime()/1000);
|
||
let v=0;
|
||
for(const t of top){
|
||
let netAfter=0;
|
||
for(const ac of acts)if(ac.asset===t.asset&&ac.ts>endTs)netAfter+=(ac.side==="BUY"?ac.size:-ac.size);
|
||
const holdings=Math.max(0,t.size-netAfter);
|
||
v+=holdings*priceOnDay(histA[t.asset],day,t.curPrice);
|
||
}
|
||
dayVal[day]=v;
|
||
}
|
||
const anchor=dayVal[dates[dates.length-1]]||1;
|
||
p.snapshots=dates.map(day=>{
|
||
const eq=10000*(dayVal[day]/(anchor||1));
|
||
return {date:day,timestamp:new Date(day+"T12:00:00Z").toISOString(),cash:0,
|
||
equity:+eq.toFixed(2),pnl:+(eq-10000).toFixed(2),return_pct:+((eq/10000-1)*100).toFixed(2),
|
||
open_positions:top.length,seeded:true};
|
||
});
|
||
}
|
||
SIM_DAY=null;
|
||
}
|
||
/* Full day-by-day backtest: for each of the past 7 days, rebuild every market's
|
||
state from that day's real price (+ correct time-to-resolution), re-run the
|
||
scoring engine, and step every strategy agent through a day of trading.
|
||
NOTE: volume/liquidity signals use current values as a proxy — Polymarket
|
||
does not expose historical volume. Price & timing are truly historical. */
|
||
async function backtestWeek(st){
|
||
const dates=lastNDates(7), start=dates[0];
|
||
AGENTS.forEach(a=>{st.agents[a.id]=defaultPortfolio();});
|
||
if(AGENTS.some(a=>a.kind==="whale"&&!st.whales[a.id])){
|
||
setStatus("backtest: finding top Polymarket traders…",true);
|
||
const picked=await discoverWhales();
|
||
AGENTS.filter(a=>a.kind==="whale").forEach(a=>{if(!st.whales[a.id]&&picked[a.slot])st.whales[a.id]=picked[a.slot];});
|
||
}
|
||
setStatus("backtest: loading markets…",true);
|
||
const markets=await fetchMarkets(2,100);
|
||
const uni=markets.filter(m=>m.clob_token_ids&&m.clob_token_ids.length===2&&(!m.end_date||m.end_date>=start)).slice(0,100);
|
||
const hist={}; let n=0;
|
||
for(const m of uni){setStatus(`backtest: price history ${++n}/${uni.length}…`,true);hist[m.id]=await fetchPriceHistory(m.clob_token_ids[0]);}
|
||
const focus=getFocus();
|
||
for(const day of dates){
|
||
SIM_DAY=day;
|
||
setStatus(`backtest: simulating ${day}…`,true);
|
||
const snaps=[];
|
||
for(const m of uni){
|
||
const yp=priceOnDay(hist[m.id],day,null);
|
||
if(yp==null||yp<=0.02||yp>=0.98)continue;
|
||
const dtr=m.end_date?(new Date(m.end_date)-new Date(day+"T12:00:00Z"))/86400000:null;
|
||
if(dtr!=null&&dtr<-0.5)continue;
|
||
snaps.push(Object.assign({},m,{yes_price:+yp.toFixed(4),no_price:+(1-yp).toFixed(4),days_to_resolution:dtr}));
|
||
}
|
||
const sugs=generateSuggestions(snaps);
|
||
const priceMap={}; snaps.forEach(s=>priceMap[s.id]={yes_price:s.yes_price,no_price:s.no_price});
|
||
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
||
const p=st.agents[cfg.id];
|
||
markToMarket(p,priceMap,cfg,{policyExits:true});
|
||
}
|
||
reduceStrategyOverlap(st);
|
||
const claimedMarkets=new Set();
|
||
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
||
const p=st.agents[cfg.id];
|
||
const occupied=occupiedStrategyMarkets(st,cfg.id);
|
||
claimedMarkets.forEach(id=>occupied.add(id));
|
||
openPositions(p,cfg,cfg.rank(sugs),focus,null,occupied,peerMarketStats(st,cfg.id)).forEach(id=>claimedMarkets.add(id));
|
||
recordSnapshot(p);
|
||
}
|
||
const copyPick=chooseCopycatLeader(st);
|
||
const leaderId=copyPick?copyPick.id:AGENTS.filter(a=>a.kind==="strategy")[0].id;
|
||
st.copycatLeader=leaderId;
|
||
runCopycat(st.agents.copycat,st.agents[leaderId],priceMap,copyPick);
|
||
if(day===dates[dates.length-1])saveSuggestions(sugs,markets.length); // latest live-ish board for the Suggestions tab
|
||
}
|
||
SIM_DAY=null;
|
||
await backtestWhales(st,dates);
|
||
// establish current whale paper positions so live tracking continues
|
||
for(const a of AGENTS.filter(x=>x.kind==="whale")){const wh=st.whales[a.id];if(wh){try{await runWhaleAgent(st.agents[a.id],wh.wallet);}catch(e){}}}
|
||
st.seeded=true;
|
||
}
|
||
|
||
async function runDailyCycle(){
|
||
if(CYCLE_RUNNING)return {suggestions:(loadSuggestions().suggestions||[]).length,skipped:true,busy:true,hour:currentCycleHour()};
|
||
CYCLE_RUNNING=true;
|
||
try{
|
||
const cloud=await loadAuthoritativeCloudState();
|
||
if(!cloud.ok)throw new Error("Cloud sync unavailable. Refusing to run a local-only cycle.");
|
||
let st=loadState();
|
||
const hour=currentCycleHour();
|
||
if(st.seeded&&st.last_cycle_hour===hour){
|
||
setStatus("up to date",false);
|
||
return {suggestions:(loadSuggestions().suggestions||[]).length,skipped:true,hour};
|
||
}
|
||
SNAP_TS=nowIso();
|
||
setStatus("fetching markets…",true);
|
||
const markets=await fetchMarkets(Infinity,100,count=>setStatus(`fetching all active markets (${count})…`,true));
|
||
const analysisMarkets=selectMarketsForAnalysis(markets);
|
||
setStatus(`selected top ${analysisMarkets.length.toLocaleString()} markets for analysis…`,true);
|
||
setStatus("checking real-world context…",true);
|
||
const realWorldSignals=await fetchRealWorldSignals(analysisMarkets);
|
||
setStatus("analyzing expected value…",true);
|
||
const sugs=generateSuggestions(analysisMarkets,SUGGESTION_TOTAL,SUGGESTION_PER_CATEGORY,realWorldSignals);
|
||
saveSuggestions(sugs,markets.length,analysisMarkets.length);
|
||
st=loadState();
|
||
const emailOffsets=agentHistoryOffsets(st);
|
||
const focus=getFocus();
|
||
// Price map for suggestion-market positions (strategy + copycat only).
|
||
const ids=new Set();
|
||
AGENTS.filter(a=>a.kind==="strategy"||a.kind==="copycat").forEach(a=>
|
||
(st.agents[a.id].positions||[]).forEach(pos=>{if(pos.market_id)ids.add(pos.market_id);}));
|
||
setStatus("marking positions…",true);
|
||
const priceMap={};
|
||
for(const id of ids){priceMap[id]=await fetchMarketPrice(id);}
|
||
// 1) in-house strategy agents
|
||
setStatus("strategy agents trading…",true);
|
||
const preBoard=AGENTS.filter(a=>a.kind==="strategy").map(a=>({id:a.id,eq:equity(st.agents[a.id])})).sort((x,y)=>y.eq-x.eq);
|
||
const leaderEq=preBoard[0]?preBoard[0].eq:STARTING_BALANCE;
|
||
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
||
const p=st.agents[cfg.id];
|
||
markToMarket(p,priceMap,cfg,{policyExits:true});
|
||
}
|
||
reduceStrategyOverlap(st);
|
||
const claimedMarkets=new Set();
|
||
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
||
const p=st.agents[cfg.id];
|
||
const rank=preBoard.findIndex(x=>x.id===cfg.id)+1||preBoard.length;
|
||
const decision=adaptiveDecision(cfg,p,rank,preBoard.length,leaderEq);
|
||
const occupied=occupiedStrategyMarkets(st,cfg.id);
|
||
claimedMarkets.forEach(id=>occupied.add(id));
|
||
openPositions(p,cfg,cfg.rank(sugs),focus,decision,occupied,peerMarketStats(st,cfg.id)).forEach(id=>claimedMarkets.add(id));
|
||
recordSnapshot(p);
|
||
}
|
||
// 2) copycat shadows the in-house agent with the strongest MACD/momentum score
|
||
const copyPick=chooseCopycatLeader(st);
|
||
st.copycatLeader=copyPick?copyPick.id:AGENTS.filter(a=>a.kind==="strategy")[0].id;
|
||
runCopycat(st.agents["copycat"],st.agents[st.copycatLeader],priceMap,copyPick);
|
||
// 3) whale-copy agents mirror top Polymarket traders
|
||
if(AGENTS.some(a=>a.kind==="whale"&&!st.whales[a.id])){
|
||
setStatus("finding top Polymarket traders…",true);
|
||
const picked=await discoverWhales();
|
||
AGENTS.filter(a=>a.kind==="whale").forEach(a=>{if(!st.whales[a.id]&&picked[a.slot])st.whales[a.id]=picked[a.slot];});
|
||
}
|
||
for(const a of AGENTS.filter(x=>x.kind==="whale")){
|
||
const wh=st.whales[a.id];
|
||
if(!wh){recordSnapshot(st.agents[a.id]);continue;}
|
||
setStatus(`copying ${wh.name}…`,true);
|
||
const allBoard=AGENTS.map(c=>({id:c.id,eq:equity(st.agents[c.id])})).sort((x,y)=>y.eq-x.eq);
|
||
const rank=allBoard.findIndex(x=>x.id===a.id)+1||allBoard.length;
|
||
const decision=adaptiveDecision(a,st.agents[a.id],rank,allBoard.length,allBoard[0]?allBoard[0].eq:STARTING_BALANCE);
|
||
try{await runWhaleAgent(st.agents[a.id],wh.wallet,decision);}
|
||
catch(e){recordSnapshot(st.agents[a.id]);}
|
||
}
|
||
st.date=todayStr();st.last_run=SNAP_TS;st.last_cycle_hour=hour;
|
||
const emailEvents=collectNewAgentEvents(st,emailOffsets);
|
||
saveState(st);
|
||
const pushed=await pushCloudState();
|
||
setStatus("up to date",false);
|
||
if(!pushed)return {suggestions:(loadSuggestions().suggestions||[]).length,skipped:true,hour};
|
||
await notifyTradeDigest(emailEvents);
|
||
return {suggestions:sugs.length};
|
||
}finally{SNAP_TS=null;CYCLE_RUNNING=false;}
|
||
}
|
||
|
||
/* ---------- 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();renderEmailAlerts();renderLeaderboard();renderSuggestions();renderPortfolioTab();renderInvestTab();renderPaperTab();renderLiveMoneyTab();renderAbout();}
|
||
|
||
function renderOverview(){
|
||
const b=board(); const lead=b[0];
|
||
const avgRet=b.reduce((s,r)=>s+r.ret,0)/b.length;
|
||
const core=b.filter(r=>r.c.kind==="strategy"),coreAvg=core.reduce((s,r)=>s+r.ret,0)/Math.max(1,core.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:"All-agent avg",value:fmtPct(avgRet),cls:signClass(avgRet)},
|
||
{ic:"🧠",label:"Core strategy avg",value:fmtPct(coreAvg),cls:signClass(coreAvg)},
|
||
{ic:"💡",label:"Scored ideas",value:sugCount},
|
||
{ic:"🏆",label:"Agents",value:AGENTS.length},
|
||
];
|
||
$("stats").innerHTML=stats.map(s=>`<div class="stat"><div class="ic">${s.ic}</div><div class="label">${s.label}</div><div class="value ${s.cls||""}">${s.value}</div></div>`).join("");
|
||
const st=loadState();
|
||
if(st.last_run)$("lastRun").textContent="updated "+new Date(st.last_run).toLocaleString();
|
||
drawCombo("chart","comboLegend");
|
||
renderTopPicks();
|
||
}
|
||
function renderTopPicks(){
|
||
const root=$("topPicks"); if(!root)return;
|
||
const top=((loadSuggestions().suggestions)||[]).slice(0,5);
|
||
if(!top.length){root.innerHTML=`<div class="empty">Run a cycle to see top picks.</div>`;return;}
|
||
root.innerHTML=top.map(s=>`<div class="pos"><div>
|
||
<div class="pq">${s.url?`<a class="market-title" href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.question.slice(0,58))}${s.question.length>58?"…":""}</a>`:`${esc(s.question.slice(0,58))}${s.question.length>58?"…":""}`}</div>
|
||
<div class="sub"><span class="cat-badge" style="background:${catColor(s.category)}22;color:${catColor(s.category)}">${esc(s.category)}</span> BUY ${s.side} @ ${Math.round(s.entry_price*100)}¢</div>
|
||
</div><div class="right"><div style="font-family:'Space Grotesk';font-weight:700">${Math.round(s.conviction)}</div><div class="sub">conviction</div></div></div>`).join("");
|
||
}
|
||
function renderEmailAlerts(){
|
||
const input=$("emailAlertInput"),eventsRoot=$("emailAlertEvents"),agentsRoot=$("emailAlertAgents"),meta=$("emailAlertMeta"),status=$("emailAlertStatus");
|
||
if(!input||!eventsRoot||!agentsRoot)return;
|
||
const cfg=loadEmailAlerts();
|
||
input.value=cfg.email||"";
|
||
const selectedCount=AGENTS.filter(a=>cfg.agents&&cfg.agents[a.id]).length;
|
||
meta.textContent=cfg.enabled?`on for ${cfg.email} · ${selectedCount}/${AGENTS.length} agents`:"off";
|
||
eventsRoot.innerHTML=EMAIL_ALERT_TYPES.map(t=>`
|
||
<label class="chip ${cfg.events&&cfg.events[t.id]?"active":""}" style="cursor:pointer">
|
||
<input type="checkbox" data-email-event="${t.id}" ${cfg.events&&cfg.events[t.id]?"checked":""} style="display:none"> ${t.label}
|
||
</label>`).join("");
|
||
agentsRoot.innerHTML=AGENTS.map(a=>`
|
||
<label class="chip ${cfg.agents&&cfg.agents[a.id]?"active":""}" style="cursor:pointer;border-color:${cfg.agents&&cfg.agents[a.id]?a.color:""}">
|
||
<input type="checkbox" data-email-agent="${esc(a.id)}" ${cfg.agents&&cfg.agents[a.id]?"checked":""} style="display:none"> ${a.emoji} ${esc(a.name)}
|
||
</label>`).join("");
|
||
const providerLine=TRADE_EMAIL_STATUS
|
||
? (TRADE_EMAIL_STATUS.configured
|
||
? `Email backend ready via ${TRADE_EMAIL_STATUS.provider}.`
|
||
: `Email backend missing: add Resend, SMTP, or webhook env vars in Vercel.`)
|
||
: "Checking email backend...";
|
||
const sentLine=cfg.last_sent_at?` Last sent ${new Date(cfg.last_sent_at).toLocaleString()}.`:"";
|
||
status.textContent=cfg.last_error
|
||
? `Last error: ${cfg.last_error}`
|
||
: `${providerLine}${sentLine} Sends a digest whenever selected agents trade during a cycle.`;
|
||
const readAlertForm=()=>{
|
||
const email=input.value.trim();
|
||
const events={};
|
||
const agents={};
|
||
document.querySelectorAll("[data-email-event]").forEach(el=>{events[el.dataset.emailEvent]=el.checked;});
|
||
document.querySelectorAll("[data-email-agent]").forEach(el=>{agents[el.dataset.emailAgent]=el.checked;});
|
||
return {email,events,agents};
|
||
};
|
||
const hasSelectedAgent=(agents)=>Object.values(agents||{}).some(Boolean);
|
||
$("saveEmailAlertsBtn").onclick=()=>{
|
||
const form=readAlertForm();
|
||
const anyAgent=hasSelectedAgent(form.agents);
|
||
const saved=saveEmailAlerts({enabled:validEmail(form.email)&&anyAgent,email:form.email,events:form.events,agents:form.agents,last_error:validEmail(form.email)?(anyAgent?null:"Select at least one agent."):"Enter a valid email address."});
|
||
toast(saved.enabled?"Trade email alerts saved.":(anyAgent?"Enter a valid email to turn alerts on.":"Select at least one agent."));
|
||
renderEmailAlerts();pushCloudState(true);
|
||
};
|
||
$("testEmailAlertsBtn").onclick=async()=>{
|
||
const form=readAlertForm();
|
||
const anyAgent=hasSelectedAgent(form.agents);
|
||
const saved=saveEmailAlerts({enabled:validEmail(form.email)&&anyAgent,email:form.email,events:form.events,agents:form.agents,last_error:anyAgent?null:"Select at least one agent."});
|
||
if(!saved.enabled){toast(anyAgent?"Enter a valid email first.":"Select at least one agent.");renderEmailAlerts();return;}
|
||
const testAgent=AGENTS.find(a=>saved.agents&&saved.agents[a.id])||AGENTS[0];
|
||
await notifyTradeDigest([{
|
||
agent_id:testAgent.id,
|
||
agent_name:testAgent.name,
|
||
agent_emoji:testAgent.emoji,
|
||
action:"OPEN",
|
||
date:todayStr(),
|
||
question:"Test alert from Poly Arena",
|
||
side:"YES",
|
||
detail:`This confirms trade email alerts are connected for ${testAgent.name}.`,
|
||
equity:STARTING_BALANCE,
|
||
return_pct:0,
|
||
}],{test:true});
|
||
await refreshTradeEmailStatus();
|
||
pushCloudState(true);
|
||
};
|
||
document.querySelectorAll("[data-email-event],[data-email-agent]").forEach(el=>el.onchange=()=>{
|
||
const form=readAlertForm();
|
||
saveEmailAlerts({enabled:validEmail(form.email)&&hasSelectedAgent(form.agents),email:form.email,events:form.events,agents:form.agents,last_error:null});
|
||
renderEmailAlerts();pushCloudState(true);
|
||
});
|
||
}
|
||
function miniSpark(snaps,color){
|
||
if(!snaps||snaps.length<2)return `<svg width="120" height="34"></svg>`;
|
||
const pts=snaps.map(s=>s.equity),W=120,H=34,p=3;
|
||
const mn=Math.min(...pts),mx=Math.max(...pts),sp=(mx-mn)||1;
|
||
const x=i=>p+(i/(pts.length-1))*(W-2*p),y=v=>H-p-((v-mn)/sp)*(H-2*p);
|
||
const d=pts.map((v,i)=>`${i?"L":"M"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
||
return `<svg width="${W}" height="${H}"><path d="${d}" fill="none" stroke="${color}" stroke-width="2"/></svg>`;
|
||
}
|
||
function agentBlurb(cfg,st){
|
||
if(cfg.kind==="whale"){const w=st.whales[cfg.id];return w?`🐋 Mirrors <b>${esc(w.name)}</b>${w.profit?` · $${(w.profit/1e6).toFixed(1)}M profit`:""}. ${esc(cfg.blurb)}`:`${esc(cfg.blurb)} The trader is selected on the first successful run.`;}
|
||
if(cfg.kind==="copycat"){const L=AGENTS.find(a=>a.id===st.copycatLeader),p=st.agents.copycat||defaultPortfolio();return `🐒 Currently copying <b>${L?esc(L.name):"the strongest momentum setup"}</b>. ${esc(p.lastDecision&&p.lastDecision.reason?p.lastDecision.reason:cfg.blurb)}`;}
|
||
return cfg.blurb;
|
||
}
|
||
function agentPlainBlurb(cfg,st){
|
||
return agentBlurb(cfg,st).replace(/<[^>]*>/g,"");
|
||
}
|
||
function bestAndWorst(p){
|
||
const open=p.positions||[];
|
||
if(!open.length)return {best:"No open trades yet",worst:"No drawdowns yet"};
|
||
const sorted=[...open].sort((a,b)=>(b.unrealized_pnl||0)-(a.unrealized_pnl||0));
|
||
const label=x=>`${x.question.slice(0,34)}${x.question.length>34?"...":""} (${(x.unrealized_pnl||0)>=0?"+":""}${fmtUSD(x.unrealized_pnl||0)})`;
|
||
return {best:label(sorted[0]),worst:label(sorted[sorted.length-1])};
|
||
}
|
||
function decisionSummary(p){
|
||
const d=p.lastDecision;
|
||
if(!d)return "No live adaptation yet; this agent is still using its base rulebook.";
|
||
const emotion=d.emotion?` Emotion: ${d.emotion}${d.urgency!=null?` (${Math.round(d.urgency*100)}% urgency)`:""}.`:"";
|
||
return `${d.mode} mode: ${d.reason}${emotion} Limits now: ${d.maxNew} new trade${d.maxNew===1?"":"s"}, max ${(d.maxFrac*100).toFixed(1)}% per position${d.minConv?`, conviction ${d.minConv}+`:""}.`;
|
||
}
|
||
function renderAgentBrief(cfg,p,st){
|
||
const root=$("agentBrief"); if(!root)return;
|
||
const bw=bestAndWorst(p);
|
||
const risk=cfg.kind==="whale"?"Mirror risk":cfg.kind==="copycat"?"Adaptive":cfg.maxFrac>=0.12?"Higher risk":cfg.maxFrac<=0.05?"Lower risk":"Balanced";
|
||
const cadence=cfg.kind==="whale"?"Copies biggest live holdings":cfg.kind==="copycat"?"Follows MACD/momentum leader":`${cfg.maxNew||0} new trades max`;
|
||
const exitRule=cfg.kind==="strategy"?`Exit if edge fades, conviction weakens, <${EXIT_RESOLUTION_DAYS}d to resolve, or ${EXIT_STALE_DAYS}d stale`:cfg.kind==="copycat"?"Exit when momentum leader exits":"Exit when copied trader exits";
|
||
const thesis=agentPlainBlurb(cfg,st);
|
||
root.innerHTML=`
|
||
<div class="brief-main" style="border-top:3px solid ${cfg.color}">
|
||
<div class="brief-title"><span>${cfg.emoji}</span><span>${cfg.name}</span></div>
|
||
<p>${esc(thesis)}</p>
|
||
<p style="margin-top:9px"><b>Adaptation:</b> ${esc(decisionSummary(p))}</p>
|
||
</div>
|
||
<div class="brief-mini"><div class="k">Style</div><div class="v">${esc(risk)}</div></div>
|
||
<div class="brief-mini"><div class="k">Trade Rule</div><div class="v">${esc(cadence)}</div></div>
|
||
<div class="brief-mini"><div class="k">Stop Loss</div><div class="v">Sell all at -18%</div></div>
|
||
<div class="brief-mini"><div class="k">Gain Stop</div><div class="v">Sell 25% at +75%, +150%, +300%</div></div>
|
||
<div class="brief-mini"><div class="k">Policy Exit</div><div class="v">${esc(exitRule)}</div></div>
|
||
<div class="brief-mini"><div class="k">Best / Worst</div><div class="v"><span class="pos-val">${esc(bw.best)}</span><br><span class="neg-val">${esc(bw.worst)}</span></div></div>`;
|
||
}
|
||
function renderLeaderboard(){
|
||
const b=board(); const st=loadState();
|
||
$("lbSub").textContent="· "+b[0].c.name+" leads";
|
||
$("leaderboard").innerHTML=b.map((r,i)=>`
|
||
<div class="lb-card" data-agent="${r.c.id}" style="border-top:3px solid ${r.c.color}">
|
||
<span class="rankpill">#${i+1}</span>
|
||
<div class="lb-top"><span class="medal">${MEDALS[i]||""}</span><span class="lb-emoji">${r.c.emoji}</span>
|
||
<div><div class="lb-name">${r.c.name}</div><div class="lb-blurb">${agentBlurb(r.c,st)}</div></div></div>
|
||
<div class="lb-mid"><div class="lb-eq">${fmtUSD(r.eq)}</div><div class="lb-ret ${signClass(r.pnl)}">${fmtPct(r.ret)}</div></div>
|
||
<div class="lb-foot"><span class="muted small">${r.p.positions.length} open · ${r.p.closed.length} closed</span>${miniSpark(r.p.snapshots,r.c.color)}</div>
|
||
</div>`).join("");
|
||
document.querySelectorAll("#leaderboard .lb-card").forEach(el=>{
|
||
el.addEventListener("click",()=>{localStorage.setItem(VIEW_KEY,el.dataset.agent);renderPortfolioTab();showTab("portfolio");});
|
||
});
|
||
drawCombo("chart2","comboLegend2");
|
||
}
|
||
const CHART_RANGES=[
|
||
{id:"1d",label:"1D",days:1},
|
||
{id:"1w",label:"1W",days:7},
|
||
{id:"1m",label:"1M",days:30},
|
||
{id:"3m",label:"3M",days:90},
|
||
{id:"6m",label:"6M",days:180},
|
||
{id:"1y",label:"1Y",days:365},
|
||
{id:"5y",label:"5Y",days:1825},
|
||
{id:"all",label:"ALL",days:null},
|
||
];
|
||
function chartRange(){const r=localStorage.getItem(CHART_RANGE_KEY)||"1w";return CHART_RANGES.some(x=>x.id===r)?r:"1w";}
|
||
function zoomChartRange(direction){
|
||
const idx=CHART_RANGES.findIndex(r=>r.id===chartRange());
|
||
const next=clamp(idx+direction,0,CHART_RANGES.length-1);
|
||
localStorage.setItem(CHART_RANGE_KEY,CHART_RANGES[next].id);
|
||
renderAll();
|
||
}
|
||
function renderChartRanges(){
|
||
const active=chartRange();
|
||
document.querySelectorAll("[data-chart-ranges]").forEach(root=>{
|
||
root.innerHTML=`<button class="rangebtn" data-chart-zoom="-1" type="button" title="Zoom in">-</button>`+
|
||
CHART_RANGES.map(r=>`<button class="rangebtn ${r.id===active?"active":""}" data-range="${r.id}" type="button">${r.label}</button>`).join("")+
|
||
`<button class="rangebtn" data-chart-zoom="1" type="button" title="Zoom out">+</button>`;
|
||
});
|
||
document.querySelectorAll(".rangebtn").forEach(btn=>btn.onclick=()=>{localStorage.setItem(CHART_RANGE_KEY,btn.dataset.range);renderAll();});
|
||
document.querySelectorAll("[data-chart-zoom]").forEach(btn=>btn.onclick=()=>zoomChartRange(Number(btn.dataset.chartZoom||0)));
|
||
}
|
||
const snapTime=(s)=>new Date(s.timestamp||`${s.date}T12:00:00Z`).getTime();
|
||
function chartDateLabel(t,compact=false){
|
||
const d=new Date(t);
|
||
if(isNaN(d))return "";
|
||
const sameYear=d.getUTCFullYear()===new Date().getUTCFullYear();
|
||
return d.toLocaleDateString("en-US",compact
|
||
? {month:"short",day:"numeric"}
|
||
: (sameYear?{month:"short",day:"numeric"}:{month:"short",day:"numeric",year:"2-digit"}));
|
||
}
|
||
function chartTicks(times,maxTicks=5){
|
||
if(!times.length)return [];
|
||
if(times.length===1)return [{i:0,t:times[0]}];
|
||
const count=Math.min(maxTicks,times.length),seen=new Set(),ticks=[];
|
||
for(let n=0;n<count;n++){
|
||
const i=Math.round(n*(times.length-1)/(count-1));
|
||
if(!seen.has(i)){seen.add(i);ticks.push({i,t:times[i]});}
|
||
}
|
||
return ticks;
|
||
}
|
||
function niceChartBounds(vals){
|
||
let min=Math.min(...vals,0),max=Math.max(...vals,0);
|
||
if(min===max){min-=1;max+=1;}
|
||
const span=max-min,pad=Math.max(0.5,span*0.12);
|
||
min-=pad;max+=pad;
|
||
const step=Math.max(0.5,Math.pow(10,Math.floor(Math.log10(max-min)))/2);
|
||
min=Math.floor(min/step)*step;max=Math.ceil(max/step)*step;
|
||
return {min,max};
|
||
}
|
||
function smoothPath(points){
|
||
if(points.length<2)return points.map((p,i)=>`${i?"L":"M"}${p.x},${p.y}`).join(" ");
|
||
let d=`M${points[0].x},${points[0].y}`;
|
||
for(let i=1;i<points.length;i++){
|
||
const prev=points[i-1],cur=points[i];
|
||
const midX=((+prev.x)+(+cur.x))/2;
|
||
d+=` C${midX.toFixed(1)},${prev.y} ${midX.toFixed(1)},${cur.y} ${cur.x},${cur.y}`;
|
||
}
|
||
return d;
|
||
}
|
||
function rangeFilteredTimes(times){
|
||
const range=CHART_RANGES.find(r=>r.id===chartRange())||CHART_RANGES[0];
|
||
if(!range.days)return times;
|
||
const newest=Math.max(...times);
|
||
const cutoff=newest-range.days*86400000;
|
||
const filtered=times.filter(t=>t>=cutoff);
|
||
return filtered.length?filtered:times.slice(-1);
|
||
}
|
||
function drawCombo(svgId,legendId){
|
||
const st=loadState();
|
||
renderChartRanges();
|
||
const series=AGENTS.map(c=>({c,snaps:((st.agents[c.id]&&st.agents[c.id].snapshots)||[])
|
||
.filter(s=>Number.isFinite(snapTime(s))).sort((a,b)=>snapTime(a)-snapTime(b))}));
|
||
const allTimes=rangeFilteredTimes([...new Set(series.flatMap(s=>s.snaps.map(snapTime)))].sort((a,b)=>a-b));
|
||
const svg=$(svgId); if(!svg)return;
|
||
if(!allTimes.length){svg.innerHTML="";if($(legendId))$(legendId).innerHTML=`<span class="muted small">No history yet — click "Run cycle".</span>`;return;}
|
||
const W=960,H=320,padL=72,padR=36,padT=36,padB=54; const vals=[0];
|
||
const lines=series.map(s=>{
|
||
let last=0,idx=0;
|
||
const pts=allTimes.map(t=>{
|
||
while(idx<s.snaps.length&&snapTime(s.snaps[idx])<=t){
|
||
last=Number.isFinite(s.snaps[idx].return_pct)?s.snaps[idx].return_pct:((s.snaps[idx].equity||STARTING_BALANCE)/STARTING_BALANCE-1)*100;
|
||
idx++;
|
||
}
|
||
vals.push(last);return last;
|
||
});
|
||
return {c:s.c,pts};
|
||
});
|
||
const avgPts=allTimes.map((_,i)=>+(lines.reduce((sum,l)=>sum+l.pts[i],0)/Math.max(1,lines.length)).toFixed(2));
|
||
vals.push(...avgPts);
|
||
const bounds=niceChartBounds(vals),min=bounds.min,max=bounds.max,span=(max-min)||1;
|
||
const x=i=>allTimes.length===1?(padL+(W-padR))/2:padL+(i/(allTimes.length-1))*(W-padL-padR);
|
||
const y=v=>H-padB-((v-min)/span)*(H-padT-padB);
|
||
const yTicks=Array.from({length:5},(_,i)=>+(min+(span*i/4)).toFixed(1));
|
||
const zeroY=y(0).toFixed(1);
|
||
let inner=`<defs>
|
||
<linearGradient id="${svgId}-panel" x1="0" x2="0" y1="0" y2="1">
|
||
<stop offset="0%" stop-color="rgba(255,255,255,.055)"/><stop offset="100%" stop-color="rgba(255,255,255,.018)"/>
|
||
</linearGradient>
|
||
<filter id="${svgId}-soft" x="-20%" y="-20%" width="140%" height="140%">
|
||
<feDropShadow dx="0" dy="4" stdDeviation="5" flood-color="rgba(0,0,0,.22)"/>
|
||
</filter>
|
||
</defs>`;
|
||
inner+=`<rect x="${padL}" y="${padT}" width="${W-padL-padR}" height="${H-padT-padB}" rx="12" fill="url(#${svgId}-panel)" stroke="rgba(255,255,255,.08)"/>`;
|
||
for(const v of yTicks){
|
||
const yy=y(v).toFixed(1);
|
||
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${yy}" y2="${yy}" stroke="rgba(255,255,255,.075)" stroke-width="1"/>`;
|
||
inner+=`<text x="${padL-12}" y="${(+yy+4).toFixed(1)}" text-anchor="end" fill="rgba(224,234,255,.62)" font-size="11" font-family="Inter, sans-serif">${fmtPct(v)}</text>`;
|
||
}
|
||
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${zeroY}" y2="${zeroY}" stroke="rgba(255,255,255,.28)" stroke-width="1.2"/>`;
|
||
for(const tick of chartTicks(allTimes,5)){
|
||
const xx=x(tick.i).toFixed(1);
|
||
inner+=`<line x1="${xx}" x2="${xx}" y1="${padT}" y2="${H-padB}" stroke="rgba(255,255,255,.055)" stroke-width="1"/>`;
|
||
inner+=`<text x="${xx}" y="${H-18}" text-anchor="middle" fill="rgba(224,234,255,.72)" font-size="11" font-family="Inter, sans-serif">${chartDateLabel(tick.t,true)}</text>`;
|
||
}
|
||
for(const ln of lines){
|
||
const points=ln.pts.map((v,i)=>({x:x(i).toFixed(1),y:y(v).toFixed(1)}));
|
||
const d=smoothPath(points);
|
||
const last=points[points.length-1];
|
||
inner+=`<path d="${d}" fill="none" stroke="${ln.c.color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity=".92" filter="url(#${svgId}-soft)"/>`;
|
||
inner+=`<circle cx="${last.x}" cy="${last.y}" r="4.2" fill="${ln.c.color}" stroke="#101827" stroke-width="2"/>`;
|
||
}
|
||
const avgPoints=avgPts.map((v,i)=>({x:x(i).toFixed(1),y:y(v).toFixed(1)}));
|
||
inner+=`<path d="${smoothPath(avgPoints)}" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-dasharray="8 7" stroke-linecap="round" opacity=".82"/>`;
|
||
inner+=`<text x="${padL}" y="21" fill="rgba(224,234,255,.64)" font-size="11.5" font-family="Inter, sans-serif">Return %</text>`;
|
||
inner+=`<text x="${W-padR}" y="21" text-anchor="end" fill="rgba(224,234,255,.58)" font-size="11.5" font-family="Inter, sans-serif">${chartDateLabel(allTimes[0])} - ${chartDateLabel(allTimes[allTimes.length-1])}</text>`;
|
||
svg.innerHTML=inner;
|
||
if($(legendId)){
|
||
const b=board();const avg=b.reduce((s,r)=>s+r.ret,0)/b.length;
|
||
const core=b.filter(r=>r.c.kind==="strategy"),coreAvg=core.reduce((s,r)=>s+r.ret,0)/Math.max(1,core.length);
|
||
$(legendId).innerHTML=`<span class="leg"><span class="legdot" style="background:#fff"></span>All Avg <b class="${signClass(avg)}">${fmtPct(avg)}</b></span>`+
|
||
`<span class="leg"><span class="legdot" style="background:#38d2e6"></span>Core Avg <b class="${signClass(coreAvg)}">${fmtPct(coreAvg)}</b></span>`+
|
||
b.map(r=>`<span class="leg"><span class="legdot" style="background:${r.c.color}"></span>${r.c.emoji} ${r.c.name} <b class="${signClass(r.pnl)}">${fmtPct(r.ret)}</b></span>`).join("");
|
||
}
|
||
}
|
||
function renderFilterBar(all){
|
||
const focus=getFocus();
|
||
const counts={};CATEGORIES.forEach(c=>counts[c]=0);
|
||
all.forEach(s=>{const c=counts[s.category]!==undefined?s.category:"Other";counts[c]++;});
|
||
counts["All"]=all.length;
|
||
$("filterbar").innerHTML=CATEGORIES.map(c=>{const n=counts[c]||0;const dim=(c!=="All"&&n===0)?"opacity:.4;":"";
|
||
return `<span class="chip ${c===focus?"active":""}" data-cat="${c}" style="${dim}">${c}<span class="ct">${n}</span></span>`;}).join("");
|
||
document.querySelectorAll("#filterbar .chip").forEach(el=>el.addEventListener("click",()=>{setFocus(el.dataset.cat);renderSuggestions();}));
|
||
}
|
||
function renderSuggestions(){
|
||
const data=loadSuggestions();
|
||
$("sugDate").textContent=data.date?`· ${data.date}`:"";
|
||
const all=data.suggestions||[];const root=$("suggestions");
|
||
renderFilterBar(all);
|
||
if(!all.length){root.innerHTML=`<div class="empty" style="grid-column:1/-1">No suggestions yet. Click "Run cycle".</div>`;$("focusNote").textContent="";return;}
|
||
const focus=getFocus();
|
||
const pool=(focus==="All"?all:all.filter(s=>s.category===focus));
|
||
const filtered=pool.slice(0,VISIBLE_SUGGESTIONS);
|
||
const buyCount=all.filter(s=>s.trade_ready).length,watchCount=all.length-buyCount;
|
||
const evAvg=all.length?Math.round(all.reduce((s,x)=>s+Number(x.evidence_score||0),0)/all.length*100):0;
|
||
const scanned=Number(data.market_count||0).toLocaleString();
|
||
const analyzed=Number(data.analyzed_count||data.market_count||0).toLocaleString();
|
||
$("focusNote").textContent=focus==="All"
|
||
? `Scanned ${scanned} active markets, analyzed the top ${analyzed} by activity/liquidity/gap, and kept ${all.length} ideas (${buyCount} BUY, ${watchCount} WATCH). Showing ${filtered.length}; agents trade only EV+ ideas after liquidity, chase, and real-world evidence checks. Avg evidence ${evAvg}.`
|
||
: `Scanned ${scanned} active markets and analyzed the top ${analyzed}. Showing ${filtered.length} of ${pool.length} ${focus} ideas; agents only open EV+ BUY ${focus} positions while this is selected.`;
|
||
if(!filtered.length){root.innerHTML=`<div class="empty" style="grid-column:1/-1">No ${esc(focus)} suggestions in this live batch.</div>`;return;}
|
||
root.innerHTML=filtered.map(s=>{
|
||
const col=catColor(s.category);
|
||
const drivers=s.drivers.map(d=>`<span class="driver">${d}</span>`).join("");
|
||
const days=s.days_to_resolution!=null?`${s.days_to_resolution}d`:"—";
|
||
return `<div class="sug" style="border-left-color:${col}">
|
||
<div class="sug-row" style="margin-bottom:2px"><span class="cat-badge" style="background:${col}22;color:${col}">${esc(s.category||"Other")}</span></div>
|
||
<div class="q">${s.url?`<a class="market-title" href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.question)}</a>`:esc(s.question)}</div>
|
||
<div class="event">${esc(s.event||"")}</div>
|
||
<div class="sug-row">
|
||
<span class="pill ${s.side}">${s.trade_ready?"BUY":"WATCH"} ${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>Net edge <b>${((Math.abs(s.net_edge!=null?s.net_edge:s.edge))*100).toFixed(1)}c</b></span><span>Evidence <b>${Math.round((s.evidence_score||0)*100)}</b></span>
|
||
<span>Vol <b>${fmtUSD(s.volume)}</b></span><span>24h <b>${fmtUSD(s.volume_24hr)}</b></span><span>Resolves <b>${days}</b></span>
|
||
${s.url?`<a class="market-link" href="${esc(s.url)}" target="_blank" rel="noopener">Open on Polymarket ↗</a>`:""}
|
||
${PERSONAL_MODE?`<button class="btn ghost" data-stage-suggestion="${esc(s.market_id)}">Stage ticket</button>`:""}
|
||
</div></div>`;
|
||
}).join("");
|
||
document.querySelectorAll("[data-stage-suggestion]").forEach(btn=>btn.onclick=()=>stageSuggestionTicket(btn.dataset.stageSuggestion));
|
||
}
|
||
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());
|
||
renderAgentTechnical(viewId);
|
||
renderAgentReports();
|
||
renderAgentChat(viewId);
|
||
}
|
||
const avg=(arr)=>arr.length?arr.reduce((s,x)=>s+x,0)/arr.length:0;
|
||
function ema(vals,period){
|
||
const k=2/(period+1),out=[];let prev=vals[0]||0;
|
||
vals.forEach((v,i)=>{prev=i?((v*k)+(prev*(1-k))):v;out.push(prev);});
|
||
return out;
|
||
}
|
||
function renderAgentTechnical(agentId){
|
||
const svg=$("agentTAChart"),readout=$("agentTAReadout");if(!svg||!readout)return;
|
||
const st=loadState(),cfg=agentById(agentId),p=st.agents[agentId]||defaultPortfolio();
|
||
const snaps=(p.snapshots||[]).filter(s=>Number.isFinite(snapTime(s))).sort((a,b)=>snapTime(a)-snapTime(b)).slice(-60);
|
||
if(snaps.length<2){svg.innerHTML="";readout.innerHTML=`<div class="empty">Need at least two return snapshots for technical analysis.</div>`;return;}
|
||
const closes=snaps.map(s=>s.equity||STARTING_BALANCE),times=snaps.map(snapTime),rets=snaps.map(s=>s.return_pct||0);
|
||
const ma5=closes.map((_,i)=>avg(closes.slice(Math.max(0,i-4),i+1)));
|
||
const ma10=closes.map((_,i)=>avg(closes.slice(Math.max(0,i-9),i+1)));
|
||
const macd=ema(closes,6).map((v,i)=>v-ema(closes,13)[i]),signal=ema(macd,5),hist=macd.map((v,i)=>v-signal[i]);
|
||
const W=960,H=420,padL=64,padR=26,padT=28,padB=40,priceH=250,macdTop=300;
|
||
const vals=closes.concat(ma5,ma10),mn=Math.min(...vals),mx=Math.max(...vals),span=(mx-mn)||1;
|
||
const x=i=>padL+(i/(snaps.length-1))*(W-padL-padR),y=v=>padT+(mx-v)/span*(priceH-padT);
|
||
const mMax=Math.max(...hist.map(Math.abs),1),my=v=>macdTop+48-(v/mMax)*42;
|
||
let inner=`<rect x="${padL}" y="${padT}" width="${W-padL-padR}" height="${priceH-padT}" rx="12" fill="rgba(255,255,255,.025)" stroke="rgba(255,255,255,.08)"/>`;
|
||
for(let i=0;i<5;i++){const v=mn+(span*i/4),yy=y(v);inner+=`<line x1="${padL}" x2="${W-padR}" y1="${yy}" y2="${yy}" stroke="rgba(255,255,255,.06)"/><text x="${padL-10}" y="${yy+4}" text-anchor="end" fill="rgba(224,234,255,.62)" font-size="11">${fmtUSD(v)}</text>`;}
|
||
const w=Math.max(3,(W-padL-padR)/snaps.length*.48);
|
||
closes.forEach((c,i)=>{const o=i?closes[i-1]:c,up=c>=o,col=up?"#7c8cff":"#fb7185";const xx=x(i);
|
||
inner+=`<rect x="${xx-w/2}" y="${y(Math.max(o,c))}" width="${w}" height="${Math.max(2,Math.abs(y(c)-y(o)))}" rx="1.5" fill="${col}" opacity=".68"/>`;});
|
||
const path=(arr)=>arr.map((v,i)=>`${i?"L":"M"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
||
inner+=`<path d="${path(ma5)}" fill="none" stroke="#fbbf24" stroke-width="2.4"/><path d="${path(ma10)}" fill="none" stroke="#38d2e6" stroke-width="2.4"/>`;
|
||
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${my(0)}" y2="${my(0)}" stroke="rgba(255,255,255,.18)"/>`;
|
||
hist.forEach((v,i)=>{const xx=x(i),yy=my(Math.max(0,v)),hh=Math.abs(my(v)-my(0));inner+=`<rect x="${xx-w/2}" y="${v>=0?yy:my(0)}" width="${w}" height="${Math.max(1,hh)}" fill="${v>=0?"#94a3b8":"#fb7185"}" opacity=".55"/>`;});
|
||
for(const tick of chartTicks(times,4)){inner+=`<text x="${x(tick.i)}" y="${H-14}" text-anchor="middle" fill="rgba(224,234,255,.7)" font-size="11">${chartDateLabel(tick.t,true)}</text>`;}
|
||
inner+=`<text x="${padL}" y="18" fill="rgba(224,234,255,.7)" font-size="12">${cfg.name} equity candles · MA5 yellow · MA10 cyan · neutral MACD bars</text>`;
|
||
svg.innerHTML=inner;
|
||
const last=closes.length-1,trend=ma5[last]>=ma10[last]?"bullish":"bearish",mom=hist[last]>=0?"positive":"negative";
|
||
readout.innerHTML=`<div class="invest-num"><div class="k">Trend</div><div class="v">${trend}</div></div>
|
||
<div class="invest-num" style="margin-top:10px"><div class="k">MACD</div><div class="v">${mom}</div></div>
|
||
<p class="muted">Latest return ${fmtPct(rets[last])}. MA5 is ${fmtUSD(ma5[last])}; MA10 is ${fmtUSD(ma10[last])}. Activity proxy: ${snaps[last].open_positions||0} open positions.</p>`;
|
||
}
|
||
function agentCompetitionPlan(cfg,row,rank,leader){
|
||
if(rank===1)return "Plan: defend the lead, keep sizing disciplined, and let risk controls protect gains instead of forcing new trades.";
|
||
if(cfg.kind==="copycat")return `Plan: keep shadowing the strongest MACD/momentum setup, then rotate automatically if another in-house strategy shows better forward pressure.`;
|
||
if(cfg.kind==="whale")return "Plan: stay close to the copied trader's largest live positions while using the stop-loss and gain-stop rules to avoid blindly riding every swing.";
|
||
if(cfg.id==="value")return "Plan: close the gap by waiting for the cleanest model-versus-market mispricings and avoiding crowded trades without enough edge.";
|
||
if(cfg.id==="momentum")return "Plan: attack fast-moving markets where fresh volume confirms attention, hoping speed beats slower value strategies.";
|
||
if(cfg.id==="favorite")return "Plan: grind upward through high-probability favorites, aiming to outlast more volatile agents during choppy markets.";
|
||
if(cfg.id==="longshot")return "Plan: keep risk small but search for one underpriced outsider that can reprice sharply and leapfrog the leaderboard.";
|
||
if(cfg.id==="diversifier")return "Plan: spread bets broadly, reduce single-market damage, and try to win through consistency rather than one heroic call.";
|
||
return "Plan: keep following its rulebook and use fresh market data to pressure the leaderboard.";
|
||
}
|
||
function recentActionSummary(p){
|
||
const recent=(p.history||[]).slice(-8);
|
||
const counts={STOP:0,GAIN:0,EXIT:0,CLOSE:0,OPEN:0};
|
||
recent.forEach(h=>{if(counts[h.action]!==undefined)counts[h.action]++;});
|
||
const bits=[];
|
||
if(counts.STOP)bits.push(`${counts.STOP} stop-loss sale${counts.STOP===1?"":"s"}`);
|
||
if(counts.GAIN)bits.push(`${counts.GAIN} gain-stop cash-in${counts.GAIN===1?"":"s"}`);
|
||
if(counts.EXIT||counts.CLOSE)bits.push(`${counts.EXIT+counts.CLOSE} exit${counts.EXIT+counts.CLOSE===1?"":"s"}`);
|
||
if(counts.OPEN)bits.push(`${counts.OPEN} new entr${counts.OPEN===1?"y":"ies"}`);
|
||
return bits.length?bits.join(", "):"no major recent trade actions";
|
||
}
|
||
function snapshotNearDayAgo(snaps,lastTime){
|
||
const cutoff=lastTime-86400000;
|
||
const before=snaps.filter(s=>snapTime(s)<=cutoff).pop();
|
||
if(before)return before;
|
||
return snaps.length>1?snaps[0]:null;
|
||
}
|
||
function dailyActionSummary(p,lastTime){
|
||
const cutoff=lastTime-86400000;
|
||
const counts={STOP:0,GAIN:0,EXIT:0,CLOSE:0,OPEN:0};
|
||
(p.history||[]).forEach(h=>{
|
||
const t=h.date?new Date(`${h.date}T12:00:00`).getTime():NaN;
|
||
if(Number.isFinite(t)&&t>=cutoff&&counts[h.action]!==undefined)counts[h.action]++;
|
||
});
|
||
const bits=[];
|
||
if(counts.OPEN)bits.push(`${counts.OPEN} opened`);
|
||
if(counts.GAIN)bits.push(`${counts.GAIN} gain-stop sale${counts.GAIN===1?"":"s"}`);
|
||
if(counts.STOP)bits.push(`${counts.STOP} stop-loss sale${counts.STOP===1?"":"s"}`);
|
||
if(counts.EXIT||counts.CLOSE)bits.push(`${counts.EXIT+counts.CLOSE} exit${counts.EXIT+counts.CLOSE===1?"":"s"}`);
|
||
return bits.length?bits.join(", "):recentActionSummary(p);
|
||
}
|
||
function recentClosedAttribution(p,lastTime){
|
||
const cutoff=lastTime-86400000;
|
||
return (p.closed||[]).filter(pos=>{
|
||
const t=pos.closed_at?new Date(pos.closed_at).getTime():NaN;
|
||
return Number.isFinite(t)&&t>=cutoff;
|
||
}).sort((a,b)=>Math.abs(b.realized_pnl||0)-Math.abs(a.realized_pnl||0));
|
||
}
|
||
function shortQuestion(q,len=48){
|
||
q=String(q||"");
|
||
return `${q.slice(0,len)}${q.length>len?"...":""}`;
|
||
}
|
||
function reportPositionLine(p){
|
||
if(!p.positions.length)return "No open positions yet, so this hour's focus is finding entries that fit the strategy.";
|
||
const best=[...p.positions].sort((a,b)=>(b.unrealized_pnl||0)-(a.unrealized_pnl||0))[0];
|
||
const worst=[...p.positions].sort((a,b)=>(a.unrealized_pnl||0)-(b.unrealized_pnl||0))[0];
|
||
if(best===worst)return `Current focus: ${best.question.slice(0,58)}${best.question.length>58?"...":""}, marked ${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}.`;
|
||
return `Best mark: ${best.question.slice(0,42)}${best.question.length>42?"...":""} (${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}). Watch item: ${worst.question.slice(0,42)}${worst.question.length>42?"...":""} (${fmtUSD(worst.unrealized_pnl||0)}).`;
|
||
}
|
||
function attributionLine(pos){
|
||
if(!pos)return "No single trade explains the move.";
|
||
const bits=[];
|
||
if(pos.entry_reason)bits.push(pos.entry_reason);
|
||
if(pos.net_edge!=null)bits.push(`entry net edge ${((Math.abs(pos.net_edge))*100).toFixed(1)}c`);
|
||
if(pos.evidence_score!=null)bits.push(`evidence ${Math.round(Number(pos.evidence_score||0)*100)}`);
|
||
if(pos.friction!=null)bits.push(`friction ${((Number(pos.friction||0))*100).toFixed(1)}c`);
|
||
return bits.length?bits.join(" · "):"This older position was opened before detailed attribution was stored.";
|
||
}
|
||
function dailyPerformanceExplanation(row){
|
||
const snaps=(row.p.snapshots||[]).slice().sort((a,b)=>snapTime(a)-snapTime(b));
|
||
const last=snaps[snaps.length-1];
|
||
if(!last)return "Why today is unclear: the agent needs at least one return snapshot before it can explain performance.";
|
||
const lastTime=snapTime(last);
|
||
const prev=snapshotNearDayAgo(snaps,lastTime);
|
||
if(!prev)return "Why today is still forming: there is not enough 24-hour history yet, so the report is waiting for more snapshots.";
|
||
const delta=(last.return_pct||0)-(prev.return_pct||0);
|
||
const equityDelta=(last.equity||0)-(prev.equity||0);
|
||
const open=row.p.positions||[];
|
||
const best=open.slice().sort((a,b)=>(b.unrealized_pnl||0)-(a.unrealized_pnl||0))[0];
|
||
const worst=open.slice().sort((a,b)=>(a.unrealized_pnl||0)-(b.unrealized_pnl||0))[0];
|
||
const recentClosed=recentClosedAttribution(row.p,lastTime);
|
||
const mostImportantClose=recentClosed[0];
|
||
const realizedToday=recentClosed.reduce((s,p)=>s+Number(p.realized_pnl||0),0);
|
||
const m=agentMomentumStats(row.p);
|
||
const macdText=`MACD ${m.macd>=0?"+":""}${m.macd.toFixed(2)}, momentum ${m.recent>=0?"+":""}${m.recent.toFixed(2)} pts`;
|
||
const actions=dailyActionSummary(row.p,lastTime);
|
||
const closedText=mostImportantClose
|
||
? `The largest realized driver was '${shortQuestion(mostImportantClose.question)}' at ${fmtUSD(mostImportantClose.realized_pnl||0)}; total realized P&L over the past day was ${fmtUSD(realizedToday)}.`
|
||
: "There were no major realized closes in the past day.";
|
||
if(delta>0.25){
|
||
const openText=best?`The strongest open position helping it is '${shortQuestion(best.question)}' at ${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}.`:"It benefited mostly from cash discipline and closed-trade results rather than a single open winner.";
|
||
return `Past 24h: up ${delta.toFixed(2)} return points (${fmtUSD(equityDelta)}). ${openText} Attribution: ${attributionLine(best)} ${closedText} Trade behavior: ${actions}. Technical pressure: ${macdText}.`;
|
||
}
|
||
if(delta<-0.25){
|
||
const openText=worst?`The biggest open drag is '${shortQuestion(worst.question)}' at ${fmtUSD(worst.unrealized_pnl||0)}.`:"The decline came more from broad mark-downs or realized losses than one obvious open position.";
|
||
return `Past 24h: down ${Math.abs(delta).toFixed(2)} return points (${fmtUSD(equityDelta)}). ${openText} Attribution: ${attributionLine(worst)} ${closedText} Trade behavior: ${actions}. Technical pressure: ${macdText}.`;
|
||
}
|
||
const openText=best&&worst&&best!==worst
|
||
? `Best open mark is '${shortQuestion(best.question,36)}' (${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}) while the weakest is '${shortQuestion(worst.question,36)}' (${fmtUSD(worst.unrealized_pnl||0)}).`
|
||
: "Open positions mostly offset each other or the agent is carrying limited exposure.";
|
||
return `Past 24h: nearly flat at ${delta>=0?"+":""}${delta.toFixed(2)} return points (${fmtUSD(equityDelta)}). ${openText} ${closedText} Trade behavior: ${actions}. Technical pressure: ${macdText}.`;
|
||
}
|
||
function renderAgentReports(){
|
||
const root=$("agentReports"); if(!root)return;
|
||
const b=board(); const st=loadState(); const leader=b[0];
|
||
if($("reportDate"))$("reportDate").textContent=`${st.last_run?new Date(st.last_run).toLocaleString():todayStr()}`;
|
||
root.innerHTML=b.map((row,i)=>{
|
||
const rank=i+1, ret=fmtPct(row.ret), open=row.p.positions.length;
|
||
const thesis=agentPlainBlurb(row.c,st);
|
||
const plan=agentCompetitionPlan(row.c,row,rank,leader);
|
||
const line=reportPositionLine(row.p);
|
||
const crash=row.ret<-18?`<p><b>Drawdown guard:</b> This account is down more than 18%, so new trades are throttled to one high-conviction entry with extra cash reserve. It is not trying to force a comeback.</p>`:"";
|
||
return `<article class="report" style="border-top-color:${row.c.color}">
|
||
<div class="report-title"><div class="report-name">${row.c.emoji} ${row.c.name}</div><div class="report-rank">#${rank} · ${ret}</div></div>
|
||
<p><b>Strategy:</b> ${esc(thesis)}</p>
|
||
<p><b>Adaptation:</b> ${esc(decisionSummary(row.p))}</p>
|
||
<p><b>Performance:</b> ${esc(dailyPerformanceExplanation(row))}</p>
|
||
${crash}
|
||
<p><b>Status:</b> ${open} open position${open===1?"":"s"} with ${fmtUSD(row.eq)} equity. ${esc(line)}</p>
|
||
<p><b>${esc(plan)}</b></p>
|
||
</article>`;
|
||
}).join("");
|
||
}
|
||
function loadAgentChats(){try{return JSON.parse(localStorage.getItem(CHAT_KEY)||"{}");}catch(e){return {};}}
|
||
function saveAgentChats(chats){localStorage.setItem(CHAT_KEY,JSON.stringify(chats));}
|
||
function agentVoice(cfg){
|
||
const voices={
|
||
value:{tone:"patient and valuation-obsessed",hello:"I am looking for mispriced contracts, not noise.",signoff:"I would rather be late than pay the wrong price."},
|
||
momentum:{tone:"fast, direct, and volume-aware",hello:"I am watching where attention is accelerating.",signoff:"If the tape cools off, I cool off too."},
|
||
favorite:{tone:"steady and probability-first",hello:"I like high-probability edges with less drama.",signoff:"My job is compounding, not showing off."},
|
||
longshot:{tone:"opportunistic but risk-aware",hello:"I hunt asymmetric mispricing, but I keep the bet size honest.",signoff:"One repricing can matter, but only if I survive the misses."},
|
||
diversifier:{tone:"calm, portfolio-minded, and broad",hello:"I care about the whole basket more than one perfect call.",signoff:"Many small edges can beat one loud opinion."},
|
||
copycat:{tone:"adaptive and comparative",hello:"I am watching which strategy has the strongest forward pressure.",signoff:"I rotate when the evidence rotates."},
|
||
whale1:{tone:"observant and mirror-focused",hello:"I am reading the footprint of a top public trader.",signoff:"I copy exposure, but I still respect the arena risk rules."},
|
||
whale2:{tone:"measured and copy-trader-like",hello:"I am tracking a different profitable wallet for contrast.",signoff:"The point is to learn which real-world style travels well."},
|
||
whale3:{tone:"sharp and signal-driven",hello:"I am looking for signal in where strong traders concentrate size.",signoff:"A copied trade still has to survive mark-to-market."},
|
||
whale4:{tone:"flexible and market-current-aware",hello:"I ride the copied flow, but I do not pretend it is magic.",signoff:"The market gets the last word."},
|
||
};
|
||
return voices[cfg.id]||{tone:"analytical",hello:"I am reading the current book.",signoff:"Ask me where you want to go deeper."};
|
||
}
|
||
function agentChatSeed(agentId){
|
||
const cfg=agentById(agentId),st=loadState(),p=st.agents[agentId]||defaultPortfolio(),eq=equity(p),ret=(eq/p.starting_balance-1)*100;
|
||
const voice=agentVoice(cfg);
|
||
return [{role:"agent",text:`${voice.hello} I am ${cfg.name}, sitting at ${fmtPct(ret)} with ${fmtUSD(eq)} equity. Talk to me normally: ask what I am thinking, where I am exposed, what I regret, or what I would do next.`}];
|
||
}
|
||
function topPositionText(p,n=3){
|
||
const open=(p.positions||[]).slice().sort((a,b)=>Math.abs(b.unrealized_pnl||0)-Math.abs(a.unrealized_pnl||0)).slice(0,n);
|
||
if(!open.length)return "I do not have open positions yet.";
|
||
return open.map((pos,i)=>`${i+1}. ${shortQuestion(pos.question,58)} — ${pos.side}, value ${fmtUSD(pos.value||0)}, P&L ${(pos.unrealized_pnl||0)>=0?"+":""}${fmtUSD(pos.unrealized_pnl||0)}`).join("\n");
|
||
}
|
||
function relevantSuggestionText(cfg,n=3){
|
||
const sugs=(loadSuggestions().suggestions||[]);
|
||
const ranked=cfg.rank?safeRankSuggestions(cfg,sugs):sugs;
|
||
const picks=ranked.filter(s=>s.side==="YES"||s.side==="NO").slice(0,n);
|
||
if(!picks.length)return "I need a fresh cycle before I can point to new suggestions.";
|
||
return picks.map((s,i)=>`${i+1}. ${shortQuestion(s.question,58)} — BUY ${s.side} @ ${Math.round(s.entry_price*100)}c, conviction ${Math.round(s.conviction)}`).join("\n");
|
||
}
|
||
function safeRankSuggestions(cfg,sugs){
|
||
try{return cfg.rank(sugs);}catch(e){return sugs;}
|
||
}
|
||
function lastUserTopic(history){
|
||
const last=[...(history||[])].reverse().find(m=>m.role==="user"&&String(m.text||"").trim());
|
||
return last?String(last.text).toLowerCase():"";
|
||
}
|
||
function agentOneLineState(row,p){
|
||
const pnl=row.pnl>=0?`up ${fmtUSD(row.pnl)}`:`down ${fmtUSD(Math.abs(row.pnl))}`;
|
||
return `Right now I am #${row.rank||"?"}, ${pnl}, with ${p.positions.length} open position${p.positions.length===1?"":"s"}.`;
|
||
}
|
||
function chatMemory(history){
|
||
const users=(history||[]).filter(m=>m.role==="user").map(m=>String(m.text||"").trim()).filter(Boolean);
|
||
const last=users[users.length-1]||"";
|
||
const all=users.slice(-4).join(" ").toLowerCase();
|
||
return {last,lastLower:last.toLowerCase(),all};
|
||
}
|
||
function agentMood(row,p){
|
||
const actions=recentActionSummary(p);
|
||
if(row.ret>8)return `I am ahead, but I am trying not to confuse a hot streak with skill. Recent tape: ${actions}.`;
|
||
if(row.ret>0)return `I am modestly positive, so my job is to protect the progress and keep entries clean. Recent tape: ${actions}.`;
|
||
if(row.ret<-18)return `I am in damage-control mode. I should be slower, smaller, and pickier until the book stops leaking. Recent tape: ${actions}.`;
|
||
if(row.ret<-5)return `I am under pressure, so I am less interested in catching up fast and more interested in not making the next mistake. Recent tape: ${actions}.`;
|
||
return `I am close to flat, which means the next few entries matter more than the scoreboard noise. Recent tape: ${actions}.`;
|
||
}
|
||
function positionNarrative(p){
|
||
const open=(p.positions||[]).slice();
|
||
if(!open.length)return "I have no open positions yet, so the honest answer is that I am waiting for the cycle to find something worth owning.";
|
||
const sorted=open.sort((a,b)=>(b.unrealized_pnl||0)-(a.unrealized_pnl||0));
|
||
const best=sorted[0],worst=sorted[sorted.length-1];
|
||
const total=open.reduce((s,x)=>s+Number(x.unrealized_pnl||0),0);
|
||
const exposure=open.reduce((s,x)=>s+Number(x.value||0),0);
|
||
let text=`My open book has ${open.length} position${open.length===1?"":"s"}, about ${fmtUSD(exposure)} exposed, and ${fmtUSD(total)} in marked P&L.\n\n`;
|
||
text+=`Best helper: ${shortQuestion(best.question,70)} — ${best.side} at ${pct(best.current_price||best.entry_price||0)}, marked ${(best.unrealized_pnl||0)>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}.\n`;
|
||
if(best!==worst)text+=`Main problem: ${shortQuestion(worst.question,70)} — ${worst.side} at ${pct(worst.current_price||worst.entry_price||0)}, marked ${fmtUSD(worst.unrealized_pnl||0)}.`;
|
||
return text;
|
||
}
|
||
function suggestionNarrative(cfg,n=4){
|
||
const sugs=(loadSuggestions().suggestions||[]);
|
||
const ranked=cfg.rank?safeRankSuggestions(cfg,sugs):sugs;
|
||
const picks=ranked.filter(s=>s.side==="YES"||s.side==="NO").slice(0,n);
|
||
if(!picks.length)return "I do not have fresh actionable suggestions yet. Run a cycle and I will have more to chew on.";
|
||
return picks.map((s,i)=>`${i+1}. ${shortQuestion(s.question,76)}\n ${s.side} around ${pct(s.entry_price)} · conviction ${Math.round(s.conviction)} · edge ${(Number(s.edge||0)*100).toFixed(1)}c`).join("\n");
|
||
}
|
||
function regretNarrative(row,p){
|
||
const bw=bestAndWorst(p);
|
||
if(row.ret>=0)return `My main regret is not a disaster trade right now; it is staying humble. Best current helper: ${bw.best}. Weakest mark: ${bw.worst}. If I get sloppy after being up, that is usually where gains disappear.`;
|
||
return `My regret is that the weak side of the book is dragging harder than the strong side can help. Best current helper: ${bw.best}. Weakest mark: ${bw.worst}. From here I should demand stronger conviction before adding anything new.`;
|
||
}
|
||
function leaderboardNarrative(rows,row,rank){
|
||
const leader=rows[0];
|
||
const nearby=rows.slice(Math.max(0,rank-2),Math.min(rows.length,rank+2)).map((r,i)=>{
|
||
const actual=Math.max(1,rank-1)+i;
|
||
return `${actual}. ${r.c.name}: ${fmtPct(r.ret)} (${fmtUSD(r.eq)})`;
|
||
}).join("\n");
|
||
const gap=leader&&leader.c.id!==row.c.id?`The leader is ${leader.c.name}, and I am behind by ${fmtUSD(leader.eq-row.eq)}.`:"I am the current leader, so the risk is overtrading from first place.";
|
||
return `${gap}\n\nNearby board:\n${nearby}`;
|
||
}
|
||
function conversationalAgentReply(cfg,row,p,q){
|
||
const voice=agentVoice(cfg);
|
||
const samples=[
|
||
`${voice.hello} ${agentOneLineState(row,p)} ${agentMood(row,p)}`,
|
||
`My quick read: I am acting in a ${voice.tone} style. ${decisionSummary(p)} ${voice.signoff}`,
|
||
`${positionNarrative(p)}\n\nThat is the part of my book I would inspect first.`,
|
||
`I do not want to pretend certainty. The useful question is whether my next trade has enough edge to deserve risk. Right now my best style-fit ideas are:\n${suggestionNarrative(cfg,3)}`,
|
||
`${regretNarrative(row,p)}`
|
||
];
|
||
const idx=Math.abs((q||"").split("").reduce((s,ch)=>s+ch.charCodeAt(0),0))%samples.length;
|
||
return samples[idx];
|
||
}
|
||
function agentChatReply(agentId,question,history=[]){
|
||
const st=loadState(),cfg=agentById(agentId),p=st.agents[agentId]||defaultPortfolio();
|
||
const rows=board(),row=rows.find(r=>r.c.id===agentId)||{c:cfg,p,eq:equity(p),pnl:equity(p)-p.starting_balance,ret:(equity(p)/p.starting_balance-1)*100};
|
||
const rank=rows.findIndex(r=>r.c.id===agentId)+1||rows.length,leader=rows[0];
|
||
row.rank=rank;
|
||
const q=String(question||"").toLowerCase();
|
||
const mem=chatMemory(history);
|
||
const prior=mem.lastLower||lastUserTopic(history);
|
||
const combined=`${q} ${prior} ${mem.all}`;
|
||
const perf=dailyPerformanceExplanation(row);
|
||
if(/^(hi|hey|hello|yo|sup|what'?s up)\b/.test(q)){
|
||
const voice=agentVoice(cfg);
|
||
return `${voice.hello}\n\n${agentOneLineState(row,p)} ${agentMood(row,p)}\n\nAsk me like a normal trader: “what are you worried about,” “what trade hurt you,” “what would you avoid,” or “how do you catch the leader?”`;
|
||
}
|
||
if(/thank|thanks|cool|nice|great|got it/.test(q)){
|
||
return `Of course. Keep pressing me on the weak spots too; that is where the useful stuff usually is.`;
|
||
}
|
||
if(/feel|worried|confident|nervous|mad|happy|regret|annoy/.test(q)){
|
||
return `${agentMood(row,p)}\n\n${regretNarrative(row,p)}`;
|
||
}
|
||
if(/why|perform|performance|up|down|bad|poor|well|crash|gain|loss|today|day/.test(q)||(/^(why|how|what about|and that|what happened)\??$/.test(q)&&/perform|up|down|loss|gain|bad|well/.test(prior))){
|
||
return `Performance read:\n\n${perf}\n\nWhat I take from it: ${row.ret>=0?"my current exposures have helped, but I should not start forcing trades just because the chart looks better.":"I need cleaner entries, smaller mistakes, and patience. Chasing from a drawdown is exactly how a bad run becomes a terrible one."}`;
|
||
}
|
||
if(/position|trade|holding|holdings|bet|market/.test(combined)){
|
||
return `${positionNarrative(p)}\n\nShort version: ${reportPositionLine(p)}`;
|
||
}
|
||
if(/strategy|style|how do you trade|rule|approach/.test(combined)){
|
||
return `My style is ${agentVoice(cfg).tone}.\n\n${agentPlainBlurb(cfg,st)}\n\nCurrent adaptation: ${decisionSummary(p)}`;
|
||
}
|
||
if(/risk|stop|loss|gain|take profit|sell/.test(combined)){
|
||
return `My risk rules are mechanical:\n\nStop loss: sell the full remaining position at -18% from entry.\nGain stop: sell 25% at +75%, another 25% at +150%, and another 25% at +300%.\n\nRecent risk behavior: ${recentActionSummary(p)}.`;
|
||
}
|
||
if(/mistake|wrong|regret|fix|improve|better/.test(combined)){
|
||
return `${regretNarrative(row,p)}\n\nThe fix is not “bet harder.” The fix is stricter entries, fewer positions, and cutting trades when the model edge fades. That is the discipline I am trying to follow now.`;
|
||
}
|
||
if(/plan|next|compete|leader|rank|catch/.test(combined)){
|
||
return `${agentCompetitionPlan(cfg,row,rank,leader)}\n\nI am #${rank}. ${leader&&leader.c.id!==cfg.id?`${leader.c.name} leads at ${fmtUSD(leader.eq)}, so I am trailing by ${fmtUSD(leader.eq-row.eq)}.`:"I am currently defending the lead."}\n\nMy best near-term ideas from the current suggestion pool:\n${relevantSuggestionText(cfg)}`;
|
||
}
|
||
if(/suggest|idea|entry|buy/.test(combined)){
|
||
return `The freshest ideas that fit my style are:\n\n${suggestionNarrative(cfg,4)}\n\nI still need the normal cycle rules to decide whether sizing, overlap, cash reserve, and conviction are acceptable.`;
|
||
}
|
||
if(/compare|other agent|better|best/.test(combined)){
|
||
return `${leaderboardNarrative(rows,row,rank)}\n\nMy edge is not just rank; it is whether my style is working in the current market regime.`;
|
||
}
|
||
if(/explain|what do you mean|say more|more detail|why is that/.test(combined)){
|
||
return `Let me make it concrete.\n\n${positionNarrative(p)}\n\n${decisionSummary(p)}\n\nSo my next move should be based on evidence, not mood: stronger conviction, enough liquidity, and no oversized comeback bets.`;
|
||
}
|
||
return conversationalAgentReply(cfg,row,p,q);
|
||
}
|
||
function agentChatPayload(agentId,question,history=[]){
|
||
const st=loadState(),cfg=agentById(agentId),p=st.agents[agentId]||defaultPortfolio();
|
||
const rows=board(),row=rows.find(r=>r.c.id===agentId)||{c:cfg,p,eq:equity(p),pnl:equity(p)-p.starting_balance,ret:(equity(p)/p.starting_balance-1)*100};
|
||
const rank=(rows.findIndex(r=>r.c.id===agentId)+1)||rows.length;
|
||
const ranked=cfg.rank?safeRankSuggestions(cfg,(loadSuggestions().suggestions||[])):(loadSuggestions().suggestions||[]);
|
||
return {
|
||
agent:{id:cfg.id,name:cfg.name,kind:cfg.kind,blurb:agentPlainBlurb(cfg,st),voice:agentVoice(cfg).tone},
|
||
question,
|
||
history:(history||[]).slice(-12).filter(m=>!m.pending).map(m=>({role:m.role==="user"?"user":"assistant",text:m.text})),
|
||
portfolio:{
|
||
equity:+row.eq.toFixed(2),cash:+(p.cash||0).toFixed(2),return_pct:+row.ret.toFixed(2),pnl:+row.pnl.toFixed(2),
|
||
rank,open_positions:(p.positions||[]).length,last_decision:decisionSummary(p),
|
||
positions:(p.positions||[]).slice().sort((a,b)=>Math.abs(b.unrealized_pnl||0)-Math.abs(a.unrealized_pnl||0)).slice(0,10),
|
||
recent_actions:(p.history||[]).slice(-8).map(h=>h.detail||`${h.action||"ACTION"} ${h.question||""}`),
|
||
snapshots:(p.snapshots||[]).slice(-8),
|
||
},
|
||
leaderboard:rows.slice(0,10).map((r,i)=>({rank:i+1,name:r.c.name,return_pct:+r.ret.toFixed(2),equity:+r.eq.toFixed(2)})),
|
||
suggestions:ranked.filter(s=>s.side==="YES"||s.side==="NO").slice(0,8),
|
||
};
|
||
}
|
||
async function askAgentAI(agentId,question,history=[]){
|
||
if(localStorage.getItem(PAID_AGENT_CHAT_KEY)!=="1")throw new Error("free local agent chat is enabled");
|
||
const r=await fetch("/api/live",{
|
||
method:"POST",
|
||
headers:{"Content-Type":"application/json"},
|
||
body:JSON.stringify(Object.assign({action:"agent_chat"},agentChatPayload(agentId,question,history))),
|
||
});
|
||
const data=await r.json().catch(()=>({}));
|
||
if(!r.ok||!data.ok)throw new Error(data.error||"Agent AI chat is not available yet.");
|
||
return data.text||"I am here, but I do not have a strong answer from the current context.";
|
||
}
|
||
function renderAgentChat(agentId){
|
||
const root=$("agentChatRoot");if(!root)return;
|
||
const cfg=agentById(agentId),st=loadState(),p=st.agents[agentId]||defaultPortfolio(),eq=equity(p),ret=(eq/p.starting_balance-1)*100;
|
||
const chats=loadAgentChats();
|
||
if(!chats[agentId]){chats[agentId]=agentChatSeed(agentId);saveAgentChats(chats);}
|
||
const msgs=chats[agentId].slice(-30);
|
||
root.innerHTML=`<div class="agent-chat">
|
||
<div class="chat-panel">
|
||
<div class="chat-persona"><div class="chat-avatar" style="border-color:${cfg.color}55">${cfg.emoji}</div><div><div class="chat-name">${esc(cfg.name)}</div><div class="chat-meta">${fmtUSD(eq)} equity · ${fmtPct(ret)} return · ${p.positions.length} open</div></div></div>
|
||
<p class="muted" style="margin:0">${esc(agentPlainBlurb(cfg,st))}</p>
|
||
<div class="chat-prompts">
|
||
<button class="chat-prompt" data-chat-prompt="What's on your mind right now?">Talk</button>
|
||
<button class="chat-prompt" data-chat-prompt="Why did you perform well or poorly today?">Performance</button>
|
||
<button class="chat-prompt" data-chat-prompt="What positions are helping or hurting you most?">Positions</button>
|
||
<button class="chat-prompt" data-chat-prompt="What is your strategy?">Strategy</button>
|
||
<button class="chat-prompt" data-chat-prompt="What are you worried about?">Worries</button>
|
||
<button class="chat-prompt" data-chat-prompt="What is your plan to compete from here?">Plan</button>
|
||
<button class="chat-prompt" data-chat-prompt="Explain your risk rules and stop losses.">Risk</button>
|
||
</div>
|
||
</div>
|
||
<div class="chat-panel">
|
||
<div class="chat-window" id="agentChatWindow">${msgs.map(m=>`<div class="chat-msg ${m.role==="user"?"user":"agent"} ${m.pending?"pending":""}">${esc(m.text)}</div>`).join("")}</div>
|
||
<div class="chat-input-row"><input class="input" id="agentChatInput" placeholder="Ask ${esc(cfg.name)} about performance, trades, risk, or plans..." /><button class="btn primary" id="agentChatSend">Chat</button></div>
|
||
</div>
|
||
</div>`;
|
||
const send=async(text)=>{
|
||
const q=String(text||"").trim();if(!q)return;
|
||
const next=loadAgentChats();if(!next[agentId])next[agentId]=agentChatSeed(agentId);
|
||
const history=next[agentId].slice();
|
||
next[agentId].push({role:"user",text:q},{role:"agent",text:"Reading my portfolio...",pending:true});
|
||
next[agentId]=next[agentId].slice(-60);saveAgentChats(next);renderAgentChat(agentId);
|
||
let answer;
|
||
try{
|
||
answer=await askAgentAI(agentId,q,history);
|
||
}catch(e){
|
||
answer=agentChatReply(agentId,q,history);
|
||
}
|
||
const done=loadAgentChats();if(!done[agentId])done[agentId]=[];
|
||
const idx=[...done[agentId]].reverse().findIndex(m=>m.pending);
|
||
if(idx>=0)done[agentId][done[agentId].length-1-idx]={role:"agent",text:answer};
|
||
else done[agentId].push({role:"agent",text:answer});
|
||
done[agentId]=done[agentId].slice(-60);saveAgentChats(done);renderAgentChat(agentId);
|
||
};
|
||
document.querySelectorAll("[data-chat-prompt]").forEach(btn=>btn.onclick=()=>send(btn.dataset.chatPrompt));
|
||
const input=$("agentChatInput"),sendBtn=$("agentChatSend");
|
||
if(sendBtn)sendBtn.onclick=()=>send(input&&input.value);
|
||
if(input)input.onkeydown=(e)=>{if(e.key==="Enter")send(input.value);};
|
||
const win=$("agentChatWindow");if(win)win.scrollTop=win.scrollHeight;
|
||
}
|
||
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 the latest suggestions.</div>`;
|
||
return;
|
||
}
|
||
const sort=localStorage.getItem(PF_SORT_KEY)||"pnl_desc";
|
||
const ordered=[...positions].sort((a,b)=>{
|
||
if(sort==="pnl_asc")return (a.unrealized_pnl||0)-(b.unrealized_pnl||0);
|
||
if(sort==="value_desc")return (b.value||0)-(a.value||0);
|
||
if(sort==="newest")return String(b.opened_at||"").localeCompare(String(a.opened_at||""));
|
||
if(sort==="category")return String(a.category||"").localeCompare(String(b.category||""))||((b.unrealized_pnl||0)-(a.unrealized_pnl||0));
|
||
return (b.unrealized_pnl||0)-(a.unrealized_pnl||0);
|
||
});
|
||
root.innerHTML=ordered.map(p=>{const pnl=p.unrealized_pnl||0,col=catColor(p.category);
|
||
const title=`<a class="market-title" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">${esc(p.question)}</a>`;
|
||
return `<div class="pos"><div>
|
||
<div class="pq">${title}</div>
|
||
<div class="sub">${p.category?`<span class="cat-badge" style="background:${col}22;color:${col}">${esc(p.category)}</span> `:""}${p.shares} ${p.side} @ ${Math.round(p.entry_price*100)}¢ → ${Math.round(p.current_price*100)}¢</div>
|
||
${p.peer_note?`<div class="sub">Peer read: ${esc(p.peer_note)}</div>`:""}
|
||
${p.net_edge!=null||p.evidence_score!=null?`<div class="sub">Entry quality: ${p.net_edge!=null?`net edge ${((Math.abs(p.net_edge))*100).toFixed(1)}c`:"net edge n/a"} · ${p.evidence_score!=null?`evidence ${Math.round(Number(p.evidence_score||0)*100)}`:"evidence n/a"}</div>`:""}
|
||
<div class="sub">${esc(stopLossLabel(p))}</div>
|
||
<div class="sub">${esc(gainStopLabel(p))}</div>
|
||
<div class="sub"><a class="market-link" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">Open exact market</a></div>
|
||
</div><div class="right"><div>${fmtUSD(p.value)}</div><div class="${signClass(pnl)}">${pnl>=0?"+":""}${fmtUSD(pnl)}</div></div></div>`;}).join("");
|
||
}
|
||
function renderHistory(history){
|
||
const root=$("history");if(!root)return;
|
||
if(!history.length){root.innerHTML=`<div class="empty">No activity yet.</div>`;return;}
|
||
root.innerHTML=history.map(h=>`<div class="log-item"><span class="badge ${h.action}">${h.action}</span><span class="log-date">${h.date}</span> — ${esc(h.detail)}</div>`).join("");
|
||
}
|
||
function hashPassword(s){
|
||
let h=2166136261;for(const ch of String(s)){h^=ch.charCodeAt(0);h=Math.imul(h,16777619);}
|
||
return (h>>>0).toString(16);
|
||
}
|
||
const unlockKey=(id)=>`pma_unlocked_${id}`;
|
||
function isPaperUnlocked(acct){return !acct.passwordHash||sessionStorage.getItem(unlockKey(acct.id))==="1";}
|
||
function defaultPaperAccount(name="Paper Trader",password=""){
|
||
const id="acct_"+Date.now().toString(36)+Math.random().toString(36).slice(2,6);
|
||
if(password)sessionStorage.setItem(unlockKey(id),"1");
|
||
return {id,name,cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],history:[],snapshots:[],passwordHash:password?hashPassword(password):null};
|
||
}
|
||
function loadPaperStore(){
|
||
let store;try{store=JSON.parse(localStorage.getItem(PAPER_KEY));}catch(e){store=null;}
|
||
if(!store||!store.accounts){
|
||
const acct=defaultPaperAccount("My Paper Account");
|
||
store={activeId:acct.id,accounts:{[acct.id]:acct}};
|
||
localStorage.setItem(PAPER_KEY,JSON.stringify(store));
|
||
}
|
||
if(!store.accounts[store.activeId])store.activeId=Object.keys(store.accounts)[0];
|
||
Object.values(store.accounts).forEach(a=>{if(!a.history)a.history=[];if(!a.positions)a.positions=[];if(!a.snapshots)a.snapshots=[];if(a.passwordHash===undefined)a.passwordHash=null;});
|
||
return store;
|
||
}
|
||
function savePaperStore(store,sync=true){
|
||
localStorage.setItem(PAPER_KEY,JSON.stringify(store));
|
||
if(sync){pushCloudState();syncActivePaperAccount(store);}
|
||
}
|
||
function activePaperAccount(store=loadPaperStore()){
|
||
return store.accounts[store.activeId]||Object.values(store.accounts)[0];
|
||
}
|
||
async function paperAccountRequest(payload){
|
||
const r=await fetch("/api/accounts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
|
||
const d=await r.json().catch(()=>({ok:false,error:"Account service returned an invalid response"}));
|
||
if(!r.ok||!d.ok)throw new Error(d.error||"Paper account request failed");
|
||
return d;
|
||
}
|
||
function installCloudPaperAccount(account,token){
|
||
const store=loadPaperStore();
|
||
account.cloudBacked=true;account.passwordHash="cloud";
|
||
store.accounts[account.id]=account;store.activeId=account.id;
|
||
sessionStorage.setItem(unlockKey(account.id),"1");
|
||
sessionStorage.setItem(paperSessionKey(account.id),token);
|
||
savePaperStore(store,false);
|
||
return store;
|
||
}
|
||
async function createCloudPaperAccount(name,password){
|
||
const local=defaultPaperAccount(name,password);
|
||
local.passwordHash="cloud";local.cloudBacked=true;
|
||
const d=await paperAccountRequest({action:"create",name,password,account:local});
|
||
installCloudPaperAccount(d.account,d.session_token);
|
||
}
|
||
async function loginCloudPaperAccount(name,password){
|
||
const d=await paperAccountRequest({action:"login",name,password});
|
||
installCloudPaperAccount(d.account,d.session_token);
|
||
}
|
||
let PAPER_SYNC_TIMER=null;
|
||
function syncActivePaperAccount(store=loadPaperStore()){
|
||
const acct=activePaperAccount(store);
|
||
if(!acct||!acct.cloudBacked)return;
|
||
const token=sessionStorage.getItem(paperSessionKey(acct.id));
|
||
if(!token)return;
|
||
clearTimeout(PAPER_SYNC_TIMER);
|
||
PAPER_SYNC_TIMER=setTimeout(async()=>{
|
||
try{
|
||
await paperAccountRequest({action:"save",account_id:acct.id,session_token:token,account:acct});
|
||
}catch(e){
|
||
toast(e.message||"Cloud paper save failed.");
|
||
}
|
||
},500);
|
||
}
|
||
function paperEquity(acct){
|
||
return acct.cash+(acct.positions||[]).reduce((s,p)=>s+(p.value||p.shares*p.current_price||0),0);
|
||
}
|
||
function recordPaperSnapshot(acct){
|
||
const eq=paperEquity(acct),ts=nowIso(),last=acct.snapshots&&acct.snapshots[acct.snapshots.length-1];
|
||
if(last&&last.timestamp&&Math.abs(new Date(ts)-new Date(last.timestamp))<30000)return;
|
||
acct.snapshots=acct.snapshots||[];
|
||
acct.snapshots.push({timestamp:ts,date:todayStr(),cash:+acct.cash.toFixed(2),equity:+eq.toFixed(2),return_pct:+((eq/acct.starting_balance-1)*100).toFixed(2),open_positions:(acct.positions||[]).length});
|
||
acct.snapshots=acct.snapshots.slice(-500);
|
||
}
|
||
function markPaperToMarket(acct){
|
||
const sugs=(loadSuggestions().suggestions)||[];
|
||
const byId={};sugs.forEach(s=>byId[s.market_id]=s);
|
||
(acct.positions||[]).forEach(pos=>{
|
||
if(pos.kind==="AGENT"){
|
||
const st=loadState(),agent=st.agents[pos.agentId],eq=agent?equity(agent):STARTING_BALANCE;
|
||
pos.current_price=+eq.toFixed(2);pos.value=+(pos.units*eq).toFixed(2);pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);return;
|
||
}
|
||
const s=byId[pos.market_id];
|
||
if(s){
|
||
const price=pos.side==="YES"?s.yes_price:s.no_price;
|
||
pos.current_price=+price.toFixed(4);
|
||
pos.url=s.url||pos.url||"";
|
||
}
|
||
pos.value=+(pos.shares*pos.current_price).toFixed(2);
|
||
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
|
||
});
|
||
}
|
||
function requirePaperUnlock(acct,password){
|
||
if(isPaperUnlocked(acct))return true;
|
||
if(password&&hashPassword(password)===acct.passwordHash){sessionStorage.setItem(unlockKey(acct.id),"1");return true;}
|
||
toast("Unlock this paper account first.");
|
||
return false;
|
||
}
|
||
function cloneAgentBook(agentId){
|
||
const st=loadState(),src=st.agents[agentId],cfg=agentById(agentId);
|
||
if(!src)return null;
|
||
return {cash:+src.cash.toFixed(2),positions:JSON.parse(JSON.stringify(src.positions||[])),
|
||
history:[{date:logDay(),action:"COPY",detail:`Copied ${cfg.name}'s current paper book exactly.`}]};
|
||
}
|
||
function currentPaperMarkets(){return PAPER_SEARCH_RESULTS||(loadSuggestions().suggestions)||[];}
|
||
function paperBuy(marketId,side,amount){
|
||
const store=loadPaperStore(),acct=activePaperAccount(store),s=currentPaperMarkets().find(x=>x.market_id===marketId);
|
||
if(!requirePaperUnlock(acct,$("paperPasswordInput")&&$("paperPasswordInput").value))return;
|
||
if(!s)return toast("Market not found in current ideas.");
|
||
amount=Math.max(0,Math.min(Number(amount||0),acct.cash));
|
||
if(amount<1)return toast("Enter a paper amount first.");
|
||
const entry=side==="YES"?s.yes_price:s.no_price;
|
||
if(entry<=0||entry>=1)return toast("That market price is unavailable.");
|
||
const shares=+(amount/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
|
||
acct.cash=+(acct.cash-cost).toFixed(2);
|
||
acct.positions.push({market_id:s.market_id,question:s.question,side,shares,entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),
|
||
cost,value:cost,unrealized_pnl:0,category:s.category,opened_at:cycleIso(),url:s.url||"",paper:true});
|
||
acct.history.push({date:logDay(),action:"OPEN",detail:`Paper bought ${shares} ${side} '${s.question.slice(0,40)}' @ ${pct(entry)} for ${fmtUSD(cost)}`});
|
||
recordPaperSnapshot(acct);
|
||
savePaperStore(store);renderPaperTab();toast("Paper bet placed.");
|
||
}
|
||
function paperBuyAgent(agentId,amount){
|
||
const store=loadPaperStore(),acct=activePaperAccount(store),agent=loadState().agents[agentId],cfg=agentById(agentId);
|
||
if(!requirePaperUnlock(acct,$("paperPasswordInput")&&$("paperPasswordInput").value))return;
|
||
amount=Math.max(0,Math.min(Number(amount||0),acct.cash));
|
||
if(!agent||amount<1)return toast("Enter a paper amount first.");
|
||
const eq=Math.max(1,equity(agent)),units=amount/eq;
|
||
acct.cash=+(acct.cash-amount).toFixed(2);
|
||
acct.positions.push({kind:"AGENT",agentId,question:`${cfg.name} agent shares`,side:"AGENT",shares:+units.toFixed(6),units,
|
||
entry_price:+eq.toFixed(2),current_price:+eq.toFixed(2),cost:+amount.toFixed(2),value:+amount.toFixed(2),unrealized_pnl:0,category:"Agent",opened_at:cycleIso(),paper:true});
|
||
acct.history.push({date:logDay(),action:"OPEN",detail:`Bought ${fmtUSD(amount)} of ${cfg.name}'s return stream.`});
|
||
recordPaperSnapshot(acct);
|
||
savePaperStore(store);renderPaperTab();toast(`Bought ${cfg.name} shares.`);
|
||
}
|
||
function paperTradeFromMarket(m){
|
||
const a=analyzeMarket(m);
|
||
return a||{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:m.yes_price,no_price:m.no_price,side:"YES",entry_price:m.yes_price,conviction:0,
|
||
volume:m.volume||0,volume_24hr:m.volume_24hr||0,days_to_resolution:m.days_to_resolution,
|
||
rationale:"Search result: available for manual paper trading.",drivers:["manual search"]};
|
||
}
|
||
function paperClose(index){
|
||
const store=loadPaperStore(),acct=activePaperAccount(store),pos=acct.positions[index];
|
||
if(!requirePaperUnlock(acct,$("paperPasswordInput")&&$("paperPasswordInput").value))return;
|
||
if(!pos)return;
|
||
const proceeds=+(pos.shares*pos.current_price).toFixed(2),realized=+(proceeds-pos.cost).toFixed(2);
|
||
acct.cash=+(acct.cash+proceeds).toFixed(2);
|
||
acct.history.push({date:logDay(),action:"CLOSE",detail:`Paper sold '${pos.question.slice(0,40)}' for ${fmtUSD(proceeds)} (${fmtUSD(realized)})`});
|
||
acct.positions.splice(index,1);
|
||
recordPaperSnapshot(acct);
|
||
savePaperStore(store);renderPaperTab();
|
||
}
|
||
function paperMoveCash(kind,amount,password){
|
||
const store=loadPaperStore(),acct=activePaperAccount(store);
|
||
if(!requirePaperUnlock(acct,password))return;
|
||
amount=Math.max(0,Number(amount||0));if(amount<1)return toast("Enter an amount.");
|
||
if(kind==="withdraw"&&amount>acct.cash)return toast("Not enough paper cash.");
|
||
acct.cash=+(acct.cash+(kind==="deposit"?amount:-amount)).toFixed(2);
|
||
if(kind==="deposit")acct.starting_balance=+(acct.starting_balance+amount).toFixed(2);
|
||
acct.history.push({date:logDay(),action:kind==="deposit"?"DEPOSIT":"WITHDRAW",detail:`${kind==="deposit"?"Deposited":"Withdrew"} ${fmtUSD(amount)} paper cash.`});
|
||
recordPaperSnapshot(acct);
|
||
savePaperStore(store);renderPaperTab();toast(kind==="deposit"?"Paper deposit added.":"Paper withdrawal complete.");
|
||
}
|
||
function renderExactTrades(){
|
||
const root=$("exactTrades");if(!root)return;
|
||
const rows=board().flatMap(row=>(row.p.positions||[]).map(pos=>({agent:row.c,pos})))
|
||
.sort((a,b)=>(b.pos.value||0)-(a.pos.value||0));
|
||
if(!rows.length){root.innerHTML=`<div class="empty">No open agent trades yet.</div>`;return;}
|
||
root.innerHTML=rows.slice(0,80).map(({agent,pos})=>`<div class="trade-card">
|
||
<div class="trade-head"><div><div class="trade-title">${agent.emoji} ${esc(agent.name)} · <a class="market-title" href="${esc(positionUrl(pos))}" target="_blank" rel="noopener">${esc(pos.question)}</a></div>
|
||
<div class="trade-meta"><span>${esc(pos.side)}</span><span>${Number(pos.shares||0).toFixed(2)} shares</span><span>entry ${pct(pos.entry_price)}</span><span>now ${pct(pos.current_price)}</span><span>${fmtUSD(pos.value||0)}</span></div></div>
|
||
<a class="btn ghost" href="${esc(positionUrl(pos))}" target="_blank" rel="noopener">Open market</a></div>
|
||
</div>`).join("");
|
||
}
|
||
function loadInvestAllocations(){
|
||
try{return JSON.parse(localStorage.getItem(INVEST_KEY))||{};}catch(e){return {};}
|
||
}
|
||
function saveInvestAllocations(a){
|
||
localStorage.setItem(INVEST_KEY,JSON.stringify(a));
|
||
pushCloudState();
|
||
}
|
||
function agentExposureSummary(cfg,p,amount){
|
||
const eq=Math.max(equity(p),1);
|
||
const open=(p.positions||[]).filter(x=>(x.value||0)>0).slice().sort((a,b)=>(b.value||0)-(a.value||0)).slice(0,3);
|
||
if(!amount||!open.length)return `<div class="empty">Add an amount to preview this agent's largest mapped exposures.</div>`;
|
||
return open.map(pos=>{
|
||
const mapped=amount*((pos.value||0)/eq);
|
||
const q=`<a class="market-title" href="${esc(positionUrl(pos))}" target="_blank" rel="noopener">${esc(pos.question.slice(0,52))}${pos.question.length>52?"...":""}</a>`;
|
||
return `<div class="exposure-row"><span>${q}<br><span class="muted small">${esc(pos.side)} · ${esc(pos.category||"Other")}</span></span><b>${fmtUSD(mapped)}</b></div>`;
|
||
}).join("");
|
||
}
|
||
function renderInvestTab(){
|
||
const root=$("investAgents"); if(!root)return;
|
||
const totals=$("investTotals");
|
||
const st=loadState(), alloc=loadInvestAllocations();
|
||
const rows=board().map(row=>{
|
||
const amount=Number(alloc[row.c.id]||0);
|
||
return Object.assign({},row,{amount});
|
||
});
|
||
const total=rows.reduce((s,r)=>s+(Number.isFinite(r.amount)?r.amount:0),0);
|
||
const chosen=rows.filter(r=>r.amount>0).length;
|
||
const avgReturn=chosen?rows.filter(r=>r.amount>0).reduce((s,r)=>s+r.ret*(r.amount/total),0):0;
|
||
if(totals)totals.innerHTML=[
|
||
{k:"Planned capital",v:fmtUSD(total)},
|
||
{k:"Agents funded",v:String(chosen)},
|
||
{k:"Weighted return",v:chosen?fmtPct(avgReturn):"+0.00%"},
|
||
].map(x=>`<div class="invest-num"><div class="k">${x.k}</div><div class="v">${x.v}</div></div>`).join("");
|
||
root.innerHTML=rows.map(row=>{
|
||
const pct=total>0?(row.amount/total*100):0;
|
||
return `<div class="invest-agent" style="border-left:3px solid ${row.c.color};padding-left:12px">
|
||
<div class="lb-emoji">${row.c.emoji}</div>
|
||
<div>
|
||
<div class="invest-name">${row.c.name}</div>
|
||
<div class="invest-copy">${fmtPct(row.ret)} return · ${row.p.positions.length} open · ${pct.toFixed(1)}% of draft allocation</div>
|
||
<div style="margin-top:8px">${agentExposureSummary(row.c,row.p,row.amount)}</div>
|
||
</div>
|
||
<label class="small muted">Amount<br><input type="number" min="0" step="25" value="${row.amount||""}" placeholder="$0" data-invest-agent="${row.c.id}" aria-label="Allocation for ${esc(row.c.name)}"></label>
|
||
</div>`;
|
||
}).join("");
|
||
document.querySelectorAll("[data-invest-agent]").forEach(input=>{
|
||
const saveDraft=()=>{
|
||
const next=loadInvestAllocations();
|
||
const val=Math.max(0,Number(input.value||0));
|
||
if(val)next[input.dataset.investAgent]=val;
|
||
else delete next[input.dataset.investAgent];
|
||
localStorage.setItem(INVEST_KEY,JSON.stringify(next));
|
||
};
|
||
input.oninput=saveDraft;
|
||
input.onchange=()=>{saveDraft();pushCloudState();renderInvestTab();};
|
||
});
|
||
renderExactTrades();
|
||
}
|
||
function renderPaperTab(){
|
||
const acctSelect=$("paperAccountSelect"),root=$("paperMarketList");
|
||
if(!acctSelect||!root)return;
|
||
const store=loadPaperStore(),acct=activePaperAccount(store);
|
||
markPaperToMarket(acct);savePaperStore(store,false);
|
||
acctSelect.innerHTML=Object.values(store.accounts).map(a=>`<option value="${a.id}" ${a.id===store.activeId?"selected":""}>${esc(a.name)}</option>`).join("");
|
||
acctSelect.onchange=()=>{store.activeId=acctSelect.value;savePaperStore(store);renderPaperTab();};
|
||
const agentSel=$("copyAgentSelect");
|
||
if(agentSel)agentSel.innerHTML=AGENTS.map(a=>`<option value="${a.id}">${a.emoji} ${esc(a.name)}</option>`).join("");
|
||
const buyAgentSel=$("buyAgentSelect");
|
||
if(buyAgentSel)buyAgentSel.innerHTML=AGENTS.map(a=>`<option value="${a.id}">${a.emoji} ${esc(a.name)}</option>`).join("");
|
||
const lock=$("paperLockPanel");
|
||
if(lock)lock.innerHTML=acct.passwordHash&&!isPaperUnlocked(acct)
|
||
? `<div class="locked-panel"><b>${acct.cloudBacked?"Cloud paper account locked.":"Password protected."}</b><div class="account-row" style="margin:8px 0 0"><input class="input" id="paperUnlockInput" type="password" placeholder="Password"><button class="btn" id="paperUnlockBtn">${acct.cloudBacked?"Cloud log in":"Unlock"}</button></div></div>`
|
||
: acct.cloudBacked?`<div class="small muted" style="margin-bottom:10px">Cloud-synced paper account unlocked. Trades and portfolio history save across devices.</div>`
|
||
: acct.passwordHash?`<div class="small muted" style="margin-bottom:10px">Local account unlocked for this browser session.</div>`:`<div class="small muted" style="margin-bottom:10px">Local-only paper account. Add a password when creating an account to sync across devices.</div>`;
|
||
const unlockBtn=$("paperUnlockBtn");
|
||
if(unlockBtn)unlockBtn.onclick=async()=>{
|
||
if(acct.cloudBacked){
|
||
try{await loginCloudPaperAccount(acct.name,$("paperUnlockInput").value);renderPaperTab();toast("Cloud paper account loaded.");}
|
||
catch(e){toast(e.message);}
|
||
}else if(requirePaperUnlock(acct,$("paperUnlockInput").value)){renderPaperTab();toast("Paper account unlocked.");}
|
||
};
|
||
const locked=acct.passwordHash&&!isPaperUnlocked(acct);
|
||
if(locked){
|
||
$("paperStats").innerHTML=`<div class="locked-panel" style="grid-column:1/-1">Log in to view this paper portfolio.</div>`;
|
||
$("paperPositions").innerHTML=`<div class="empty">Locked.</div>`;
|
||
$("paperHistory").innerHTML=`<div class="empty">Locked.</div>`;
|
||
const chart=$("paperPortfolioChart");if(chart)chart.innerHTML="";
|
||
renderPaperMarkets();renderPaperLeaderboard(store);return;
|
||
}
|
||
const eq=paperEquity(acct),pnl=eq-acct.starting_balance;
|
||
recordPaperSnapshot(acct);
|
||
$("paperStats").innerHTML=[
|
||
{ic:"💰",label:"Equity",value:fmtUSD(eq)},
|
||
{ic:"💵",label:"Cash",value:fmtUSD(acct.cash)},
|
||
{ic:"📈",label:"P&L",value:fmtUSD(pnl),cls:signClass(pnl)},
|
||
{ic:"🎯",label:"Return",value:fmtPct((eq/acct.starting_balance-1)*100),cls:signClass(pnl)},
|
||
].map(s=>`<div class="stat"><div class="ic">${s.ic}</div><div class="label">${s.label}</div><div class="value ${s.cls||""}">${s.value}</div></div>`).join("");
|
||
renderPaperPositions(acct);
|
||
renderPaperMarkets();
|
||
renderPaperHistory(acct);
|
||
renderPaperLeaderboard(store);
|
||
renderPaperPortfolioChart(acct);
|
||
}
|
||
function renderPaperPositions(acct){
|
||
const root=$("paperPositions");if(!root)return;
|
||
if(!acct.positions.length){root.innerHTML=`<div class="empty">No paper positions yet. Copy an agent or place a paper bet from the market browser.</div>`;return;}
|
||
root.innerHTML=acct.positions.map((p,i)=>{const pnl=p.unrealized_pnl||0,col=catColor(p.category);
|
||
const detail=p.kind==="AGENT"
|
||
? `${Number(p.shares||0).toFixed(6)} agent units · entry ${fmtUSD(p.entry_price)} → ${fmtUSD(p.current_price)}`
|
||
: `${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)}¢`;
|
||
return `<div class="pos"><div>
|
||
<div class="pq"><a class="market-title" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">${esc(p.question)}</a></div>
|
||
<div class="sub">${detail}</div>
|
||
${p.kind==="AGENT"?"":`<div class="sub"><a class="market-link" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">Open market</a></div>`}
|
||
</div><div class="right"><div>${fmtUSD(p.value||0)}</div><div class="${signClass(pnl)}">${pnl>=0?"+":""}${fmtUSD(pnl)}</div><button class="btn ghost" data-paper-close="${i}" style="margin-top:7px">Sell</button></div></div>`;
|
||
}).join("");
|
||
document.querySelectorAll("[data-paper-close]").forEach(btn=>btn.onclick=()=>paperClose(Number(btn.dataset.paperClose)));
|
||
}
|
||
function renderPaperMarkets(){
|
||
const root=$("paperMarketList"),filter=$("paperFilterbar");if(!root)return;
|
||
const all=currentPaperMarkets(),focus=getFocus();
|
||
if(filter){
|
||
const counts={};CATEGORIES.forEach(c=>counts[c]=0);all.forEach(s=>{counts[counts[s.category]!==undefined?s.category:"Other"]++;});counts.All=all.length;
|
||
filter.innerHTML=CATEGORIES.map(c=>`<span class="chip ${focus===c?"active":""}" data-paper-cat="${c}">${c}<span class="ct">${counts[c]||0}</span></span>`).join("");
|
||
document.querySelectorAll("[data-paper-cat]").forEach(el=>el.onclick=()=>{setFocus(el.dataset.paperCat);renderPaperTab();});
|
||
}
|
||
const shown=all.filter(s=>focus==="All"||!focus||s.category===focus);
|
||
if(!shown.length){root.innerHTML=`<div class="empty">Run a cycle or search all Polymarket markets to load paper-tradable ideas.</div>`;return;}
|
||
root.innerHTML=shown.map(s=>`<div class="trade-card">
|
||
<div class="trade-head"><div><div class="trade-title"><a class="market-title" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">${esc(s.question)}</a></div>
|
||
<div class="trade-meta"><span>${esc(s.category)}</span><span>YES ${pct(s.yes_price)}</span><span>NO ${pct(s.no_price)}</span><span>${Math.round(s.conviction)} conviction</span></div></div>
|
||
<a class="btn ghost" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">Open market</a></div>
|
||
<div class="trade-actions">
|
||
<input class="input" type="number" min="1" step="25" value="100" data-paper-amount="${esc(s.market_id)}" aria-label="Paper amount for ${esc(s.question)}">
|
||
<button class="btn" data-paper-buy="${esc(s.market_id)}" data-side="YES">Paper YES</button>
|
||
<button class="btn" data-paper-buy="${esc(s.market_id)}" data-side="NO">Paper NO</button>
|
||
</div>
|
||
</div>`).join("");
|
||
document.querySelectorAll("[data-paper-buy]").forEach(btn=>btn.onclick=()=>{
|
||
const input=[...document.querySelectorAll("[data-paper-amount]")].find(el=>el.dataset.paperAmount===btn.dataset.paperBuy);
|
||
paperBuy(btn.dataset.paperBuy,btn.dataset.side,input&&input.value);
|
||
});
|
||
}
|
||
function renderPaperHistory(acct){
|
||
const root=$("paperHistory");if(!root)return;
|
||
const hist=(acct.history||[]).slice(-40).reverse();
|
||
root.innerHTML=hist.length?hist.map(h=>`<div class="log-item"><span class="badge ${h.action}">${h.action}</span><span class="log-date">${h.date}</span> — ${esc(h.detail)}</div>`).join(""):`<div class="empty">No paper activity yet.</div>`;
|
||
}
|
||
function renderPaperLeaderboard(store){
|
||
const root=$("paperLeaderboard");if(!root)return;
|
||
const rows=Object.values(store.accounts).map(a=>{markPaperToMarket(a);const eq=paperEquity(a),ret=(eq/a.starting_balance-1)*100;return {a,eq,ret};}).sort((x,y)=>y.ret-x.ret);
|
||
root.innerHTML=rows.map((r,i)=>`<div class="leader-row"><div><b>#${i+1} ${esc(r.a.name)}</b><div class="small muted">${r.a.positions.length} open · ${fmtUSD(r.eq)} equity</div></div><div class="${signClass(r.ret)}"><b>${fmtPct(r.ret)}</b></div></div>`).join("");
|
||
}
|
||
function renderPaperPortfolioChart(acct){
|
||
const svg=$("paperPortfolioChart");if(!svg)return;
|
||
const snaps=(acct.snapshots||[]).filter(s=>Number.isFinite(snapTime(s))).sort((a,b)=>snapTime(a)-snapTime(b));
|
||
if(snaps.length<2){svg.innerHTML=`<text x="24" y="40" fill="rgba(224,234,255,.7)" font-size="14">Portfolio value history will appear after a few account changes.</text>`;return;}
|
||
const W=720,H=260,padL=62,padR=24,padT=26,padB=38,vals=snaps.map(s=>s.equity),mn=Math.min(...vals),mx=Math.max(...vals),span=(mx-mn)||1;
|
||
const x=i=>padL+(i/(snaps.length-1))*(W-padL-padR),y=v=>padT+(mx-v)/span*(H-padT-padB);
|
||
const d=vals.map((v,i)=>`${i?"L":"M"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
||
let inner=`<rect x="${padL}" y="${padT}" width="${W-padL-padR}" height="${H-padT-padB}" rx="12" fill="rgba(255,255,255,.025)" stroke="rgba(255,255,255,.08)"/>`;
|
||
for(let i=0;i<4;i++){const v=mn+(span*i/3),yy=y(v);inner+=`<line x1="${padL}" x2="${W-padR}" y1="${yy}" y2="${yy}" stroke="rgba(255,255,255,.06)"/><text x="${padL-10}" y="${yy+4}" text-anchor="end" fill="rgba(224,234,255,.62)" font-size="11">${fmtUSD(v)}</text>`;}
|
||
inner+=`<path d="${d}" fill="none" stroke="#7c8cff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>`;
|
||
for(const tick of chartTicks(snaps.map(snapTime),4)){inner+=`<text x="${x(tick.i)}" y="${H-12}" text-anchor="middle" fill="rgba(224,234,255,.7)" font-size="11">${chartDateLabel(tick.t,true)}</text>`;}
|
||
svg.innerHTML=inner;
|
||
}
|
||
async function searchPaperMarkets(){
|
||
const q=($("paperSearchInput").value||"").trim();
|
||
if(!q)return toast("Enter a market search.");
|
||
PAPER_MARKET_OFFSET=0;PAPER_MARKET_QUERY=q;
|
||
await loadPaperMarketPage(true);
|
||
}
|
||
async function loadPaperMarketPage(reset=false,loadAll=false){
|
||
const q=PAPER_MARKET_QUERY||(($("paperSearchInput")&&$("paperSearchInput").value)||"").trim();
|
||
setStatus("searching Polymarket...",true);
|
||
try{
|
||
if(reset){PAPER_SEARCH_RESULTS=[];PAPER_MARKET_OFFSET=0;}
|
||
let loaded=0,rawCount=0,keepGoing=true;
|
||
const limit=100,maxPages=loadAll?MAX_ACTIVE_MARKET_PAGES:1;
|
||
let cursor=null;
|
||
for(let page=0;page<maxPages&&keepGoing;page++){
|
||
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",limit:String(limit),order:"volume24hr",ascending:"false"});
|
||
if(loadAll){if(cursor)params.set("after_cursor",cursor);}
|
||
else params.set("offset",String(PAPER_MARKET_OFFSET));
|
||
if(q)params.set("search",q);
|
||
const r=await fetch(`${GAMMA}/markets${loadAll?"/keyset":""}?${params}`);
|
||
if(!r.ok)throw new Error("market load failed");
|
||
const pageData=await r.json();
|
||
const raw=loadAll?(pageData.markets||[]):pageData;
|
||
rawCount+=raw.length;
|
||
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
|
||
PAPER_SEARCH_RESULTS=(PAPER_SEARCH_RESULTS||[]).concat(next);
|
||
PAPER_MARKET_OFFSET+=raw.length;
|
||
if(loadAll)cursor=pageData.next_cursor||null;
|
||
loaded+=next.length;
|
||
if(loadAll)setStatus(`loading active markets (${PAPER_SEARCH_RESULTS.length})…`,true);
|
||
if(raw.length<limit||(loadAll&&!cursor))keepGoing=false;
|
||
}
|
||
renderPaperTab();setStatus("up to date",false);
|
||
toast(loaded?`Loaded ${PAPER_SEARCH_RESULTS.length} active markets.`:rawCount?"No tradable binary markets found in this page.":"No more active markets found.");
|
||
}catch(e){setStatus("market load error",false);toast("Polymarket market load failed.");}
|
||
}
|
||
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("");
|
||
}
|
||
const LIVE_CHECKS=[
|
||
["personal_scope","I will use only my own money and my own Polymarket/deposit wallet"],
|
||
["no_investors","No outside investors, pooled funds, user deposits, or copy-trading customers"],
|
||
["eligibility","I personally satisfy the provider and Polymarket eligibility/location rules"],
|
||
["wallet","My own deposit wallet is created, controlled by me, and funded only by me"],
|
||
["clob","Personal CLOB credentials are configured only for my own wallet"],
|
||
["manual_only","Every staged intent requires manual review before any order is placed"],
|
||
["reconciliation","I will compare cash, open orders, fills, and positions after each test"],
|
||
["risk","Personal max amount, per-agent caps, stop rules, and emergency pause are set"],
|
||
["security","Secret management, encryption, rate limits, abuse controls, and security review completed"],
|
||
["records","Append-only audit logs are recording profile changes and staged intents"],
|
||
["operations","Incident alerting and emergency pause path are ready"],
|
||
["testing","Tiny manual test trades are checked before increasing size"],
|
||
];
|
||
function defaultLiveState(){
|
||
const checks={};LIVE_CHECKS.forEach(([k])=>checks[k]=false);
|
||
const agents={};AGENTS.forEach(a=>agents[a.id]={enabled:false,maxAllocation:0,maxPositionPct:10,autoTrade:false,manualApprove:true});
|
||
return {checks,walletAddress:"",jurisdiction:"",depositLimit:0,withdrawReserve:0,agents,audit:[]};
|
||
}
|
||
function loadLiveState(){
|
||
let s;try{s=JSON.parse(localStorage.getItem(LIVE_KEY));}catch(e){s=null;}
|
||
if(!s||!s.checks)s=defaultLiveState();
|
||
LIVE_CHECKS.forEach(([k])=>{if(!(k in s.checks))s.checks[k]=false;});
|
||
if(!s.agents)s.agents={};
|
||
AGENTS.forEach(a=>{if(!s.agents[a.id])s.agents[a.id]={enabled:false,maxAllocation:0,maxPositionPct:10,autoTrade:false,manualApprove:true};});
|
||
if(!s.audit)s.audit=[];
|
||
return s;
|
||
}
|
||
function saveLiveState(s,detail){
|
||
if(detail)s.audit.push({at:nowIso(),detail});
|
||
s.audit=s.audit.slice(-80);
|
||
localStorage.setItem(LIVE_KEY,JSON.stringify(s));
|
||
pushCloudState();
|
||
}
|
||
async function fetchLiveBackendStatus(){
|
||
try{
|
||
const [liveRes,policyRes]=await Promise.all([
|
||
fetch("/api/live",{cache:"no-store"}),
|
||
fetch("/api/policies",{cache:"no-store"}),
|
||
]);
|
||
LIVE_BACKEND_STATUS=await liveRes.json();
|
||
LIVE_POLICY_STATUS=await policyRes.json();
|
||
}catch(e){
|
||
LIVE_BACKEND_STATUS={ok:false,locked_reason:"Live backend status endpoint is unavailable.",providers:[]};
|
||
LIVE_POLICY_STATUS={ok:false,note:"Policy status endpoint is unavailable."};
|
||
}
|
||
renderLiveBackendStatus();
|
||
if(PERSONAL_MODE)loadTradeTickets();
|
||
}
|
||
function renderLiveBackendStatus(){
|
||
const root=$("liveBackendStatus");if(!root)return;
|
||
const hookRoot=$("liveWebhookStatus");
|
||
const policyRoot=$("livePolicyStatus");
|
||
const s=LIVE_BACKEND_STATUS;
|
||
if(!s){root.innerHTML="Checking backend readiness...";if(hookRoot)hookRoot.innerHTML="Checking webhook setup...";if(policyRoot)policyRoot.innerHTML="Checking policy status...";return;}
|
||
const providers=(s.providers||[]).map(p=>`<div class="leader-row"><span>${esc(p.label)}${p.expected?`<div class="small muted">${esc(p.expected)}</div>`:""}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"configured":"missing"}</b></div>`).join("");
|
||
const personalReqs=(s.personal_requirements||[]).map(p=>`<div class="leader-row"><span>${esc(p.label)}${p.expected?`<div class="small muted">${esc(p.expected)}</div>`:""}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"ready":"missing"}</b></div>`).join("");
|
||
const stack=(s.provider_stack||[]).map(p=>`<div class="leader-row"><span><b>${esc(p.provider)}</b><div class="small muted">${esc(p.role)}</div><div class="small muted">${(p.checks||[]).map(c=>`${esc(c.key)}: ${c.configured?"ok":"missing"}`).join(" · ")}</div></span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"ready":"needs keys"}</b></div>`).join("");
|
||
const next=(s.next_required||[]).slice(0,12).map(x=>`<li>${esc(x)}</li>`).join("");
|
||
const personalNext=(s.personal_next_required||[]).slice(0,12).map(x=>`<li>${esc(x)}</li>`).join("");
|
||
const reqs=(s.launch_requirements||[]).map(x=>`<li>${esc(x.label)}</li>`).join("");
|
||
root.innerHTML=`<b>${s.personal_trading_enabled?"Personal prerequisites ready":"Manual personal mode locked to staging"}</b><div class="small muted" style="margin:6px 0">${esc(s.locked_reason||"Public trading remains locked.")}</div>${personalReqs?`<div class="locked-panel" style="margin-top:10px"><b>Personal-only requirements</b>${personalReqs}</div>`:""}${personalNext?`<div class="locked-panel" style="margin-top:10px"><b>Before personal live execution</b><ul>${personalNext}</ul></div>`:""}${stack?`<div class="locked-panel" style="margin-top:10px"><b>Public provider stack</b>${stack}</div>`:""}${providers}${next?`<div class="locked-panel" style="margin-top:10px"><b>Public platform blockers</b><ul>${next}</ul></div>`:""}${reqs?`<div class="locked-panel" style="margin-top:10px"><b>Public launch requirements</b><ol>${reqs}</ol></div>`:""}<div class="small muted" style="margin-top:8px">This cockpit stages manual intents only. Public investor deposits and unattended agent trading stay off.</div>`;
|
||
if(hookRoot){
|
||
const hooks=(s.webhooks||[]).map(h=>`<div class="leader-row"><span><b>${esc(h.provider)}</b><div class="small muted">${esc(h.label)}</div></span><a class="market-link" href="${esc(h.url)}" target="_blank" rel="noopener">${esc(h.url)}</a></div>`).join("");
|
||
hookRoot.innerHTML=hooks||"Webhook URLs will appear after backend readiness loads.";
|
||
}
|
||
if(policyRoot){
|
||
const p=LIVE_POLICY_STATUS||{};
|
||
const versions=p.versions||{};
|
||
const rules=(p.rules||[]).map(rule=>`<li>${esc(rule)}</li>`).join("");
|
||
policyRoot.innerHTML=`<b>${p.approved?"Policy versions configured":"Policy documents not approved yet"}</b><div class="small muted" style="margin:6px 0">${esc(p.note||"")}</div><div class="leader-row"><span>Terms</span><b>${esc(versions.terms||"missing")}</b></div><div class="leader-row"><span>Privacy</span><b>${esc(versions.privacy||"missing")}</b></div><div class="leader-row"><span>Risk disclosure</span><b>${esc(versions.risk_disclosure||"missing")}</b></div>${rules?`<ol class="steps">${rules}</ol>`:""}`;
|
||
}
|
||
}
|
||
async function dryRunLiveOrderIntent(){
|
||
const s=loadLiveState();
|
||
const intent={
|
||
user_id:PERSONAL_USER_ID,
|
||
wallet_address:s.walletAddress,
|
||
agent_id:$("liveIntentAgent").value,
|
||
market_id:$("liveIntentMarket").value.trim(),
|
||
question:($("liveIntentQuestion")&&$("liveIntentQuestion").value||"").trim(),
|
||
side:$("liveIntentSide").value,
|
||
max_amount:Number($("liveIntentAmount").value||0),
|
||
limit_price:Number(($("liveIntentPrice")&&$("liveIntentPrice").value)||0)||null,
|
||
execution_mode:"manual_approval",
|
||
account_scope:PERSONAL_MODE?"personal":"public_readiness",
|
||
};
|
||
try{
|
||
if(PERSONAL_MODE)await createTradeTicket({
|
||
user_id:intent.user_id,
|
||
agent_id:intent.agent_id,
|
||
market_id:intent.market_id,
|
||
question:intent.question,
|
||
side:intent.side,
|
||
max_amount:intent.max_amount,
|
||
limit_price:intent.limit_price,
|
||
rationale:"Manually staged from the personal cockpit.",
|
||
});
|
||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({intent})});
|
||
const d=await r.json();
|
||
$("liveOrderDryRun").innerHTML=`<div class="locked-panel"><b>Order intent dry-run</b><div class="small muted">${esc(d.error||"Accepted dry-run.")}</div><pre style="white-space:pre-wrap;font-size:11px;color:#cdd6e8">${esc(JSON.stringify(d.audit_event||d,null,2))}</pre></div>`;
|
||
saveLiveState(s,`${PERSONAL_MODE?"Staged manual":"Dry-ran"} ${intent.side} order intent for ${intent.agent_id} on ${intent.market_id}`);
|
||
if(PERSONAL_MODE)await loadTradeTickets();
|
||
}catch(e){
|
||
toast(e.message||"Order dry-run failed.");
|
||
}
|
||
}
|
||
async function createTradeTicket(ticket){
|
||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...ticket,action:"ticket"})});
|
||
const d=await r.json();
|
||
if(!r.ok)throw new Error(d.error||"Ticket staging failed");
|
||
toast("Manual trade ticket staged.");
|
||
return d.ticket;
|
||
}
|
||
async function stageSuggestionTicket(marketId){
|
||
const s=(loadSuggestions().suggestions||[]).find(x=>String(x.market_id)===String(marketId));
|
||
if(!s)return toast("Suggestion not found.");
|
||
const amount=Number(prompt("Max amount for this manual ticket?", "10")||0);
|
||
if(!amount||amount<=0)return;
|
||
try{
|
||
await createTradeTicket({
|
||
user_id:PERSONAL_USER_ID,
|
||
agent_id:localStorage.getItem(VIEW_KEY)||"value",
|
||
market_id:s.market_id,
|
||
question:s.question,
|
||
market_url:s.url,
|
||
side:s.side,
|
||
max_amount:amount,
|
||
limit_price:s.entry_price,
|
||
rationale:s.rationale,
|
||
});
|
||
await loadTradeTickets();
|
||
showTab("live");
|
||
}catch(e){toast(e.message||"Ticket staging failed.");}
|
||
}
|
||
async function loadTradeTickets(){
|
||
if(!PERSONAL_MODE)return;
|
||
try{
|
||
const [ticketRes,portfolioRes,capitalRes]=await Promise.all([
|
||
fetch(`/api/live?action=tickets&user_id=${encodeURIComponent(PERSONAL_USER_ID)}&limit=30`,{cache:"no-store"}),
|
||
fetch(`/api/live?action=real_portfolio&user_id=${encodeURIComponent(PERSONAL_USER_ID)}`,{cache:"no-store"}),
|
||
fetch(`/api/live?action=personal_capital&user_id=${encodeURIComponent(PERSONAL_USER_ID)}`,{cache:"no-store"}),
|
||
]);
|
||
const d=await ticketRes.json(),pf=await portfolioRes.json(),cap=await capitalRes.json();
|
||
PERSONAL_TICKETS=d.tickets||[];
|
||
REAL_PORTFOLIO={positions:pf.positions||[],fills:pf.fills||[]};
|
||
PERSONAL_CAPITAL={account:cap.account||{cash:0},allocations:cap.allocations||[],events:cap.events||[]};
|
||
}catch(e){PERSONAL_TICKETS=[];REAL_PORTFOLIO={positions:[],fills:[]};PERSONAL_CAPITAL={account:{cash:0},allocations:[],events:[]};}
|
||
renderTradeTickets();
|
||
renderRealPortfolio();
|
||
renderPersonalCapital();
|
||
}
|
||
async function updateTicketStatus(ticketId,status){
|
||
try{
|
||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"ticket_status",user_id:PERSONAL_USER_ID,ticket_id:ticketId,status})});
|
||
const d=await r.json();
|
||
if(!r.ok)throw new Error(d.error||"Ticket update failed");
|
||
await loadTradeTickets();
|
||
toast("Ticket updated.");
|
||
}catch(e){toast(e.message||"Ticket update failed.");}
|
||
}
|
||
function renderTradeTickets(){
|
||
const root=$("personalTicketQueue");if(!root)return;
|
||
if(!PERSONAL_MODE){root.innerHTML=`<div class="empty">Personal trade tickets are hidden on the public site.</div>`;return;}
|
||
if(!PERSONAL_TICKETS.length){root.innerHTML=`<div class="empty">No staged tickets yet. Stage one from Suggestions or the manual console.</div>`;return;}
|
||
root.innerHTML=PERSONAL_TICKETS.map(t=>{
|
||
const instructions=t.instructions||{},steps=instructions.steps||[];
|
||
const url=t.market_url||instructions.market_url||marketSearchUrl(t.question||t.market_id);
|
||
return `<div class="trade-card">
|
||
<div class="trade-head"><div><div class="trade-title">${esc(t.question||t.market_id)}</div><div class="trade-meta"><span>${esc(t.agent_id)}</span><span>${esc(t.status)}</span><span>${new Date(t.created_at).toLocaleString()}</span></div></div><span class="pill ${esc(t.side)}">BUY ${esc(t.side)}</span></div>
|
||
<div class="small muted">Max ${fmtUSD(Number(t.max_amount||0))}${t.limit_price?` · limit ${Math.round(Number(t.limit_price)*100)}¢`:""} · ticket #${t.id}</div>
|
||
${t.rationale?`<div class="rationale">${esc(t.rationale)}</div>`:""}
|
||
<ol class="steps">${steps.map(step=>`<li>${esc(step)}</li>`).join("")}</ol>
|
||
<div class="trade-actions">
|
||
<a class="btn" href="${esc(url)}" target="_blank" rel="noopener">Open Polymarket</a>
|
||
<button class="btn ghost" data-ticket-status="${t.id}:reviewed">Reviewed</button>
|
||
<button class="btn ghost" data-ticket-status="${t.id}:skipped">Skipped</button>
|
||
<button class="btn ghost" data-ticket-status="${t.id}:cancelled">Cancel</button>
|
||
</div>
|
||
<div class="account-row" style="margin-top:12px">
|
||
<select class="select" data-fill-action="${t.id}"><option>BUY</option><option>SELL</option></select>
|
||
<input class="input" type="number" min="0.01" step="0.01" placeholder="Shares" data-fill-shares="${t.id}">
|
||
<input class="input" type="number" min="0.01" max="0.99" step="0.01" placeholder="Actual price" data-fill-price="${t.id}">
|
||
<input class="input" type="number" min="0" step="0.01" placeholder="Fees" data-fill-fees="${t.id}">
|
||
<input class="input" placeholder="Tx/link note" data-fill-note="${t.id}">
|
||
<button class="btn primary" data-record-fill="${t.id}">Record manual fill</button>
|
||
</div>
|
||
</div>`;
|
||
}).join("");
|
||
document.querySelectorAll("[data-ticket-status]").forEach(btn=>btn.onclick=()=>{const [id,status]=btn.dataset.ticketStatus.split(":");updateTicketStatus(id,status);});
|
||
document.querySelectorAll("[data-record-fill]").forEach(btn=>btn.onclick=()=>recordTicketFill(btn.dataset.recordFill));
|
||
}
|
||
function ticketById(id){return PERSONAL_TICKETS.find(t=>String(t.id)===String(id));}
|
||
async function recordTicketFill(ticketId){
|
||
const t=ticketById(ticketId);
|
||
if(!t)return toast("Ticket not found.");
|
||
const fill={
|
||
action:"record_fill",
|
||
user_id:PERSONAL_USER_ID,
|
||
ticket_id:t.id,
|
||
agent_id:t.agent_id,
|
||
market_id:t.market_id,
|
||
question:t.question,
|
||
market_url:t.market_url,
|
||
side:t.side,
|
||
fill_action:(document.querySelector(`[data-fill-action="${ticketId}"]`)||{}).value||"BUY",
|
||
shares:Number((document.querySelector(`[data-fill-shares="${ticketId}"]`)||{}).value||0),
|
||
price:Number((document.querySelector(`[data-fill-price="${ticketId}"]`)||{}).value||0),
|
||
fees:Number((document.querySelector(`[data-fill-fees="${ticketId}"]`)||{}).value||0),
|
||
tx_note:(document.querySelector(`[data-fill-note="${ticketId}"]`)||{}).value||"",
|
||
};
|
||
try{
|
||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(fill)});
|
||
const d=await r.json();
|
||
if(!r.ok)throw new Error(d.error||"Manual fill failed");
|
||
await loadTradeTickets();
|
||
toast("Manual fill recorded for tracking.");
|
||
}catch(e){toast(e.message||"Manual fill failed.");}
|
||
}
|
||
async function updateRealMark(positionId){
|
||
const input=document.querySelector(`[data-real-mark="${positionId}"]`);
|
||
const price=Number(input&&input.value||0);
|
||
try{
|
||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"mark_position",user_id:PERSONAL_USER_ID,position_id:positionId,current_price:price})});
|
||
const d=await r.json();
|
||
if(!r.ok)throw new Error(d.error||"Mark update failed");
|
||
await loadTradeTickets();
|
||
toast("Position mark updated.");
|
||
}catch(e){toast(e.message||"Mark update failed.");}
|
||
}
|
||
function renderRealPortfolio(){
|
||
const summary=$("realPositionSummary"),posRoot=$("realPositions"),fillsRoot=$("realFills");
|
||
if(!summary||!posRoot||!fillsRoot)return;
|
||
const positions=REAL_PORTFOLIO.positions||[],fills=REAL_PORTFOLIO.fills||[];
|
||
const invested=positions.reduce((s,p)=>s+Number(p.cost_basis||0),0);
|
||
const marketValue=positions.reduce((s,p)=>s+Number(p.shares||0)*Number(p.current_price||p.avg_price||0),0);
|
||
const realized=positions.reduce((s,p)=>s+Number(p.realized_pnl||0),0);
|
||
const pnl=marketValue+realized-invested;
|
||
summary.innerHTML=`<div class="invest-total">
|
||
<div class="invest-num"><div class="k">Tracked value</div><div class="v">${fmtUSD(marketValue)}</div></div>
|
||
<div class="invest-num"><div class="k">Cost basis</div><div class="v">${fmtUSD(invested)}</div></div>
|
||
<div class="invest-num"><div class="k">Tracked P&L</div><div class="v ${signClass(pnl)}">${fmtUSD(pnl)}</div></div>
|
||
</div>`;
|
||
posRoot.innerHTML=positions.length?positions.map(p=>{
|
||
const mark=Number(p.current_price||p.avg_price||0),value=Number(p.shares||0)*mark,pnl=value+Number(p.realized_pnl||0)-Number(p.cost_basis||0);
|
||
return `<div class="trade-card">
|
||
<div class="trade-head"><div><div class="trade-title">${esc(p.question||p.market_id)}</div><div class="trade-meta"><span>${esc(p.side)}</span><span>${Number(p.shares||0).toFixed(2)} shares</span><span>avg ${Math.round(Number(p.avg_price||0)*100)}¢</span></div></div><b class="${signClass(pnl)}">${fmtUSD(pnl)}</b></div>
|
||
<div class="small muted">Value ${fmtUSD(value)} · Cost ${fmtUSD(Number(p.cost_basis||0))} · Realized ${fmtUSD(Number(p.realized_pnl||0))}</div>
|
||
<div class="trade-actions">
|
||
${p.market_url?`<a class="btn ghost" href="${esc(p.market_url)}" target="_blank" rel="noopener">Open market</a>`:""}
|
||
<input class="input" type="number" min="0.01" max="0.99" step="0.01" value="${mark||""}" data-real-mark="${p.id}">
|
||
<button class="btn ghost" data-update-mark="${p.id}">Update mark</button>
|
||
</div>
|
||
</div>`;
|
||
}).join(""):`<div class="empty">No real fills recorded yet. Record a manual fill from a ticket after you trade outside Poly Arena.</div>`;
|
||
fillsRoot.innerHTML=fills.length?fills.slice(0,25).map(f=>`<div class="log-item"><span class="badge ${esc(f.action)}">${esc(f.action)}</span><span class="log-date">${new Date(f.filled_at).toLocaleString()}</span> — ${Number(f.shares||0).toFixed(2)} ${esc(f.side)} @ ${Math.round(Number(f.price||0)*100)}¢ on ${esc((f.question||f.market_id||"").slice(0,52))}</div>`).join(""):`<div class="empty">No manual fills yet.</div>`;
|
||
document.querySelectorAll("[data-update-mark]").forEach(btn=>btn.onclick=()=>updateRealMark(btn.dataset.updateMark));
|
||
}
|
||
async function personalCapitalAction(capitalAction){
|
||
const amount=Number(($("personalCapitalAmount")&&$("personalCapitalAmount").value)||0);
|
||
const agentId=($("personalCapitalAgent")&&$("personalCapitalAgent").value)||"";
|
||
try{
|
||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({
|
||
action:"capital_action",
|
||
user_id:PERSONAL_USER_ID,
|
||
capital_action:capitalAction,
|
||
agent_id:agentId,
|
||
amount,
|
||
note:"Personal cockpit bookkeeping action",
|
||
})});
|
||
const d=await r.json();
|
||
if(!r.ok)throw new Error(d.error||"Capital tracker update failed");
|
||
PERSONAL_CAPITAL={account:d.account||{cash:0},allocations:d.allocations||[],events:d.events||[]};
|
||
renderPersonalCapital();
|
||
toast("Personal tracker updated.");
|
||
}catch(e){toast(e.message||"Capital tracker update failed.");}
|
||
}
|
||
function renderPersonalCapital(){
|
||
const summary=$("personalCapitalSummary"),allocRoot=$("personalAgentAllocations"),histRoot=$("personalCapitalHistory"),agentSel=$("personalCapitalAgent");
|
||
if(!summary||!allocRoot||!histRoot)return;
|
||
if(agentSel)agentSel.innerHTML=AGENTS.map(a=>`<option value="${esc(a.id)}">${a.emoji} ${esc(a.name)}</option>`).join("");
|
||
const cash=Number(PERSONAL_CAPITAL.account&&PERSONAL_CAPITAL.account.cash||0);
|
||
const allocations=PERSONAL_CAPITAL.allocations||[];
|
||
const allocated=allocations.reduce((s,a)=>s+Number(a.amount||0),0);
|
||
summary.innerHTML=`<div class="invest-total">
|
||
<div class="invest-num"><div class="k">Tracked outside cash</div><div class="v">${fmtUSD(cash)}</div></div>
|
||
<div class="invest-num"><div class="k">Agent allocations</div><div class="v">${fmtUSD(allocated)}</div></div>
|
||
<div class="invest-num"><div class="k">Personal tracker total</div><div class="v">${fmtUSD(cash+allocated)}</div></div>
|
||
</div>`;
|
||
allocRoot.innerHTML=allocations.length?allocations.map(a=>{
|
||
const cfg=agentById(a.agent_id)||{name:a.agent_id,emoji:"",color:"#7c8cff"};
|
||
return `<div class="leader-row"><span><b>${cfg.emoji} ${esc(cfg.name)}</b><div class="small muted">bookkeeping allocation only</div></span><b>${fmtUSD(Number(a.amount||0))}</b></div>`;
|
||
}).join(""):`<div class="empty">No agent allocations yet. Record outside cash, then allocate any amount to an agent you want to shadow.</div>`;
|
||
const events=(PERSONAL_CAPITAL.events||[]).slice(0,20);
|
||
histRoot.innerHTML=events.length?events.map(e=>{
|
||
const cfg=e.agent_id?agentById(e.agent_id):null;
|
||
return `<div class="log-item"><span class="badge">${esc(e.action)}</span><span class="log-date">${new Date(e.created_at).toLocaleString()}</span> — ${fmtUSD(Number(e.amount||0))}${cfg?` · ${cfg.emoji} ${esc(cfg.name)}`:""}</div>`;
|
||
}).join(""):`<div class="empty">No personal tracker activity yet.</div>`;
|
||
}
|
||
function currentLiveMarkets(){return LIVE_SEARCH_RESULTS||[];}
|
||
function renderLiveMarkets(){
|
||
const root=$("liveMarketList"),filter=$("liveMarketFilterbar");if(!root)return;
|
||
const all=currentLiveMarkets(),focus=getFocus();
|
||
if(filter){
|
||
const counts={};CATEGORIES.forEach(c=>counts[c]=0);all.forEach(s=>{counts[counts[s.category]!==undefined?s.category:"Other"]++;});counts.All=all.length;
|
||
filter.innerHTML=CATEGORIES.map(c=>`<span class="chip ${focus===c?"active":""}" data-live-cat="${c}">${c}<span class="ct">${counts[c]||0}</span></span>`).join("");
|
||
document.querySelectorAll("[data-live-cat]").forEach(el=>el.onclick=()=>{setFocus(el.dataset.liveCat);renderLiveMarkets();});
|
||
}
|
||
const shown=all.filter(s=>focus==="All"||!focus||s.category===focus);
|
||
if(!shown.length){root.innerHTML=`<div class="empty">Search Polymarket or load active markets to stage manual tickets from the cockpit.</div>`;return;}
|
||
root.innerHTML=shown.map(s=>`<div class="trade-card">
|
||
<div class="trade-head"><div><div class="trade-title"><a class="market-title" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">${esc(s.question)}</a></div>
|
||
<div class="trade-meta"><span>${esc(s.category)}</span><span>YES ${pct(s.yes_price)}</span><span>NO ${pct(s.no_price)}</span><span>${Math.round(s.conviction||0)} conviction</span></div></div>
|
||
<a class="btn ghost" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">Open Polymarket</a></div>
|
||
<div class="rationale">${esc(s.rationale||"Active Polymarket market available for manual review.")}</div>
|
||
<div class="trade-actions">
|
||
<select class="select" data-live-side="${esc(s.market_id)}"><option>YES</option><option>NO</option></select>
|
||
<input class="input" type="number" min="1" step="25" value="100" data-live-amount="${esc(s.market_id)}" aria-label="Manual amount for ${esc(s.question)}">
|
||
<input class="input" type="number" min="0.01" max="0.99" step="0.01" value="${Number(s.entry_price||s.yes_price||0).toFixed(2)}" data-live-price="${esc(s.market_id)}" aria-label="Limit price for ${esc(s.question)}">
|
||
<button class="btn primary" data-live-ticket="${esc(s.market_id)}">Stage manual ticket</button>
|
||
</div>
|
||
</div>`).join("");
|
||
document.querySelectorAll("[data-live-ticket]").forEach(btn=>btn.onclick=()=>stageLiveMarketTicket(btn.dataset.liveTicket));
|
||
}
|
||
async function searchLiveMarkets(){
|
||
const q=($("liveSearchInput").value||"").trim();
|
||
if(!q)return toast("Enter a Polymarket search.");
|
||
LIVE_MARKET_OFFSET=0;LIVE_MARKET_QUERY=q;
|
||
await loadLiveMarketPage(true);
|
||
}
|
||
async function loadLiveMarketPage(reset=false,loadAll=false){
|
||
const q=LIVE_MARKET_QUERY||(($("liveSearchInput")&&$("liveSearchInput").value)||"").trim();
|
||
setStatus("searching Polymarket...",true);
|
||
try{
|
||
if(reset){LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;}
|
||
let loaded=0,rawCount=0,keepGoing=true;
|
||
const limit=100,maxPages=loadAll?MAX_ACTIVE_MARKET_PAGES:1;
|
||
let cursor=null;
|
||
for(let page=0;page<maxPages&&keepGoing;page++){
|
||
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",limit:String(limit),order:"volume24hr",ascending:"false"});
|
||
if(loadAll){if(cursor)params.set("after_cursor",cursor);}
|
||
else params.set("offset",String(LIVE_MARKET_OFFSET));
|
||
if(q)params.set("search",q);
|
||
const r=await fetch(`${GAMMA}/markets${loadAll?"/keyset":""}?${params}`);
|
||
if(!r.ok)throw new Error("market load failed");
|
||
const pageData=await r.json();
|
||
const raw=loadAll?(pageData.markets||[]):pageData;
|
||
rawCount+=raw.length;
|
||
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
|
||
LIVE_SEARCH_RESULTS=(LIVE_SEARCH_RESULTS||[]).concat(next);
|
||
LIVE_MARKET_OFFSET+=raw.length;
|
||
if(loadAll)cursor=pageData.next_cursor||null;
|
||
loaded+=next.length;
|
||
if(loadAll)setStatus(`loading active markets (${LIVE_SEARCH_RESULTS.length})…`,true);
|
||
if(raw.length<limit||(loadAll&&!cursor))keepGoing=false;
|
||
}
|
||
renderLiveMarkets();setStatus("up to date",false);
|
||
toast(loaded?`Loaded ${LIVE_SEARCH_RESULTS.length} active markets.`:rawCount?"No tradable binary markets found in this page.":"No more active markets found.");
|
||
}catch(e){setStatus("market load error",false);toast("Polymarket market load failed.");}
|
||
}
|
||
async function stageLiveMarketTicket(marketId){
|
||
const s=currentLiveMarkets().find(x=>String(x.market_id)===String(marketId));
|
||
if(!s)return toast("Market not found.");
|
||
const side=(document.querySelector(`[data-live-side="${marketId}"]`)||{}).value||"YES";
|
||
const amount=Number((document.querySelector(`[data-live-amount="${marketId}"]`)||{}).value||0);
|
||
const price=Number((document.querySelector(`[data-live-price="${marketId}"]`)||{}).value||0);
|
||
try{
|
||
await createTradeTicket({
|
||
user_id:PERSONAL_USER_ID,
|
||
agent_id:($("liveIntentAgent")&&$("liveIntentAgent").value)||localStorage.getItem(VIEW_KEY)||"value",
|
||
market_id:s.market_id,
|
||
question:s.question,
|
||
market_url:s.url,
|
||
side,
|
||
max_amount:amount,
|
||
limit_price:price,
|
||
rationale:`Manually staged from the personal Polymarket browser. ${s.rationale||""}`.trim(),
|
||
});
|
||
await loadTradeTickets();
|
||
toast("Manual ticket staged. Open Polymarket to place it yourself.");
|
||
}catch(e){toast(e.message||"Ticket staging failed.");}
|
||
}
|
||
async function saveRiskProfileToServer(s){
|
||
const permissions=JSON.parse(JSON.stringify(s.agents||{}));
|
||
Object.values(permissions).forEach(p=>{p.autoTrade=false;p.manualApprove=true;});
|
||
const body={
|
||
user_id:"local-readiness-user",
|
||
wallet_address:s.walletAddress,
|
||
jurisdiction:s.jurisdiction,
|
||
deposit_limit:s.depositLimit,
|
||
withdraw_reserve:s.withdrawReserve,
|
||
agent_permissions:permissions,
|
||
};
|
||
const r=await fetch("/api/risk-profile",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});
|
||
const d=await r.json();
|
||
if(!r.ok)throw new Error(d.error||"Risk profile save failed");
|
||
return d;
|
||
}
|
||
async function recordLiveConsent(){
|
||
const s=loadLiveState();
|
||
try{
|
||
const r=await fetch("/api/consent",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({
|
||
user_id:"local-readiness-user",
|
||
jurisdiction:s.jurisdiction,
|
||
accept_terms:$("liveAcceptTerms").checked,
|
||
accept_privacy:$("liveAcceptPrivacy").checked,
|
||
accept_risk:$("liveAcceptRisk").checked,
|
||
})});
|
||
const d=await r.json();
|
||
if(!r.ok)throw new Error(`${d.error}: ${(d.missing||[]).join(", ")}`);
|
||
saveLiveState(s,"Recorded live-money consent versions");
|
||
renderLiveMoneyTab();toast("Consent recorded.");
|
||
}catch(e){
|
||
toast(e.message||"Consent is not ready to record yet.");
|
||
}
|
||
}
|
||
function renderLiveMoneyTab(){
|
||
const root=$("liveChecklist");if(!root)return;
|
||
const s=loadLiveState(),done=LIVE_CHECKS.filter(([k])=>s.checks[k]).length;
|
||
$("liveReadyPct").textContent=`${done}/${LIVE_CHECKS.length} ready`;
|
||
root.innerHTML=LIVE_CHECKS.map(([k,label])=>`<label class="trade-card" style="display:flex;gap:10px;align-items:center">
|
||
<input type="checkbox" data-live-check="${k}" ${s.checks[k]?"checked":""}> <span>${esc(label)}</span>
|
||
</label>`).join("");
|
||
$("liveWalletAddress").value=s.walletAddress||"";
|
||
$("liveJurisdiction").value=s.jurisdiction||"";
|
||
$("liveDepositLimit").value=s.depositLimit||"";
|
||
$("liveWithdrawReserve").value=s.withdrawReserve||"";
|
||
const intentAgent=$("liveIntentAgent");
|
||
if(intentAgent)intentAgent.innerHTML=AGENTS.map(a=>`<option value="${a.id}">${a.emoji} ${esc(a.name)}</option>`).join("");
|
||
renderLiveBackendStatus();
|
||
$("liveAgentPermissions").innerHTML=AGENTS.map(a=>{const p=s.agents[a.id];
|
||
return `<div class="invest-agent" style="border-left:3px solid ${a.color};padding-left:12px">
|
||
<div class="lb-emoji">${a.emoji}</div>
|
||
<div><div class="invest-name">${esc(a.name)}</div><div class="invest-copy">${esc(a.blurb.slice(0,120))}${a.blurb.length>120?"...":""}</div></div>
|
||
<div class="trade-actions">
|
||
<label class="small muted"><input type="checkbox" data-live-agent="${a.id}" data-field="enabled" ${p.enabled?"checked":""}> enabled</label>
|
||
<label class="small muted"><input type="checkbox" data-live-agent="${a.id}" data-field="autoTrade" ${p.autoTrade?"checked":""} disabled> auto unavailable</label>
|
||
<input class="input" type="number" min="0" step="100" value="${p.maxAllocation||""}" placeholder="Max $" data-live-agent="${a.id}" data-field="maxAllocation">
|
||
<input class="input" type="number" min="1" max="100" step="1" value="${p.maxPositionPct||10}" placeholder="Max %" data-live-agent="${a.id}" data-field="maxPositionPct">
|
||
</div>
|
||
</div>`;
|
||
}).join("");
|
||
const audit=s.audit.slice(-10).reverse();
|
||
$("liveAuditTrail").innerHTML=audit.length?audit.map(x=>`<div class="log-item"><span class="log-date">${new Date(x.at).toLocaleString()}</span> — ${esc(x.detail)}</div>`).join(""):`<div class="empty">No readiness changes logged yet.</div>`;
|
||
renderPersonalCapital();
|
||
renderLiveMarkets();
|
||
document.querySelectorAll("[data-live-check]").forEach(el=>el.onchange=()=>{const next=loadLiveState();next.checks[el.dataset.liveCheck]=el.checked;saveLiveState(next,`${el.checked?"Completed":"Reopened"}: ${LIVE_CHECKS.find(([k])=>k===el.dataset.liveCheck)[1]}`);renderLiveMoneyTab();});
|
||
document.querySelectorAll("[data-live-agent]").forEach(el=>el.onchange=()=>{const next=loadLiveState(),p=next.agents[el.dataset.liveAgent],field=el.dataset.field;p[field]=el.type==="checkbox"?el.checked:Number(el.value||0);saveLiveState(next,`Updated ${agentById(el.dataset.liveAgent).name} permission: ${field}`);renderLiveMoneyTab();});
|
||
}
|
||
function esc(s){return String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));}
|
||
|
||
/* ---------- Tab routing ---------- */
|
||
const TABS=["overview","leaderboard","suggestions","portfolio","invest","paper","live","about"];
|
||
function showTab(name){
|
||
if(!TABS.includes(name))name="overview";
|
||
if(PERSONAL_MODE&&name==="invest")name="overview";
|
||
if(!PERSONAL_MODE&&name==="live")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 ---------- */
|
||
applyPersonalMode();
|
||
initProviderConfig();
|
||
refreshTradeEmailStatus();
|
||
$("runBtn").addEventListener("click",async()=>{
|
||
const btn=$("runBtn");btn.disabled=true;btn.textContent="Running…";
|
||
try{const r=await runDailyCycle();const b=board();toast(r.busy?`Cycle already running · ${b[0].c.emoji} ${b[0].c.name} leads`:r.skipped?`Already ran this minute · ${b[0].c.emoji} ${b[0].c.name} leads`:`Cycle done · ${r.suggestions} ideas · ${b[0].c.emoji} ${b[0].c.name} leads`);renderAll();}
|
||
catch(e){toast("Run failed: "+e.message);setStatus("error",false);}
|
||
btn.disabled=false;btn.textContent="Run cycle";
|
||
});
|
||
const b64Encode=(s)=>btoa(Array.from(new TextEncoder().encode(s),b=>String.fromCharCode(b)).join(""));
|
||
const b64Decode=(s)=>new TextDecoder().decode(Uint8Array.from(atob(s),c=>c.charCodeAt(0)));
|
||
function buildSyncCode(){
|
||
const data={version:1,exported_at:nowIso(),items:collectSyncItems()};
|
||
return "PMA1."+b64Encode(JSON.stringify(data));
|
||
}
|
||
async function readClipboardOrPrompt(){
|
||
try{if(navigator.clipboard&&navigator.clipboard.readText)return (await navigator.clipboard.readText()).trim();}
|
||
catch(e){}
|
||
return (prompt("Paste your Polymarket Arena sync code:")||"").trim();
|
||
}
|
||
$("copyStateBtn").addEventListener("click",async()=>{
|
||
const code=buildSyncCode();
|
||
try{await navigator.clipboard.writeText(code);toast("Sync code copied. Paste it on the other device with Import Sync.");}
|
||
catch(e){prompt("Copy this sync code:",code);}
|
||
});
|
||
$("importStateBtn").addEventListener("click",async()=>{
|
||
const code=await readClipboardOrPrompt();
|
||
if(!code)return;
|
||
try{
|
||
const payload=JSON.parse(b64Decode(code.replace(/^PMA1\./,"")));
|
||
if(!payload||payload.version!==1||!payload.items)throw new Error("Bad sync code");
|
||
if(!confirm("Import this sync state and replace this device's current local portfolio data?"))return;
|
||
applySyncItems(payload.items);
|
||
await pushCloudState(true);
|
||
toast("Sync imported. This device now matches the copied state.");
|
||
renderAll();showTab(location.hash.slice(1)||"portfolio");
|
||
}catch(e){toast("Import failed. Check that the sync code was copied fully.");}
|
||
});
|
||
$("createPaperAccountBtn").addEventListener("click",async()=>{
|
||
const name=($("paperAccountName").value||"Paper Trader").trim().slice(0,40);
|
||
const password=($("paperAccountPassword").value||"").trim();
|
||
try{
|
||
if(password){
|
||
await createCloudPaperAccount(name||"Paper Trader",password);
|
||
toast("Cloud paper account created.");
|
||
}else{
|
||
const store=loadPaperStore(),acct=defaultPaperAccount(name||"Paper Trader",password);
|
||
store.accounts[acct.id]=acct;store.activeId=acct.id;
|
||
savePaperStore(store);toast("Local paper account created.");
|
||
}
|
||
$("paperAccountName").value="";$("paperAccountPassword").value="";
|
||
renderPaperTab();
|
||
}catch(e){toast(e.message);}
|
||
});
|
||
$("copyAgentBtn").addEventListener("click",()=>{
|
||
const agentId=$("copyAgentSelect").value,book=cloneAgentBook(agentId),cfg=agentById(agentId);
|
||
if(!book)return toast("Agent book unavailable.");
|
||
if(!confirm(`Replace this paper account with ${cfg.name}'s current paper positions?`))return;
|
||
const store=loadPaperStore(),acct=activePaperAccount(store);
|
||
if(!requirePaperUnlock(acct,$("paperPasswordInput")&&$("paperPasswordInput").value))return;
|
||
acct.cash=book.cash;acct.positions=book.positions;acct.history=(acct.history||[]).concat(book.history);
|
||
savePaperStore(store);renderPaperTab();showTab("paper");toast(`Copied ${cfg.name} into paper account.`);
|
||
});
|
||
$("paperDepositBtn").addEventListener("click",()=>paperMoveCash("deposit",$("paperMoneyAmount").value,$("paperPasswordInput").value));
|
||
$("paperWithdrawBtn").addEventListener("click",()=>paperMoveCash("withdraw",$("paperMoneyAmount").value,$("paperPasswordInput").value));
|
||
$("buyAgentBtn").addEventListener("click",()=>paperBuyAgent($("buyAgentSelect").value,$("buyAgentAmount").value));
|
||
$("paperSearchBtn").addEventListener("click",()=>searchPaperMarkets());
|
||
$("paperAllBtn").addEventListener("click",()=>{PAPER_MARKET_QUERY="";PAPER_SEARCH_RESULTS=[];loadPaperMarketPage(true,true);});
|
||
$("paperMoreBtn").addEventListener("click",()=>loadPaperMarketPage(false));
|
||
$("paperClearSearchBtn").addEventListener("click",()=>{PAPER_SEARCH_RESULTS=null;PAPER_MARKET_OFFSET=0;PAPER_MARKET_QUERY="";renderPaperTab();});
|
||
$("paperLoginBtn").addEventListener("click",async()=>{
|
||
const acct=activePaperAccount();
|
||
const cloudName=($("paperAccountName").value||"").trim();
|
||
const topPassword=($("paperAccountPassword").value||"").trim();
|
||
try{
|
||
if(cloudName&&topPassword){
|
||
await loginCloudPaperAccount(cloudName,topPassword);
|
||
$("paperAccountName").value="";$("paperAccountPassword").value="";
|
||
renderPaperTab();toast("Cloud paper account loaded.");
|
||
}else if(acct.cloudBacked){
|
||
await loginCloudPaperAccount(acct.name,$("paperPasswordInput").value);
|
||
renderPaperTab();toast("Cloud paper account loaded.");
|
||
}else if(requirePaperUnlock(acct,$("paperPasswordInput").value)){renderPaperTab();toast("Logged in.");}
|
||
}catch(e){toast(e.message);}
|
||
});
|
||
$("paperSignOutBtn").addEventListener("click",()=>{
|
||
const acct=activePaperAccount();
|
||
sessionStorage.removeItem(unlockKey(acct.id));
|
||
sessionStorage.removeItem(paperSessionKey(acct.id));
|
||
renderPaperTab();toast("Signed out of paper account.");
|
||
});
|
||
$("saveLiveProfileBtn").addEventListener("click",async()=>{
|
||
const s=loadLiveState();
|
||
s.walletAddress=$("liveWalletAddress").value.trim();
|
||
s.jurisdiction=$("liveJurisdiction").value;
|
||
s.depositLimit=Number($("liveDepositLimit").value||0);
|
||
s.withdrawReserve=Number($("liveWithdrawReserve").value||0);
|
||
try{
|
||
await saveRiskProfileToServer(s);
|
||
saveLiveState(s,"Updated live-money wallet/funds profile and server risk profile");
|
||
renderLiveMoneyTab();toast("Live-money readiness profile saved to Neon.");
|
||
}catch(e){
|
||
saveLiveState(s,"Updated local live-money wallet/funds profile");
|
||
renderLiveMoneyTab();toast(e.message||"Saved locally, but server risk profile failed.");
|
||
}
|
||
});
|
||
$("dryRunOrderBtn").addEventListener("click",()=>dryRunLiveOrderIntent());
|
||
$("personalDepositBtn").addEventListener("click",()=>personalCapitalAction("DEPOSIT"));
|
||
$("personalWithdrawBtn").addEventListener("click",()=>personalCapitalAction("WITHDRAW"));
|
||
$("personalBuyAgentBtn").addEventListener("click",()=>personalCapitalAction("BUY_AGENT"));
|
||
$("personalSellAgentBtn").addEventListener("click",()=>personalCapitalAction("SELL_AGENT"));
|
||
$("liveSearchBtn").addEventListener("click",()=>searchLiveMarkets());
|
||
$("liveAllBtn").addEventListener("click",()=>{LIVE_MARKET_QUERY="";LIVE_SEARCH_RESULTS=[];loadLiveMarketPage(true,true);});
|
||
$("liveMoreBtn").addEventListener("click",()=>loadLiveMarketPage(false));
|
||
$("liveClearSearchBtn").addEventListener("click",()=>{LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;LIVE_MARKET_QUERY="";renderLiveMarkets();});
|
||
$("recordConsentBtn").addEventListener("click",()=>recordLiveConsent());
|
||
window.addEventListener("focus",()=>refreshFromCloudAndRender(true));
|
||
document.addEventListener("visibilitychange",()=>{
|
||
if(!document.hidden)refreshFromCloudAndRender(true);
|
||
});
|
||
async function autoRunDueCycle(){
|
||
if(document.hidden||CYCLE_RUNNING)return;
|
||
const st=loadState();
|
||
if(!st.seeded||st.last_cycle_hour===currentCycleHour())return;
|
||
try{await runDailyCycle();renderAll();toast("Live update ready.");}
|
||
catch(e){setStatus("error — click Run to retry",false);}
|
||
}
|
||
setInterval(()=>autoRunDueCycle(),RUN_INTERVAL_MS);
|
||
|
||
/* On load: first visit runs a 7-day backtest; afterwards the shared portfolio advances as often as the cycle key allows. */
|
||
(async function init(){
|
||
showTab(location.hash.slice(1)||"overview");
|
||
fetchLiveBackendStatus();
|
||
const cloud=await loadAuthoritativeCloudState();
|
||
let st=loadState();
|
||
if(cloud.loaded){
|
||
toast("Cloud state loaded for this device.");
|
||
}else if(cloud.ok&&st.seeded){
|
||
pushCloudState();
|
||
}else if(!cloud.ok){
|
||
setStatus("cloud sync unavailable",false);
|
||
}
|
||
renderAll();
|
||
const btn=$("runBtn");
|
||
if(!cloud.ok){
|
||
toast("Cloud sync is unavailable. Reconnect before running a cycle.");
|
||
return;
|
||
}
|
||
if(!st.seeded){
|
||
btn.disabled=true;btn.textContent="Backtesting…";
|
||
try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();st.last_cycle_hour=currentCycleHour();saveState(st);if(!(await pushCloudState()))throw new Error("Cloud sync unavailable");renderAll();
|
||
toast("7-day backtest complete — live tracking begins today.");}
|
||
catch(e){setStatus("backtest error — click Run",false);}
|
||
btn.disabled=false;btn.textContent="Run cycle";
|
||
}else if(st.last_cycle_hour!==currentCycleHour()){
|
||
btn.disabled=true;btn.textContent="Running…";
|
||
try{await runDailyCycle();renderAll();toast("Live 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>
|