Improve portfolio UX and add stop loss

This commit is contained in:
Theodore Song
2026-07-05 16:58:46 -04:00
parent d16e8527ef
commit f47cf392d1
2 changed files with 146 additions and 26 deletions
+6 -6
View File
@@ -1,14 +1,14 @@
# 📊 Polymarket Analyst — standalone website # 📊 Polymarket Analyst — standalone website
A **single-file website**. Everything (fetching live Polymarket data, the A **single-file website**. Everything (fetching live Polymarket data, the
analysis engine, and the AI paper-trading agent) runs in your browser. The analysis engine, and the ten AI paper-trading agents) runs in your browser. The
paper portfolio is saved in your browser's local storage. No server, no keys. paper portfolio is saved in your browser's local storage. No server, no keys.
## Just look at it now ## Just look at it now
Double-click **`index.html`** — it opens in your browser and works immediately. Double-click **`index.html`** — it opens in your browser and works immediately.
On first open it fetches live markets, generates today's suggestions, and the On first open it fetches live markets, generates today's suggestions, and the
agent makes its first trades. It auto-refreshes once per day; the **Run daily agents run a seven-day backtest, then begin live tracking. It auto-refreshes
cycle now** button forces a refresh anytime. once per day; the **Run cycle** button forces a refresh anytime.
## Put it online (free) so you can reach it from any device ## Put it online (free) so you can reach it from any device
@@ -22,11 +22,11 @@ Pick one — all give you a public URL:
**Option B — GitHub Pages** **Option B — GitHub Pages**
1. Create a new GitHub repo and upload `index.html`. 1. Create a new GitHub repo and upload `index.html`.
2. Repo → Settings → Pages → Branch: `main`, folder: `/root` → Save. 2. Repo → Settings → Pages → Branch: `main`, folder: `/root` → Save.
3. Your site appears at `https://<you>.github.io/<repo>/`. 3. Your site appears at `https://theodore-song.github.io/<repo>/`.
**Option C — Vercel** **Option C — Vercel**
1. <https://vercel.com> → Add New → Project → import the folder (or `vercel` 1. <https://vercel.com> → Add New → Project → import this GitHub repo under the
CLI in this folder) → Deploy. `theodore_song` Vercel account (or use the `vercel` CLI in this folder) → Deploy.
## Notes ## Notes
- Paper trading only — no real money, nothing places real orders. - Paper trading only — no real money, nothing places real orders.
+140 -20
View File
@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Polymarket Analyst — 5 AI Agents Compete to Trade Prediction Markets</title> <title>Polymarket Analyst — 10 AI Agents Compete to Trade Prediction Markets</title>
<meta name="description" content="Daily AI analysis of Polymarket with five competing autonomous paper-trading agents and a live leaderboard. Runs entirely in your browser." /> <meta name="description" content="Daily AI analysis of Polymarket with ten competing autonomous paper-trading agents, whale-copy portfolios, and a live leaderboard. Runs entirely in your browser." />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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 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">
@@ -45,19 +45,20 @@ h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif}
.topnav{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:18px;flex-wrap:wrap; .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; 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)} background:rgba(10,15,28,.72);backdrop-filter:blur(16px);box-shadow:var(--shadow)}
.brand{display:flex;align-items:center;gap:11px} .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; .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)} 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-name{font-size:18px;font-weight:700;letter-spacing:-.2px;line-height:1}
.brand-sub{font-size:11px;color:var(--muted);margin-top:2px} .brand-sub{font-size:11px;color:var(--muted);margin-top:2px}
.tabs{display:flex;gap:4px;background:rgba(255,255,255,.04);padding:4px;border-radius:12px;border:1px solid var(--border)} .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; .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} 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:hover{color:var(--text)}
.tab.active{color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent2));box-shadow:0 6px 18px -8px var(--accent)} .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} .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); .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)} 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{width:8px;height:8px;border-radius:50%;background:var(--muted)}
.dot.live{background:var(--green);animation:pulse 1.6s infinite} .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)}} @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)}}
@@ -134,6 +135,22 @@ h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif}
.segbtn:hover{color:var(--text);border-color:var(--border-strong)} .segbtn:hover{color:var(--text);border-color:var(--border-strong)}
.segbtn.active{color:#fff;border-color:transparent} .segbtn.active{color:#fff;border-color:transparent}
.agent-brief{display:grid;grid-template-columns:1.2fr repeat(4,minmax(110px,.55fr));gap:14px;margin-bottom:18px}
.brief-main{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;backdrop-filter:blur(14px)}
.brief-title{display:flex;align-items:center;gap:10px;font-family:'Space Grotesk';font-weight:700;font-size:18px;margin-bottom:7px}
.brief-main p{margin:0;color:#cdd6e8;font-size:13.5px}
.brief-mini{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:14px;padding:14px}
.brief-mini .k{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
.brief-mini .v{margin-top:5px;font-size:14px;font-weight:700}
.card-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.select{appearance:none;border:1px solid var(--border);border-radius:9px;background:rgba(255,255,255,.05);color:var(--text);
font:600 12.5px 'Inter',sans-serif;padding:7px 28px 7px 10px;cursor:pointer}
.select:focus{outline:1px solid var(--accent)}
.loading-empty{display:grid;gap:10px;place-items:center;padding:28px 12px;text-align:center}
.loading-ring{width:34px;height:34px;border-radius:50%;border:3px solid rgba(255,255,255,.12);border-top-color:var(--accent);animation:spin .9s linear infinite}
.empty-copy{max-width:420px;color:var(--muted);font-size:13.5px}
@keyframes spin{to{transform:rotate(360deg)}}
/* ---------- suggestions ---------- */ /* ---------- suggestions ---------- */
.filterbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px} .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{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}
@@ -172,9 +189,28 @@ a.market-link:hover{text-decoration:underline}
.badge{font-weight:700;font-size:10px;padding:2px 7px;border-radius:5px;margin-right:7px} .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.OPEN{background:rgba(124,140,255,.2);color:var(--accent)}
.badge.CLOSE{background:rgba(251,191,36,.2);color:var(--yellow)} .badge.CLOSE{background:rgba(251,191,36,.2);color:var(--yellow)}
.badge.STOP{background:rgba(251,113,133,.2);color:var(--red)}
.log-date{color:var(--muted)} .log-date{color:var(--muted)}
.empty{color:var(--muted);font-size:13.5px;padding:14px 0;text-align:center} .empty{color:var(--muted);font-size:13.5px;padding:14px 0;text-align:center}
@media (max-width:700px){
.app{padding:0 10px 42px;max-width:100%;overflow:hidden}
.topnav{display:grid;grid-template-columns:1fr;gap:12px;padding:14px;margin:10px 0 8px;border-radius:14px}
.brand-sub{white-space:normal}
.tabs{width:100%;padding-bottom:5px}
.tab{padding:8px 12px}
.nav-right{width:100%;margin-left:0;display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px}
#statusText{max-width:none;white-space:normal}
.status{align-items:flex-start}
.btn{padding:9px 11px}
.section-title{font-size:19px}
.stats{grid-template-columns:1fr;gap:12px}
.agent-brief{grid-template-columns:1fr}
.pos{align-items:flex-start;gap:12px}
.pos .pq{max-width:none}
.right{min-width:92px}
}
/* ---------- about ---------- */ /* ---------- about ---------- */
.about-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;margin-bottom:8px} .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{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:20px;backdrop-filter:blur(14px)}
@@ -292,9 +328,16 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<section class="tabpanel" data-tab="portfolio"> <section class="tabpanel" data-tab="portfolio">
<div class="section-title">🤖 Agent Portfolios</div> <div class="section-title">🤖 Agent Portfolios</div>
<div class="seg" id="agentSel"></div> <div class="seg" id="agentSel"></div>
<div class="agent-brief" id="agentBrief"></div>
<div class="stats" id="statsPf"></div> <div class="stats" id="statsPf"></div>
<div class="grid2"> <div class="grid2">
<div class="card"><div class="card-h"><h3>Open Positions</h3></div><div id="positions"></div></div> <div class="card"><div class="card-h"><h3>Open Positions</h3><div class="card-tools"><select class="select" id="positionSort" aria-label="Sort open positions">
<option value="pnl_desc">Best P&amp;L</option>
<option value="pnl_asc">Worst P&amp;L</option>
<option value="value_desc">Largest Value</option>
<option value="newest">Newest</option>
<option value="category">Category</option>
</select></div></div><div id="positions" aria-live="polite"></div></div>
<div class="card"><div class="card-h"><h3>Activity Log</h3></div><div id="history"></div></div> <div class="card"><div class="card-h"><h3>Activity Log</h3></div><div id="history"></div></div>
</div> </div>
</section> </section>
@@ -310,7 +353,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<li><b>Fetch</b> — page through hundreds of live markets and tag each by category.</li> <li><b>Fetch</b> — page through hundreds of live markets and tag each by category.</li>
<li><b>Score</b> — rank every market 0100 by conviction and estimate its edge vs. a fair-value model.</li> <li><b>Score</b> — rank every market 0100 by conviction and estimate its edge vs. a fair-value model.</li>
<li><b>Suggest</b> — surface the strongest picks per category with a plain-English rationale.</li> <li><b>Suggest</b> — surface the strongest picks per category with a plain-English rationale.</li>
<li><b>Compete</b>all five agents trade the same suggestions their own way, then snapshot equity for the leaderboard.</li> <li><b>Compete</b>the strategy agents trade the same suggestions their own way, stop-loss weak positions, then every agent snapshots equity for the leaderboard.</li>
</ol> </ol>
</div> </div>
<div class="disclaimer">⚠️ <b>Paper trading only — not financial advice.</b> Nothing here places real orders or moves money. The analysis is a transparent heuristic. All portfolios live in this browser's local storage (each device keeps its own).</div> <div class="disclaimer">⚠️ <b>Paper trading only — not financial advice.</b> Nothing here places real orders or moves money. The analysis is a transparent heuristic. All portfolios live in this browser's local storage (each device keeps its own).</div>
@@ -326,16 +369,18 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
"use strict"; "use strict";
/* ============================================================ /* ============================================================
Polymarket Arena — fully client-side. Polymarket Arena — fully client-side.
Fetches live markets, scores them, and runs FIVE competing Fetches live markets, scores them, and runs ten competing
paper-trading agents. State persists in localStorage. paper-trading agents. State persists in localStorage.
============================================================ */ ============================================================ */
const GAMMA = "https://gamma-api.polymarket.com"; const GAMMA = "https://gamma-api.polymarket.com";
const STARTING_BALANCE = 10000; const STARTING_BALANCE = 10000;
const STOP_LOSS_PCT = 0.18;
const AGENTS_KEY = "pma_agents_v2"; const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v2"; const SUG_KEY = "pma_suggestions_v2";
const FOCUS_KEY = "pma_focus_v1"; const FOCUS_KEY = "pma_focus_v1";
const VIEW_KEY = "pma_view_v1"; const VIEW_KEY = "pma_view_v1";
const PF_SORT_KEY = "pma_portfolio_sort_v1";
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"]; const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff", const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff",
"Pop Culture":"#c77dff",Other:"#94a1bb",All:"#7c8cff",Copy:"#22d3ee"}; "Pop Culture":"#c77dff",Other:"#94a1bb",All:"#7c8cff",Copy:"#22d3ee"};
@@ -346,7 +391,7 @@ const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All"; const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
const setFocus = (c) => localStorage.setItem(FOCUS_KEY, c); const setFocus = (c) => localStorage.setItem(FOCUS_KEY, c);
/* ---------- the five competing agents ---------- /* ---------- the ten competing agents ----------
Each shares the same daily suggestions but ranks/sizes them differently. */ Each shares the same daily suggestions but ranks/sizes them differently. */
const AGENTS = [ const AGENTS = [
{id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy", {id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy",
@@ -490,12 +535,15 @@ function generateSuggestions(markets,perCategory=12){
} }
/* ---------- Multi-agent store ---------- */ /* ---------- Multi-agent store ---------- */
function defaultPortfolio(){return {cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],closed:[],history:[],snapshots:[]};} function defaultPortfolio(){return {cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],closed:[],history:[],snapshots:[],stopped:{}};}
function defaultState(){const agents={};AGENTS.forEach(a=>agents[a.id]=defaultPortfolio());return {date:null,last_run:null,agents,whales:{},copycatLeader:null,seeded:false};} function defaultState(){const agents={};AGENTS.forEach(a=>agents[a.id]=defaultPortfolio());return {date:null,last_run:null,agents,whales:{},copycatLeader:null,seeded:false};}
function loadState(){ function loadState(){
let st; try{st=JSON.parse(localStorage.getItem(AGENTS_KEY));}catch(e){st=null;} let st; try{st=JSON.parse(localStorage.getItem(AGENTS_KEY));}catch(e){st=null;}
if(!st||!st.agents)st=defaultState(); if(!st||!st.agents)st=defaultState();
AGENTS.forEach(a=>{if(!st.agents[a.id])st.agents[a.id]=defaultPortfolio();}); AGENTS.forEach(a=>{
if(!st.agents[a.id])st.agents[a.id]=defaultPortfolio();
if(!st.agents[a.id].stopped)st.agents[a.id].stopped={};
});
if(!st.whales)st.whales={}; if(!st.whales)st.whales={};
return st; return st;
} }
@@ -506,6 +554,26 @@ function saveSuggestions(sugs){const p={date:todayStr(),generated_at:nowIso(),su
/* ---------- Agent mechanics ---------- */ /* ---------- Agent mechanics ---------- */
const equity=(p)=>p.cash+p.positions.reduce((s,x)=>s+x.shares*x.current_price,0); const equity=(p)=>p.cash+p.positions.reduce((s,x)=>s+x.shares*x.current_price,0);
const hasPosition=(p,id)=>p.positions.some(x=>x.market_id===id); const hasPosition=(p,id)=>p.positions.some(x=>x.market_id===id);
const stopKey=(posOrId)=>typeof posOrId==="string"?posOrId:String(posOrId.asset||posOrId.market_id||"");
const hasStoppedToday=(p,posOrId)=>p.stopped&&p.stopped[stopKey(posOrId)]===logDay();
function rememberStop(p,pos){
if(!p.stopped)p.stopped={};
const key=stopKey(pos);
if(key)p.stopped[key]=logDay();
}
function shouldStopLoss(pos){
if(!pos.cost||pos.cost<=0)return false;
return (pos.value||0)<=pos.cost*(1-STOP_LOSS_PCT);
}
function stopOutPosition(p,pos){
const proceeds=+(pos.shares*pos.current_price).toFixed(2);
p.cash=+(p.cash+proceeds).toFixed(2);
pos.exit_price=pos.current_price;pos.closed_at=nowIso();
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
p.closed.push(pos);rememberStop(p,pos);
p.history.push({date:logDay(),action:"STOP",question:pos.question,side:pos.side,
detail:`Stop-loss sold '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
}
function markToMarket(p,priceMap){ function markToMarket(p,priceMap){
const stillOpen=[]; const stillOpen=[];
for(const pos of p.positions){ for(const pos of p.positions){
@@ -521,13 +589,15 @@ function markToMarket(p,priceMap){
} }
const price=pos.side==="YES"?fresh.yes_price:fresh.no_price; 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.current_price=+price.toFixed(4);pos.value=+(pos.shares*price).toFixed(2);
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);stillOpen.push(pos); pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
if(shouldStopLoss(pos)){stopOutPosition(p,pos);continue;}
stillOpen.push(pos);
} }
p.positions=stillOpen; p.positions=stillOpen;
} }
function openPositions(p,cfg,rankedSugs,focus){ function openPositions(p,cfg,rankedSugs,focus){
const cands=rankedSugs.filter(s=>(s.side==="YES"||s.side==="NO")&&s.conviction>=cfg.minConv const cands=rankedSugs.filter(s=>(s.side==="YES"||s.side==="NO")&&s.conviction>=cfg.minConv
&&!hasPosition(p,s.market_id)&&(focus==="All"||!focus||s.category===focus)); &&!hasPosition(p,s.market_id)&&!hasStoppedToday(p,s.market_id)&&(focus==="All"||!focus||s.category===focus));
let opened=0; let opened=0;
for(const s of cands){ for(const s of cands){
if(opened>=cfg.maxNew)break; if(opened>=cfg.maxNew)break;
@@ -603,7 +673,9 @@ async function runWhaleAgent(p,wallet){
const held=new Set(tps.map(t=>t.asset)); const held=new Set(tps.map(t=>t.asset));
const keep=[]; const keep=[];
for(const pos of p.positions){ for(const pos of p.positions){
if(!held.has(pos.asset)){ if(shouldStopLoss(pos)){
stopOutPosition(p,pos);
}else if(!held.has(pos.asset)){
const proceeds=pos.shares*pos.current_price;p.cash+=proceeds; const proceeds=pos.shares*pos.current_price;p.cash+=proceeds;
pos.exit_price=pos.current_price;pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);p.closed.push(pos); pos.exit_price=pos.current_price;pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);p.closed.push(pos);
p.history.push({date:logDay(),action:"CLOSE",question:pos.question,side:pos.side, p.history.push({date:logDay(),action:"CLOSE",question:pos.question,side:pos.side,
@@ -614,7 +686,7 @@ async function runWhaleAgent(p,wallet){
const targets=[...tps].sort((a,b)=>b.value-a.value).slice(0,12); // copy their biggest 12 bets const targets=[...tps].sort((a,b)=>b.value-a.value).slice(0,12); // copy their biggest 12 bets
const book=targets.reduce((s,t)=>s+t.value,0)||1; const book=targets.reduce((s,t)=>s+t.value,0)||1;
for(const t of targets){ for(const t of targets){
if(hasAsset(p,t.asset))continue; if(hasAsset(p,t.asset)||hasStoppedToday(p,t.asset))continue;
const eq=equity(p),investable=p.cash-eq*0.05; if(investable<=50)break; const eq=equity(p),investable=p.cash-eq*0.05; if(investable<=50)break;
const weight=t.value/book; const weight=t.value/book;
const stake=Math.min(eq*Math.min(0.34,weight),investable); const stake=Math.min(eq*Math.min(0.34,weight),investable);
@@ -646,7 +718,7 @@ function runCopycat(p,leader,priceMap){
p.positions=keep; p.positions=keep;
const book=leader.positions.reduce((s,x)=>s+(x.value||x.cost),0)||1; const book=leader.positions.reduce((s,x)=>s+(x.value||x.cost),0)||1;
for(const lp of [...leader.positions].sort((a,b)=>(b.value||b.cost)-(a.value||a.cost))){ for(const lp of [...leader.positions].sort((a,b)=>(b.value||b.cost)-(a.value||a.cost))){
if(hasPosition(p,lp.market_id))continue; if(hasPosition(p,lp.market_id)||hasStoppedToday(p,lp.market_id))continue;
const eq=equity(p),investable=p.cash-eq*0.05; if(investable<=50)break; const eq=equity(p),investable=p.cash-eq*0.05; if(investable<=50)break;
const weight=(lp.value||lp.cost)/book; const weight=(lp.value||lp.cost)/book;
const stake=Math.min(eq*Math.min(0.2,weight),investable); const stake=Math.min(eq*Math.min(0.2,weight),investable);
@@ -882,6 +954,32 @@ function agentBlurb(cfg,st){
if(cfg.kind==="copycat"){const L=AGENTS.find(a=>a.id===st.copycatLeader);return `🐒 Copying <b>${L?esc(L.name):"the leader"}</b>`;} if(cfg.kind==="copycat"){const L=AGENTS.find(a=>a.id===st.copycatLeader);return `🐒 Copying <b>${L?esc(L.name):"the leader"}</b>`;}
return cfg.blurb; return cfg.blurb;
} }
function agentPlainBlurb(cfg,st){
return agentBlurb(cfg,st).replace(/<[^>]*>/g,"");
}
function bestAndWorst(p){
const open=p.positions||[];
if(!open.length)return {best:"No open trades yet",worst:"No drawdowns yet"};
const sorted=[...open].sort((a,b)=>(b.unrealized_pnl||0)-(a.unrealized_pnl||0));
const label=x=>`${x.question.slice(0,34)}${x.question.length>34?"...":""} (${(x.unrealized_pnl||0)>=0?"+":""}${fmtUSD(x.unrealized_pnl||0)})`;
return {best:label(sorted[0]),worst:label(sorted[sorted.length-1])};
}
function renderAgentBrief(cfg,p,st){
const root=$("agentBrief"); if(!root)return;
const bw=bestAndWorst(p);
const risk=cfg.kind==="whale"?"Mirror risk":cfg.kind==="copycat"?"Adaptive":cfg.maxFrac>=0.12?"Higher risk":cfg.maxFrac<=0.05?"Lower risk":"Balanced";
const cadence=cfg.kind==="whale"?"Copies biggest live holdings":cfg.kind==="copycat"?"Follows current strategy leader":`${cfg.maxNew||0} new trades max`;
const thesis=agentPlainBlurb(cfg,st);
root.innerHTML=`
<div class="brief-main" style="border-top:3px solid ${cfg.color}">
<div class="brief-title"><span>${cfg.emoji}</span><span>${cfg.name}</span></div>
<p>${esc(thesis)}</p>
</div>
<div class="brief-mini"><div class="k">Style</div><div class="v">${esc(risk)}</div></div>
<div class="brief-mini"><div class="k">Trade Rule</div><div class="v">${esc(cadence)}</div></div>
<div class="brief-mini"><div class="k">Stop Loss</div><div class="v">${Math.round(STOP_LOSS_PCT*100)}% drawdown</div></div>
<div class="brief-mini"><div class="k">Best / Worst</div><div class="v"><span class="pos-val">${esc(bw.best)}</span><br><span class="neg-val">${esc(bw.worst)}</span></div></div>`;
}
function renderLeaderboard(){ function renderLeaderboard(){
const b=board(); const st=loadState(); const b=board(); const st=loadState();
$("lbSub").textContent="· "+b[0].c.name+" leads"; $("lbSub").textContent="· "+b[0].c.name+" leads";
@@ -966,12 +1064,14 @@ function renderSuggestions(){
function renderPortfolioTab(){ function renderPortfolioTab(){
const st=loadState(); const st=loadState();
let viewId=localStorage.getItem(VIEW_KEY); if(!AGENTS.some(a=>a.id===viewId))viewId="value"; let viewId=localStorage.getItem(VIEW_KEY); if(!AGENTS.some(a=>a.id===viewId))viewId="value";
const cfg=agentById(viewId);
$("agentSel").innerHTML=AGENTS.map(a=>{ $("agentSel").innerHTML=AGENTS.map(a=>{
const active=a.id===viewId; const active=a.id===viewId;
const style=active?`background:linear-gradient(135deg,${a.color},${a.color});border-color:transparent`:""; 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(""); 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();})); 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; const p=st.agents[viewId]||defaultPortfolio();const eq=equity(p),pnl=eq-p.starting_balance;
renderAgentBrief(cfg,p,st);
const stats=[ const stats=[
{ic:"💰",label:"Equity",value:fmtUSD(eq)}, {ic:"💰",label:"Equity",value:fmtUSD(eq)},
{ic:"💵",label:"Cash",value:fmtUSD(p.cash)}, {ic:"💵",label:"Cash",value:fmtUSD(p.cash)},
@@ -980,13 +1080,33 @@ function renderPortfolioTab(){
{ic:"📂",label:"Open",value:p.positions.length}, {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(""); $("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); renderPositions(p.positions);
renderHistory((p.history||[]).slice(-50).reverse()); renderHistory((p.history||[]).slice(-50).reverse());
} }
function renderPositions(positions){ function renderPositions(positions){
const root=$("positions");if(!root)return; const root=$("positions");if(!root)return;
if(!positions.length){root.innerHTML=`<div class="empty">No open positions.</div>`;return;} if(!positions.length){
root.innerHTML=positions.map(p=>{const pnl=p.unrealized_pnl||0,col=catColor(p.category); const st=loadState();
const isBacktesting=!st.seeded&&(($("statusText")&&$("statusText").textContent)||"").includes("backtest");
root.innerHTML=isBacktesting
? `<div class="loading-empty"><div class="loading-ring"></div><div><b>Building agent portfolios...</b><div class="empty-copy">The app is replaying the past week, finding top traders, and marking positions to real Polymarket prices. This fills in automatically when the backtest finishes.</div></div></div>`
: `<div class="empty">No open positions yet. Run a cycle to let this agent shop today's suggestions.</div>`;
return;
}
const sort=localStorage.getItem(PF_SORT_KEY)||"pnl_desc";
const ordered=[...positions].sort((a,b)=>{
if(sort==="pnl_asc")return (a.unrealized_pnl||0)-(b.unrealized_pnl||0);
if(sort==="value_desc")return (b.value||0)-(a.value||0);
if(sort==="newest")return String(b.opened_at||"").localeCompare(String(a.opened_at||""));
if(sort==="category")return String(a.category||"").localeCompare(String(b.category||""))||((b.unrealized_pnl||0)-(a.unrealized_pnl||0));
return (b.unrealized_pnl||0)-(a.unrealized_pnl||0);
});
root.innerHTML=ordered.map(p=>{const pnl=p.unrealized_pnl||0,col=catColor(p.category);
return `<div class="pos"><div> return `<div class="pos"><div>
<div class="pq">${esc(p.question)}</div> <div class="pq">${esc(p.question)}</div>
<div class="sub">${p.category?`<span class="cat-badge" style="background:${col}22;color:${col}">${esc(p.category)}</span> `:""}${p.shares} ${p.side} @ ${Math.round(p.entry_price*100)}¢ → ${Math.round(p.current_price*100)}¢</div> <div 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>
@@ -1025,7 +1145,7 @@ $("runBtn").addEventListener("click",async()=>{
btn.disabled=false;btn.textContent="Run cycle"; btn.disabled=false;btn.textContent="Run cycle";
}); });
$("resetBtn").addEventListener("click",()=>{ $("resetBtn").addEventListener("click",()=>{
if(!confirm("Reset all five agents to their $10,000 starting balance?"))return; if(!confirm("Reset all ten agents to their $10,000 starting balance?"))return;
saveState(defaultState());localStorage.removeItem(SUG_KEY);toast("All agents reset.");renderAll(); saveState(defaultState());localStorage.removeItem(SUG_KEY);toast("All agents reset.");renderAll();
}); });