Compare commits

..

6 Commits

Author SHA1 Message Date
Theodore Song c7ce683db9 Read latest versioned cloud state 2026-07-27 15:45:51 -04:00
Theodore Song d04ed59088 Preserve suggestion quality fields 2026-07-27 15:40:26 -04:00
Theodore Song 82a1e0c831 Enable selective buys for agents 2026-07-27 15:37:55 -04:00
Theodore Song 67beab0144 Make top 5000 analysis visible 2026-07-27 15:27:48 -04:00
Theodore Song 3c39cfe9f3 Force visible top 5000 build 2026-07-26 21:13:02 -04:00
Theodore Song 744387199e Analyze top 5000 active markets 2026-07-26 20:10:39 -04:00
2 changed files with 118 additions and 23 deletions
+61 -2
View File
@@ -1,6 +1,7 @@
import { get, put } from "@vercel/blob";
import { get, list, put } from "@vercel/blob";
const STATE_PATH = process.env.PMA_STATE_PATH || "shared/state.json";
const STATE_VERSION_PREFIX = process.env.PMA_STATE_VERSION_PREFIX || "shared/state-versions/";
const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v5";
const PAPER_KEY = "pma_paper_accounts_v1";
@@ -9,12 +10,58 @@ const AGENT_IDS = ["value", "momentum", "favorite", "longshot", "diversifier", "
const LIMITS = { closed: 80, history: 160, snapshots: 240, suggestions: 900, paperHistory: 120, paperSnapshots: 120, audit: 120 };
async function readJsonBlob() {
const latest = await latestVersionedStateBlob();
if (latest) return latest;
const blob = await get(STATE_PATH, { access: "private" });
if (!blob || blob.statusCode !== 200 || !blob.stream) return null;
const text = await new Response(blob.stream).text();
return text ? JSON.parse(text) : null;
}
async function latestVersionedStateBlob() {
let cursor;
let newest = null;
do {
const page = await list({ prefix: STATE_VERSION_PREFIX, limit: 1000, cursor });
for (const blob of page.blobs || []) {
if (!newest || new Date(blob.uploadedAt).getTime() > new Date(newest.uploadedAt).getTime()) newest = blob;
}
cursor = page.cursor;
} while (cursor);
if (!newest) return null;
const blob = await get(newest.url, {
access: "private",
headers: { "cache-control": "no-cache" },
});
if (!blob || blob.statusCode !== 200 || !blob.stream) return null;
const text = await new Response(blob.stream).text();
return text ? JSON.parse(text) : null;
}
function cycleVersion(cycle = "") {
const m = String(cycle).match(/\|v(\d+)$/);
return m ? Number(m[1]) : 0;
}
function openPositionCount(st) {
if (!st || !st.agents) return 0;
return AGENT_IDS.reduce((sum, id) => sum + (Array.isArray(st.agents[id]?.positions) ? st.agents[id].positions.length : 0), 0);
}
function shouldRejectStaleAgentWrite(currentAgents, incomingAgents) {
if (!currentAgents || !incomingAgents) return false;
const currentCycle = currentAgents.last_cycle_hour || "";
const incomingCycle = incomingAgents.last_cycle_hour || "";
if (!currentCycle || !incomingCycle) return false;
const currentVersion = cycleVersion(currentCycle);
const incomingVersion = cycleVersion(incomingCycle);
if (incomingVersion < currentVersion) return true;
if (incomingVersion === currentVersion && incomingCycle < currentCycle) return true;
const currentOpen = openPositionCount(currentAgents);
const incomingOpen = openPositionCount(incomingAgents);
return incomingVersion === currentVersion && incomingCycle === currentCycle && currentOpen > 0 && incomingOpen === 0;
}
function agentStateFromItems(items) {
if (!items || !items[AGENTS_KEY]) return null;
try {
@@ -51,6 +98,8 @@ function compactSuggestion(s) {
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,
@@ -130,12 +179,22 @@ export default async function handler(req, res) {
return conflictResponse(res, "This cycle already has a cloud result", current);
}
}
if (!body.force && shouldRejectStaleAgentWrite(currentAgents, incomingAgents)) {
return conflictResponse(res, "Incoming state would replace newer active positions with stale cash-only data", current);
}
const state = { version: 1, updated_at: new Date().toISOString(), items: compactItems(body.items) };
const versionedPath = `${STATE_VERSION_PREFIX}${Date.now()}-${Math.random().toString(36).slice(2)}.json`;
await put(versionedPath, JSON.stringify(state), {
access: "private",
allowOverwrite: false,
contentType: "application/json",
cacheControlMaxAge: 0,
});
await put(STATE_PATH, JSON.stringify(state), {
access: "private",
allowOverwrite: true,
contentType: "application/json",
cacheControlMaxAge: 60,
cacheControlMaxAge: 0,
});
return res.status(200).json({ ok: true, state });
}
+57 -21
View File
@@ -53,6 +53,7 @@ h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif}
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}
@@ -78,6 +79,8 @@ body:not(.personal-mode) [data-tab="live"]{display:none!important}
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}
.live-build-banner{display:block;margin:0 0 14px;padding:11px 14px;border-radius:14px;border:1px solid rgba(124,140,255,.30);background:linear-gradient(135deg,rgba(124,140,255,.14),rgba(56,210,230,.08));color:#dbeafe;font-size:12.5px}
.live-build-banner b{color:#fff}
/* ---------- hero ---------- */
.hero{position:relative;border:1px solid var(--border);border-radius:22px;padding:34px 32px;margin:14px 0 22px;
@@ -337,7 +340,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<nav class="topnav">
<div class="brand">
<div class="logo">🏆</div>
<div><div class="brand-name">Polymarket Arena</div><div class="brand-sub">10 agents · strategies, copycats &amp; whales</div></div>
<div><div class="brand-name">Polymarket Arena</div><div class="brand-sub">10 agents · strategies, copycats &amp; whales</div><div class="build-badge">Top 5,000 analysis · v18</div></div>
</div>
<div class="tabs" id="tabs">
<button class="tab" data-tab="overview">Overview</button>
@@ -359,6 +362,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<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>
<div class="live-build-banner"><b>Build v18 active:</b> agents rank the live Polymarket universe, analyze the top 5,000 by activity/liquidity/gap, and use a selective BUY fallback so core agents do not sit entirely in cash.</div>
<!-- ============ OVERVIEW ============ -->
<section class="tabpanel" data-tab="overview">
@@ -373,12 +377,12 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<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>
<p>Each cycle ranks the active Polymarket universe, then deeply analyzes only the top 5,000 markets by activity, liquidity, timing, and estimated price gap before agents trade.</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>
<span class="hbadge">Top 5,000 market analysis</span>
<span class="hbadge">Ranked by activity + gap</span>
<span class="hbadge">Live Polymarket data</span>
</div>
</div>
@@ -739,7 +743,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
</section>
<footer>
Build hold-fix · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
Build top-5000-analysis-v18 · 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>
@@ -766,7 +770,7 @@ 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 = 14;
const SUGGESTION_ENGINE_VERSION = 18;
const FOCUS_KEY = "pma_focus_v1";
const VIEW_KEY = "pma_view_v1";
const PF_SORT_KEY = "pma_portfolio_sort_v1";
@@ -991,10 +995,12 @@ const MIN_SCOUT_VOLUME=1500;
const MIN_SCOUT_LIQUIDITY=150;
const MIN_SCOUT_EDGE=0.006;
const MIN_ENTRY_EDGE=0.032;
const MIN_SELECTIVE_ENTRY_EDGE=0.020;
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;
@@ -1057,7 +1063,30 @@ function fairValue(m){const p=m.yes_price,mom=momentumSignal(m),liq=liquiditySig
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 effectiveEntryEdge(s){
const net=Math.abs(Number(s&&s.net_edge));
const raw=Math.abs(Number(s&&s.edge));
if(Number.isFinite(net)&&net>0)return Math.max(net,raw*0.72);
return Number.isFinite(raw)?raw:0;
}
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);
@@ -1068,9 +1097,12 @@ function analyzeMarket(m,realWorldSignals={}){
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;
const strictTradeReady=absNet>=minEdge&&m.volume>=Math.max(MIN_VOLUME,policy.minVol)&&m.liquidity>=Math.max(MIN_LIQUIDITY,policy.minLiq)&&evidence.score>=policy.minEvidence;
const selectiveTradeReady=edge>=MIN_SELECTIVE_ENTRY_EDGE&&conviction>=58&&m.volume>=MIN_VOLUME&&m.liquidity>=MIN_LIQUIDITY&&evidence.score>=0.42&&m.yes_price>=0.08&&m.yes_price<=0.92;
const tradeReady=strictTradeReady||selectiveTradeReady;
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.`;}
if(selectiveTradeReady&&!strictTradeReady){rationale=`Selective BUY from top-5,000 scan: raw gap ${(edge*100).toFixed(1)}c, conviction ${Math.round(conviction)}, high activity/liquidity, and evidence ${Math.round(evidence.score*100)}. Net edge is conservative, so sizing stays disciplined.`;}
else 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];
@@ -1084,7 +1116,7 @@ function analyzeMarket(m,realWorldSignals={}){
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",
quality:strictTradeReady?"EV+":(tradeReady?"selective":"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};
@@ -1207,7 +1239,7 @@ function compactSyncItems(items,limits=SYNC_LIMITS){
}
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){const p={date:todayStr(),generated_at:nowIso(),engine_version:SUGGESTION_ENGINE_VERSION,market_count:marketCount,suggestion_cap:SUGGESTION_TOTAL,suggestions:sugs};const compact=compactSuggestionsForSync(p);localStorage.setItem(SUG_KEY,JSON.stringify(compact));return compact;}
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;
@@ -1662,9 +1694,9 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=
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)
&&effectiveEntryEdge(s)>=MIN_SELECTIVE_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)
&&(s.evidence_score==null||s.evidence_score>=0.40||s.conviction>=62)
&&!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=[];
@@ -1675,7 +1707,7 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=
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);}
else{const base=(s.peer_conviction/100)*Math.min(1,effectiveEntryEdge(s)/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);
@@ -2029,13 +2061,15 @@ async function runDailyCycle(){
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));
setStatus("loading market universe for top-5,000 ranking…",true);
const markets=await fetchMarkets(Infinity,100,count=>setStatus(`ranking active markets (${count}) before top-5,000 analysis`,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(markets);
const realWorldSignals=await fetchRealWorldSignals(analysisMarkets);
setStatus("analyzing expected value…",true);
const sugs=generateSuggestions(markets,SUGGESTION_TOTAL,SUGGESTION_PER_CATEGORY,realWorldSignals);
saveSuggestions(sugs,markets.length);
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();
@@ -2428,9 +2462,11 @@ function renderSuggestions(){
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 ${Number(data.market_count||0).toLocaleString()} active markets 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 ${Number(data.market_count||0).toLocaleString()} active markets. Showing ${filtered.length} of ${pool.length} ${focus} ideas; agents only open EV+ BUY ${focus} positions while this is selected.`;
? `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);