diff --git a/README.md b/README.md index a1ebfa5..594645f 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Personal research mode: https://polymarket-site-eta.vercel.app/personal.html The site fetches live Polymarket markets, generates agent suggestions, lets you -run hourly paper cycles, and syncs the shared arena state through the Vercel API -when `BLOB_READ_WRITE_TOKEN` is configured. +run frequent paper cycles, and syncs the shared arena state through the Vercel +API when `BLOB_READ_WRITE_TOKEN` is configured. Paper accounts created with a password are also saved through the backend, so a user can log in from another device and see the same paper portfolio, activity, diff --git a/api/state.js b/api/state.js index 075d065..39ef5af 100644 --- a/api/state.js +++ b/api/state.js @@ -43,18 +43,18 @@ export default async function handler(req, res) { const currentAgents = agentStateFromItems(current && current.items); const incomingAgents = agentStateFromItems(body.items); if (!body.force && currentAgents) { - const currentHour = currentAgents.last_cycle_hour || ""; - const incomingHour = incomingAgents && incomingAgents.last_cycle_hour ? incomingAgents.last_cycle_hour : ""; - if (currentHour && (!incomingAgents || !incomingHour)) { + const currentCycle = currentAgents.last_cycle_hour || ""; + const incomingCycle = incomingAgents && incomingAgents.last_cycle_hour ? incomingAgents.last_cycle_hour : ""; + if (currentCycle && (!incomingAgents || !incomingCycle)) { return conflictResponse(res, "Cloud already has a cycle result; refusing unscheduled local state", current); } - if (currentHour && incomingHour && incomingHour < currentHour) { + if (currentCycle && incomingCycle && incomingCycle < currentCycle) { return conflictResponse(res, "Incoming state is older than the shared cloud result", current); } - const sameHour = currentHour && currentHour === incomingHour; + const sameCycle = currentCycle && currentCycle === incomingCycle; const differentRun = currentAgents.last_run && incomingAgents.last_run && currentAgents.last_run !== incomingAgents.last_run; - if (sameHour && differentRun) { - return conflictResponse(res, "This cycle hour already has a cloud result", current); + if (sameCycle && differentRun) { + return conflictResponse(res, "This cycle already has a cloud result", current); } } const state = { version: 1, updated_at: new Date().toISOString(), items: body.items }; diff --git a/index.html b/index.html index ef2a04f..98875b8 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ Polymarket Analyst — 10 AI Agents Compete to Trade Prediction Markets - + @@ -387,13 +387,13 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color

Returns — all agents

$10,000 start each
-
Past week is a backtest. After that, each hourly run adds another return snapshot for every agent.
+
Past week is a backtest. After that, each live cycle adds another return snapshot for every agent.
-
💡 Hourly Suggestions
+
💡 Live Suggestions
@@ -672,10 +672,10 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
ℹ️ The competition
-

Every agent starts with $10,000 in paper money. The five in-house strategies trade the same hourly suggestions their own way; the Copycat 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 hourly and climb — or sink — the board.

+

Every agent starts with $10,000 in paper money. The five in-house strategies trade the same frequently refreshed suggestions their own way; the Copycat 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.

-

The hourly cycle

+

The live cycle

  1. Fetch — page through every active Polymarket market and tag each by category.
  2. Score — rank every market 0–100 by conviction and estimate its edge vs. a fair-value model.
  3. @@ -796,7 +796,7 @@ async function initProviderConfig(){ } /* ---------- the ten competing agents ---------- - Each shares the same hourly suggestions but ranks/sizes them differently. */ + 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 gap between the model's fair value and the current market price. 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.", @@ -840,20 +840,22 @@ 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",hourCycle:"h23"}).formatToParts(d); + 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 currentCycleHour = () => {const p=partsInLocalTime();return `${p.year}-${p.month}-${p.day}T${p.hour}`;}; +const RUN_INTERVAL_MS = 60000; +const currentCycleHour = () => {const p=partsInLocalTime();return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}`;}; 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}`; + const p=partsInLocalTime(d);return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}`; } 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"); @@ -1583,17 +1585,19 @@ async function backtestWeek(st){ } async function runDailyCycle(){ - 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); + 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)); setStatus("analyzing…",true); const sugs=generateSuggestions(markets); @@ -1646,7 +1650,7 @@ async function runDailyCycle(){ setStatus("up to date",false); if(!pushed)return {suggestions:(loadSuggestions().suggestions||[]).length,skipped:true,hour}; return {suggestions:sugs.length}; - }finally{SNAP_TS=null;} + }finally{SNAP_TS=null;CYCLE_RUNNING=false;} } /* ---------- Leaderboard helpers ---------- */ @@ -2986,7 +2990,7 @@ applyPersonalMode(); initProviderConfig(); $("runBtn").addEventListener("click",async()=>{ const btn=$("runBtn");btn.disabled=true;btn.textContent="Running…"; - try{const r=await runDailyCycle();const b=board();toast(r.skipped?`Already ran this hour · ${b[0].c.emoji} ${b[0].c.name} leads`:`Cycle done · ${r.suggestions} ideas · ${b[0].c.emoji} ${b[0].c.name} leads`);renderAll();} + 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"; }); @@ -3105,8 +3109,16 @@ 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 once per hour. */ +/* 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(); @@ -3133,7 +3145,7 @@ document.addEventListener("visibilitychange",()=>{ 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("Hourly update ready.");} + 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);}