mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-16 17:18:06 +00:00
Add return chart ranges and average line
This commit is contained in:
+115
-60
@@ -111,6 +111,11 @@ h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif}
|
|||||||
.legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:14px}
|
.legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:14px}
|
||||||
.leg{display:flex;align-items:center;gap:7px;font-size:12.5px;color:#cdd6e8}
|
.leg{display:flex;align-items:center;gap:7px;font-size:12.5px;color:#cdd6e8}
|
||||||
.legdot{width:10px;height:10px;border-radius:3px;display:inline-block}
|
.legdot{width:10px;height:10px;border-radius:3px;display:inline-block}
|
||||||
|
.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 ---------- */
|
/* ---------- leaderboard ---------- */
|
||||||
.lb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(225px,1fr));gap:16px;margin-bottom:20px}
|
.lb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(225px,1fr));gap:16px;margin-bottom:20px}
|
||||||
@@ -291,10 +296,10 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
|
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
<div class="card hoverable">
|
<div class="card hoverable">
|
||||||
<div class="card-h"><h3>The Equity Race</h3><span class="small muted" id="lastRun"></span></div>
|
<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>
|
||||||
<svg id="chart" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
|
<svg id="chart" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
|
||||||
<div class="legend" id="comboLegend"></div>
|
<div class="legend" id="comboLegend"></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 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>
|
||||||
<div class="card hoverable">
|
<div class="card hoverable">
|
||||||
<div class="card-h"><h3>Top Picks Today</h3><span class="small muted">highest conviction</span></div>
|
<div class="card-h"><h3>Top Picks Today</h3><span class="small muted">highest conviction</span></div>
|
||||||
@@ -308,10 +313,10 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
<div class="section-title">🏆 Leaderboard <span class="small muted" id="lbSub" style="font-weight:400"></span></div>
|
<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="lb-grid" id="leaderboard"></div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h"><h3>Equity Race — all agents</h3><span class="small muted">$10,000 start each</span></div>
|
<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>
|
||||||
<svg id="chart2" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
|
<svg id="chart2" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
|
||||||
<div class="legend" id="comboLegend2"></div>
|
<div class="legend" id="comboLegend2"></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 backtest. After that, each manual or daily run adds another return snapshot for every agent.</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -383,6 +388,7 @@ 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 PF_SORT_KEY = "pma_portfolio_sort_v1";
|
||||||
|
const CHART_RANGE_KEY = "pma_chart_range_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"};
|
||||||
@@ -437,6 +443,7 @@ const signClass = (n) => (n>0?"pos-val":n<0?"neg-val":"");
|
|||||||
const todayStr = () => new Date().toISOString().slice(0,10);
|
const todayStr = () => new Date().toISOString().slice(0,10);
|
||||||
const nowIso = () => new Date().toISOString();
|
const nowIso = () => new Date().toISOString();
|
||||||
let SIM_DAY = null; // set during backtest so logs/snapshots use the simulated date
|
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
|
||||||
const logDay = () => SIM_DAY || todayStr();
|
const logDay = () => SIM_DAY || todayStr();
|
||||||
function toast(msg){
|
function toast(msg){
|
||||||
let t=document.querySelector(".toast");
|
let t=document.querySelector(".toast");
|
||||||
@@ -645,11 +652,11 @@ function openPositions(p,cfg,rankedSugs,focus){
|
|||||||
}
|
}
|
||||||
function recordSnapshot(p){
|
function recordSnapshot(p){
|
||||||
const eq=equity(p);
|
const eq=equity(p);
|
||||||
const snap={date:logDay(),timestamp:(SIM_DAY?new Date(SIM_DAY+"T12:00:00Z").toISOString():nowIso()),cash:+p.cash.toFixed(2),
|
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),
|
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};
|
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);
|
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)=>a.date<b.date?-1:1);
|
p.snapshots.push(snap);p.snapshots.sort((a,b)=>new Date(a.timestamp||a.date)-new Date(b.timestamp||b.date));
|
||||||
}
|
}
|
||||||
/* ---------- Copy-trading: Polymarket leaders ---------- */
|
/* ---------- Copy-trading: Polymarket leaders ---------- */
|
||||||
const hasAsset=(p,a)=>p.positions.some(x=>x.asset===a);
|
const hasAsset=(p,a)=>p.positions.some(x=>x.asset===a);
|
||||||
@@ -882,50 +889,53 @@ async function backtestWeek(st){
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runDailyCycle(){
|
async function runDailyCycle(){
|
||||||
|
SNAP_TS=nowIso();
|
||||||
setStatus("fetching markets…",true);
|
setStatus("fetching markets…",true);
|
||||||
const markets=await fetchMarkets(4,100);
|
try{
|
||||||
setStatus("analyzing…",true);
|
const markets=await fetchMarkets(4,100);
|
||||||
const sugs=generateSuggestions(markets,12);
|
setStatus("analyzing…",true);
|
||||||
saveSuggestions(sugs);
|
const sugs=generateSuggestions(markets,12);
|
||||||
const st=loadState();
|
saveSuggestions(sugs);
|
||||||
const focus=getFocus();
|
const st=loadState();
|
||||||
// Price map for suggestion-market positions (strategy + copycat only).
|
const focus=getFocus();
|
||||||
const ids=new Set();
|
// Price map for suggestion-market positions (strategy + copycat only).
|
||||||
AGENTS.filter(a=>a.kind==="strategy"||a.kind==="copycat").forEach(a=>
|
const ids=new Set();
|
||||||
(st.agents[a.id].positions||[]).forEach(pos=>{if(pos.market_id)ids.add(pos.market_id);}));
|
AGENTS.filter(a=>a.kind==="strategy"||a.kind==="copycat").forEach(a=>
|
||||||
setStatus("marking positions…",true);
|
(st.agents[a.id].positions||[]).forEach(pos=>{if(pos.market_id)ids.add(pos.market_id);}));
|
||||||
const priceMap={};
|
setStatus("marking positions…",true);
|
||||||
for(const id of ids){priceMap[id]=await fetchMarketPrice(id);}
|
const priceMap={};
|
||||||
// 1) in-house strategy agents
|
for(const id of ids){priceMap[id]=await fetchMarketPrice(id);}
|
||||||
setStatus("strategy agents trading…",true);
|
// 1) in-house strategy agents
|
||||||
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
setStatus("strategy agents trading…",true);
|
||||||
const p=st.agents[cfg.id];
|
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
||||||
markToMarket(p,priceMap);
|
const p=st.agents[cfg.id];
|
||||||
openPositions(p,cfg,cfg.rank(sugs),focus);
|
markToMarket(p,priceMap);
|
||||||
recordSnapshot(p);
|
openPositions(p,cfg,cfg.rank(sugs),focus);
|
||||||
}
|
recordSnapshot(p);
|
||||||
// 2) copycat shadows the leading strategy agent
|
}
|
||||||
const stratBoard=AGENTS.filter(a=>a.kind==="strategy")
|
// 2) copycat shadows the leading strategy agent
|
||||||
.map(a=>({id:a.id,eq:equity(st.agents[a.id])})).sort((x,y)=>y.eq-x.eq);
|
const stratBoard=AGENTS.filter(a=>a.kind==="strategy")
|
||||||
st.copycatLeader=stratBoard[0].id;
|
.map(a=>({id:a.id,eq:equity(st.agents[a.id])})).sort((x,y)=>y.eq-x.eq);
|
||||||
runCopycat(st.agents["copycat"],st.agents[st.copycatLeader],priceMap);
|
st.copycatLeader=stratBoard[0].id;
|
||||||
// 3) whale-copy agents mirror top Polymarket traders
|
runCopycat(st.agents["copycat"],st.agents[st.copycatLeader],priceMap);
|
||||||
if(AGENTS.some(a=>a.kind==="whale"&&!st.whales[a.id])){
|
// 3) whale-copy agents mirror top Polymarket traders
|
||||||
setStatus("finding top Polymarket traders…",true);
|
if(AGENTS.some(a=>a.kind==="whale"&&!st.whales[a.id])){
|
||||||
const picked=await discoverWhales();
|
setStatus("finding top Polymarket traders…",true);
|
||||||
AGENTS.filter(a=>a.kind==="whale").forEach(a=>{if(!st.whales[a.id]&&picked[a.slot])st.whales[a.id]=picked[a.slot];});
|
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];
|
for(const a of AGENTS.filter(x=>x.kind==="whale")){
|
||||||
if(!wh){recordSnapshot(st.agents[a.id]);continue;}
|
const wh=st.whales[a.id];
|
||||||
setStatus(`copying ${wh.name}…`,true);
|
if(!wh){recordSnapshot(st.agents[a.id]);continue;}
|
||||||
try{await runWhaleAgent(st.agents[a.id],wh.wallet);}
|
setStatus(`copying ${wh.name}…`,true);
|
||||||
catch(e){recordSnapshot(st.agents[a.id]);}
|
try{await runWhaleAgent(st.agents[a.id],wh.wallet);}
|
||||||
}
|
catch(e){recordSnapshot(st.agents[a.id]);}
|
||||||
st.date=todayStr();st.last_run=nowIso();
|
}
|
||||||
saveState(st);
|
st.date=todayStr();st.last_run=SNAP_TS;
|
||||||
setStatus("up to date",false);
|
saveState(st);
|
||||||
return {suggestions:sugs.length};
|
setStatus("up to date",false);
|
||||||
|
return {suggestions:sugs.length};
|
||||||
|
}finally{SNAP_TS=null;}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Leaderboard helpers ---------- */
|
/* ---------- Leaderboard helpers ---------- */
|
||||||
@@ -1022,26 +1032,71 @@ function renderLeaderboard(){
|
|||||||
});
|
});
|
||||||
drawCombo("chart2","comboLegend2");
|
drawCombo("chart2","comboLegend2");
|
||||||
}
|
}
|
||||||
|
const CHART_RANGES=[
|
||||||
|
{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 renderChartRanges(){
|
||||||
|
const active=chartRange();
|
||||||
|
document.querySelectorAll("[data-chart-ranges]").forEach(root=>{
|
||||||
|
root.innerHTML=CHART_RANGES.map(r=>`<button class="rangebtn ${r.id===active?"active":""}" data-range="${r.id}" type="button">${r.label}</button>`).join("");
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".rangebtn").forEach(btn=>btn.onclick=()=>{localStorage.setItem(CHART_RANGE_KEY,btn.dataset.range);renderAll();});
|
||||||
|
}
|
||||||
|
const snapTime=(s)=>new Date(s.timestamp||`${s.date}T12:00:00Z`).getTime();
|
||||||
|
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){
|
function drawCombo(svgId,legendId){
|
||||||
const st=loadState();
|
const st=loadState();
|
||||||
const series=AGENTS.map(c=>({c,snaps:(st.agents[c.id]&&st.agents[c.id].snapshots)||[]}));
|
renderChartRanges();
|
||||||
const allDates=[...new Set(series.flatMap(s=>s.snaps.map(x=>x.date)))].sort();
|
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;
|
const svg=$(svgId); if(!svg)return;
|
||||||
if(!allDates.length){svg.innerHTML="";if($(legendId))$(legendId).innerHTML=`<span class="muted small">No history yet — click "Run cycle".</span>`;return;}
|
if(!allTimes.length){svg.innerHTML="";if($(legendId))$(legendId).innerHTML=`<span class="muted small">No history yet — click "Run cycle".</span>`;return;}
|
||||||
const W=600,H=240,pad=10; const vals=[STARTING_BALANCE];
|
const W=600,H=240,pad=14; const vals=[0];
|
||||||
const lines=series.map(s=>{let last=STARTING_BALANCE;const pts=allDates.map(d=>{const snap=s.snaps.find(x=>x.date===d);if(snap)last=snap.equity;vals.push(last);return last;});return {c:s.c,pts};});
|
const lines=series.map(s=>{
|
||||||
const min=Math.min(...vals)*0.999,max=Math.max(...vals)*1.001,span=(max-min)||1;
|
let last=0,idx=0;
|
||||||
const x=i=>allDates.length===1?W/2:pad+(i/(allDates.length-1))*(W-2*pad);
|
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 min=Math.min(...vals,0),max=Math.max(...vals,0),span=(max-min)||1;
|
||||||
|
const x=i=>allTimes.length===1?W/2:pad+(i/(allTimes.length-1))*(W-2*pad);
|
||||||
const y=v=>H-pad-((v-min)/span)*(H-2*pad);
|
const y=v=>H-pad-((v-min)/span)*(H-2*pad);
|
||||||
let inner="";
|
const zeroY=y(0).toFixed(1);
|
||||||
|
let inner=`<line x1="${pad}" x2="${W-pad}" y1="${zeroY}" y2="${zeroY}" stroke="rgba(255,255,255,.18)" stroke-width="1"/>`;
|
||||||
for(const ln of lines){
|
for(const ln of lines){
|
||||||
const d=ln.pts.map((v,i)=>`${i===0?"M":"L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
const d=ln.pts.map((v,i)=>`${i===0?"M":"L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
||||||
const dot=allDates.length===1?`<circle cx="${x(0).toFixed(1)}" cy="${y(ln.pts[0]).toFixed(1)}" r="4" fill="${ln.c.color}"/>`:"";
|
const dot=allTimes.length===1?`<circle cx="${x(0).toFixed(1)}" cy="${y(ln.pts[0]).toFixed(1)}" r="4" fill="${ln.c.color}"/>`:"";
|
||||||
inner+=`<path d="${d}" fill="none" stroke="${ln.c.color}" stroke-width="2.4" opacity=".95"/>${dot}`;
|
inner+=`<path d="${d}" fill="none" stroke="${ln.c.color}" stroke-width="2.4" opacity=".95"/>${dot}`;
|
||||||
}
|
}
|
||||||
|
const avgD=avgPts.map((v,i)=>`${i===0?"M":"L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
||||||
|
inner+=`<path d="${avgD}" fill="none" stroke="#ffffff" stroke-width="2.7" stroke-dasharray="7 6" opacity=".78"/>`;
|
||||||
svg.innerHTML=inner;
|
svg.innerHTML=inner;
|
||||||
if($(legendId)){
|
if($(legendId)){
|
||||||
$(legendId).innerHTML=board().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("");
|
const b=board();const avg=b.reduce((s,r)=>s+r.ret,0)/b.length;
|
||||||
|
$(legendId).innerHTML=`<span class="leg"><span class="legdot" style="background:#fff"></span>Average <b class="${signClass(avg)}">${fmtPct(avg)}</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){
|
function renderFilterBar(all){
|
||||||
|
|||||||
Reference in New Issue
Block a user