Real day-by-day backtest: replay strategy decisions on historical prices + whale on-chain trade history

This commit is contained in:
theodore-song
2026-07-05 15:23:22 -04:00
parent 31afd37b34
commit d16e8527ef
+117 -43
View File
@@ -257,7 +257,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<div class="card-h"><h3>The Equity Race</h3><span class="small muted" id="lastRun"></span></div>
<svg id="chart" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
<div class="legend" id="comboLegend"></div>
<div class="small muted" style="margin-top:10px">Past week is a backtest — current holdings marked against real Polymarket price history. Live tracking takes over from today.</div>
<div class="small muted" style="margin-top:10px">Past week is a day-by-day backtest: agents re-ran their strategy on each day's real historical prices; whales are replayed from their on-chain trade history. (Volume/liquidity signals use current values — Polymarket doesn't expose historical volume.) Live tracking begins today.</div>
</div>
<div class="card hoverable">
<div class="card-h"><h3>Top Picks Today</h3><span class="small muted">highest conviction</span></div>
@@ -389,6 +389,8 @@ const fmtPct = (n) => (n>=0?"+":"") + Number(n).toFixed(2) + "%";
const signClass = (n) => (n>0?"pos-val":n<0?"neg-val":"");
const todayStr = () => new Date().toISOString().slice(0,10);
const nowIso = () => new Date().toISOString();
let SIM_DAY = null; // set during backtest so logs/snapshots use the simulated date
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);}
@@ -513,7 +515,7 @@ function markToMarket(p,priceMap){
pos.exit_price=pos.current_price;pos.closed_at=nowIso();
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
p.closed.push(pos);
p.history.push({date:todayStr(),action:"CLOSE",question:pos.question,side:pos.side,
p.history.push({date:logDay(),action:"CLOSE",question:pos.question,side:pos.side,
detail:`Settled '${pos.question.slice(0,40)}' for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
continue;
}
@@ -544,14 +546,14 @@ function openPositions(p,cfg,rankedSugs,focus){
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,
unrealized_pnl:0,conviction:s.conviction,category:s.category,opened_at:nowIso(),url:s.url||""});
p.history.push({date:todayStr(),action:"OPEN",question:s.question,side:s.side,
p.history.push({date:logDay(),action:"OPEN",question:s.question,side:s.side,
detail:`Bought ${shares} ${s.side} '${s.question.slice(0,40)}' @ ${pct(entry)} for ${fmtUSD(cost)}`});
opened++;
}
}
function recordSnapshot(p){
const eq=equity(p);
const snap={date:todayStr(),timestamp:nowIso(),cash:+p.cash.toFixed(2),
const snap={date:logDay(),timestamp:(SIM_DAY?new Date(SIM_DAY+"T12:00:00Z").toISOString():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=p.snapshots.filter(s=>s.date!==snap.date);
@@ -604,7 +606,7 @@ async function runWhaleAgent(p,wallet){
if(!held.has(pos.asset)){
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);
p.history.push({date:todayStr(),action:"CLOSE",question:pos.question,side:pos.side,
p.history.push({date:logDay(),action:"CLOSE",question:pos.question,side:pos.side,
detail:`Trader exited '${pos.question.slice(0,36)}' — settled ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
}else keep.push(pos);
}
@@ -622,7 +624,7 @@ async function runWhaleAgent(p,wallet){
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,unrealized_pnl:0,conviction:null});
p.history.push({date:todayStr(),action:"OPEN",question:t.title,side:t.side,
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);
@@ -637,7 +639,7 @@ function runCopycat(p,leader,priceMap){
if(!held.has(pos.market_id)){
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);
p.history.push({date:todayStr(),action:"CLOSE",question:pos.question,side:pos.side,
p.history.push({date:logDay(),action:"CLOSE",question:pos.question,side:pos.side,
detail:`Leader exited '${pos.question.slice(0,36)}' — settled ${fmtUSD(proceeds)}`});
}else keep.push(pos);
}
@@ -655,7 +657,7 @@ function runCopycat(p,leader,priceMap){
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,unrealized_pnl:0,conviction:lp.conviction});
p.history.push({date:todayStr(),action:"OPEN",question:lp.question,side:lp.side,
p.history.push({date:logDay(),action:"OPEN",question:lp.question,side:lp.side,
detail:`Copied leader — ${shares} ${lp.side} '${lp.question.slice(0,36)}' @ ${pct(entry)}`});
}
recordSnapshot(p);
@@ -681,31 +683,104 @@ function priceOnDay(map,day,fallback){
if(best!=null)return best;
const ks=Object.keys(map).sort();return ks.length?map[ks[0]]:fallback;
}
async function seedWeekInto(st){
// 1) make sure every open position knows its CLOB token id
for(const a of AGENTS)for(const pos of st.agents[a.id].positions){
if(pos.token_id)continue;
if(pos.asset){pos.token_id=pos.asset;continue;}
if(pos.market_id){const t=await fetchClobTokens(pos.market_id);if(t)pos.token_id=(pos.side==="YES")?t[0]:t[1];}
}
// 2) pull 7-day price history for each unique token once
const ids=new Set();
for(const a of AGENTS)for(const pos of st.agents[a.id].positions)if(pos.token_id)ids.add(pos.token_id);
const hist={};
for(const id of ids)hist[id]=await fetchPriceHistory(id);
// 3) rebuild each agent's snapshots across the past week
const dates=lastNDates(7);
for(const a of AGENTS){
const p=st.agents[a.id];
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=>{
let pv=0;
for(const pos of p.positions){pv+=pos.shares*priceOnDay(hist[pos.token_id],day,pos.entry_price);}
const eq=p.cash+pv;
return {date:day,timestamp:new Date(day+"T12:00:00Z").toISOString(),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,seeded:true};
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,12);
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);
openPositions(p,cfg,cfg.rank(sugs),focus);
recordSnapshot(p);
}
const leaderId=AGENTS.filter(a=>a.kind==="strategy").map(a=>({id:a.id,eq:equity(st.agents[a.id])})).sort((x,y)=>y.eq-x.eq)[0].id;
st.copycatLeader=leaderId;
runCopycat(st.agents.copycat,st.agents[leaderId],priceMap);
if(day===dates[dates.length-1])saveSuggestions(sugs); // today's live-ish board for the Suggestions tab
}
SIM_DAY=null;
await backtestWhales(st,dates);
// establish today's actual 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;
}
@@ -750,7 +825,6 @@ async function runDailyCycle(){
try{await runWhaleAgent(st.agents[a.id],wh.wallet);}
catch(e){recordSnapshot(st.agents[a.id]);}
}
if(!st.seeded){setStatus("reconstructing past week…",true);await seedWeekInto(st);}
st.date=todayStr();st.last_run=nowIso();
saveState(st);
setStatus("up to date",false);
@@ -955,24 +1029,24 @@ $("resetBtn").addEventListener("click",()=>{
saveState(defaultState());localStorage.removeItem(SUG_KEY);toast("All agents reset.");renderAll();
});
/* On load: route, render, auto-run once per day. */
/* On load: first visit runs a 7-day backtest; afterwards a live daily update. */
(async function init(){
showTab(location.hash.slice(1)||"overview");
renderAll();
const sug=loadSuggestions();
if(sug.date!==todayStr()){
const btn=$("runBtn");btn.disabled=true;btn.textContent="Running…";
try{await runDailyCycle();renderAll();toast("Fresh daily competition ready.");}
const st=loadState();
const btn=$("runBtn");
if(!st.seeded){
btn.disabled=true;btn.textContent="Backtesting…";
try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();saveState(st);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.date!==todayStr()){
btn.disabled=true;btn.textContent="Running…";
try{await runDailyCycle();renderAll();toast("Daily update ready.");}
catch(e){setStatus("error — click Run to retry",false);}
btn.disabled=false;btn.textContent="Run cycle";
}else{setStatus("up to date",false);}
// If a cycle already ran today but we haven't seeded the past week yet, do it now.
const stx=loadState();
if(!stx.seeded && AGENTS.some(a=>stx.agents[a.id].positions.length)){
setStatus("reconstructing past week…",true);
try{await seedWeekInto(stx);saveState(stx);renderAll();toast("Backfilled the past week of history.");}catch(e){}
setStatus("up to date",false);
}
})();
</script>
</body>