/* * Polymarket Strategy Lab — pure browser-side backtester. * * Loads 100+ REAL resolved Polymarket categorical events from docs/data/ * historical-events.json, runs five different strategies against them, * and shows you honest results — no cherry-picking. * * Each strategy is a pure function: given an event (outcomes + their * last-trade prices + who actually won), it returns what it would have * bought, how much it paid, and how much it got back. The backtest runner * tallies these across every event. * * The strategies live here, open and readable. You can read exactly what * each rule is doing. */ // ============================== DATA ==================================== const DATA_URL = "./data/historical-events.json"; // ========================== STRATEGIES ================================== // // A strategy is: strategy(event) -> { action, cost, payout, note } // action: "trade" if we bought anything, "skip" if we passed // cost: total dollars paid (at last-trade prices) // payout: total dollars received after resolution // note: short plain-English description of what happened // // Every strategy bets into the same event in its own way. Results are tallied // across all events in the dataset. const STRATEGIES = [ { key: "basket-arb", name: "Basket Arbitrage", oneLiner: "Buy one share of every outcome — but only when the total cost is under $1.", rule: "If the sum of every outcome's last trade price is below $1.00, buy one share of every outcome. Otherwise skip. Exactly one outcome will win and pay $1, so you profit the gap.", why: "This is the textbook risk-free trade. It's the one real arbitrage on prediction markets. The question is: does it ever actually trigger in practice, on resting prices, for a retail bot that isn't co-located next to the exchange? The historical data tells the truth.", run(ev, window) { const prices = ev.outcomes.map(o => priceAt(o, ev, window)); if (prices.some(p => p == null || p <= 0 || p >= 1)) { return { action: "skip", cost: 0, payout: 0, sum: null, note: "No price data at this window for at least one outcome." }; } const sum = prices.reduce((a, b) => a + b, 0); if (sum >= 1.0) { return { action: "skip", cost: 0, payout: 0, sum, note: `Total cost $${sum.toFixed(3)}, above $1. No arbitrage — skipped.` }; } return { action: "trade", cost: sum, payout: 1.0, sum, note: `Total cost $${sum.toFixed(3)}. Bought the full set — guaranteed $1 payout.` }; }, }, { key: "favorite", name: "Bet the Favorite", oneLiner: "On every event, buy the single outcome the market thinks is most likely.", rule: "For each event, buy one share of whichever outcome has the highest price at the chosen time window. If that outcome wins you get $1, otherwise $0.", why: "Conventional wisdom: the market knows. If the favorite wins often enough you make money; if favorites are over-priced you lose. Tests whether Polymarket's top-line pricing has any slack.", run(ev, window) { let best = null, bestPrice = -1; for (const o of ev.outcomes) { const p = priceAt(o, ev, window); if (p == null) continue; if (p > bestPrice) { best = o; bestPrice = p; } } if (!best || bestPrice <= 0) return { action: "skip", cost: 0, payout: 0, note: "No valid prices." }; return { action: "trade", cost: bestPrice, payout: best.yes_final_price, note: `Bought "${best.name}" at $${bestPrice.toFixed(3)}. ${best.yes_final_price === 1 ? "Won — payout $1." : "Lost — payout $0."}`, }; }, }, { key: "longshot", name: "Bet the Longshot", oneLiner: "On every event, buy the cheapest outcome. Pray it wins.", rule: "For each event, buy one share of whichever outcome has the lowest positive price at the chosen window.", why: "The market prices longshots low for a reason. But if underdogs win more often than prices imply (a classic bias), this pays. Direct test.", run(ev, window) { let best = null, bestPrice = Infinity; for (const o of ev.outcomes) { const p = priceAt(o, ev, window); if (p == null || p <= 0) continue; if (p < bestPrice) { best = o; bestPrice = p; } } if (!best) return { action: "skip", cost: 0, payout: 0, note: "No valid prices." }; return { action: "trade", cost: bestPrice, payout: best.yes_final_price, note: `Bought "${best.name}" at $${bestPrice.toFixed(3)}. ${best.yes_final_price === 1 ? "Won — payout $1." : "Lost — payout $0."}`, }; }, }, { key: "equal-split", name: "Equal Split", oneLiner: "Buy one share of every outcome, always — no matter the price.", rule: "For each event, buy one share of every outcome. You pay the sum of prices. You receive $1 (exactly one wins).", why: "Basket Arbitrage without the safety condition. Every event is a tiny guaranteed loss equal to the “vig” — the amount by which Polymarket's prices overshoot $1. A baseline for what the market's rounding costs.", run(ev, window) { const prices = ev.outcomes.map(o => priceAt(o, ev, window)); // If any outcome lacks price data at this window, skip (we can't evaluate) if (prices.some(p => p == null)) { return { action: "skip", cost: 0, payout: 0, sum: null, note: "No price data at this window for at least one outcome." }; } if (prices.some(p => p == null || p <= 0)) { return { action: "skip", cost: 0, payout: 0, note: "Missing prices." }; } const cost = prices.reduce((a, b) => a + b, 0); return { action: "trade", cost, payout: 1.0, note: `Paid $${cost.toFixed(3)} for every outcome. Guaranteed $1 payout.` }; }, }, { key: "top-three", name: "Top Three", oneLiner: "Buy the three outcomes the market thinks are most likely. Win if any of them wins.", rule: "For each event with 3+ outcomes, buy one share of the three highest-priced outcomes at the chosen window. Pay the sum. Win $1 if any of those three wins.", why: "A hedged bet — buying most of the probability mass but skipping the tail. If the hit rate is high enough, it pays.", run(ev, window) { const priced = ev.outcomes.map(o => ({ o, p: priceAt(o, ev, window) })).filter(x => x.p != null && x.p > 0); if (priced.length < 3) return { action: "skip", cost: 0, payout: 0, note: "Fewer than 3 priced outcomes." }; const top = [...priced].sort((a, b) => b.p - a.p).slice(0, 3); const cost = top.reduce((s, x) => s + x.p, 0); const won = top.some(x => x.o.yes_final_price === 1); return { action: "trade", cost, payout: won ? 1.0 : 0.0, note: `Bought top 3 (total $${cost.toFixed(3)}). ${won ? "One won — payout $1." : "None won — payout $0."}`, }; }, }, ]; // ========================== BACKTEST RUNNER ============================= function runBacktest(strategy, events, window) { const rows = []; let totalCost = 0, totalPayout = 0; let trades = 0, wins = 0, losses = 0, skipped = 0; for (const ev of events) { const result = strategy.run(ev, window); const pnl = (result.payout || 0) - (result.cost || 0); const row = { event: ev, result, pnl }; rows.push(row); if (result.action === "trade") { trades += 1; totalCost += result.cost || 0; totalPayout += result.payout || 0; if (pnl > 0) wins += 1; else if (pnl < 0) losses += 1; } else { skipped += 1; } } const pnlAbs = totalPayout - totalCost; const roi = totalCost > 0 ? pnlAbs / totalCost : 0; const winRate = trades > 0 ? wins / trades : null; return { rows, totalCost, totalPayout, pnlAbs, roi, trades, wins, losses, skipped, winRate, eventCount: events.length, }; } // ========================== STATE ======================================= const state = { events: [], results: {}, activeKey: "basket-arb", tradeFilter: "all", bankroll: 1000, priceWindow: "24h", // key from WINDOWS below }; // Time windows: how many seconds before close to read the price. const WINDOWS = { "close": { label: "at close", seconds: 0 }, "1h": { label: "1h before close", seconds: 3600 }, "6h": { label: "6h before close", seconds: 6*3600 }, "24h": { label: "24h before close", seconds: 24*3600 }, "3d": { label: "3 days before close", seconds: 3*24*3600 }, "7d": { label: "7 days before close", seconds: 7*24*3600 }, }; /** * Return the actual price a trader would have seen on Polymarket at a specific * time. Uses real historical price data pulled from Polymarket's public CLOB * price-history endpoint — not estimates. */ function priceAt(outcome, ev, windowKey) { const hist = outcome.history; if (!hist || !hist.length) return null; const w = WINDOWS[windowKey] || WINDOWS["close"]; const closeTs = ev._closeTs; // precomputed if (closeTs == null) return null; const targetTs = closeTs - w.seconds; // If the target is before any recorded data, no price if (hist[0].t > targetTs) return null; // Binary search for the last point with t <= targetTs let lo = 0, hi = hist.length - 1; while (lo < hi) { const mid = Math.ceil((lo + hi) / 2); if (hist[mid].t <= targetTs) lo = mid; else hi = mid - 1; } return hist[lo].p; } // ========================== DOM ========================================= const $ = (s) => document.querySelector(s); const el = { tabResults: $("#tab-results"), tabStrategies: $("#tab-strategies"), panelResults: $("#panel-results"), panelStrategies:$("#panel-strategies"), eventCountInline: $("#event-count-inline"), eventCountStrat: $("#event-count-strat"), activeLabel: $("#active-strategy-label"), activeName: $("#active-strategy-name"), activeDesc: $("#active-strategy-desc"), switchBtn: $("#switch-btn"), verdictCard: $("#verdict-card"), verdictIcon: $("#verdict-icon"), verdictLabel: $("#verdict-label"), verdictDetail: $("#verdict-detail"), vstatPnl: $("#vstat-pnl"), vstatPnlLbl: $("#vstat-pnl-lbl"), vstatRoi: $("#vstat-roi"), vstatTrades: $("#vstat-trades"), vstatWinrate: $("#vstat-winrate"), vstatAnnual: $("#vstat-annual"), bankrollChoices: $("#bankroll-choices"), bankrollNote: $("#bankroll-note"), verdictExplainer: $("#verdict-explainer"), cntAll: $("#cnt-all"), cntTrades: $("#cnt-trades"), cntWins: $("#cnt-wins"), cntLosses: $("#cnt-losses"), cntSkipped: $("#cnt-skipped"), tradeList: $("#trade-list"), strategyGrid: $("#strategy-grid"), modal: $("#strategy-modal"), modalContent: $("#strategy-modal-content"), }; // ========================== BOOT ======================================== boot().catch(err => { console.error("lab boot failed", err); el.verdictLabel.textContent = "Couldn't load historical data"; el.verdictDetail.textContent = String(err.message || err); }); async function boot() { const resp = await fetch(DATA_URL + "?t=" + Date.now()); if (!resp.ok) throw new Error("historical-events.json " + resp.status); const payload = await resp.json(); state.events = Array.isArray(payload?.events) ? payload.events : []; if (!state.events.length) throw new Error("No events found in dataset"); // Precompute the close timestamp (seconds since epoch) for each event, so // priceAt() can do a cheap binary search per lookup. for (const ev of state.events) { const raw = ev.closed_time || ev.end_date || ""; const iso = String(raw).replace(" +00", "+00:00").replace("Z", "+00:00"); const d = new Date(iso); ev._closeTs = isNaN(d.getTime()) ? null : Math.floor(d.getTime() / 1000); } const ends = state.events .map(e => e._closeTs ? new Date(e._closeTs * 1000) : null) .filter(d => d != null) .sort((a, b) => a - b); state.spanFirst = ends[0]; state.spanLast = ends[ends.length - 1]; state.spanDays = Math.max(1, (state.spanLast - state.spanFirst) / (1000 * 60 * 60 * 24)); el.eventCountInline.textContent = `${state.events.length} events · ${formatSpanDescription(state.spanFirst, state.spanLast)}`; el.eventCountStrat.textContent = state.events.length; rerunBacktests(); wireInteractions(); renderStrategyGrid(); renderActiveStrategy(); } function rerunBacktests() { for (const s of STRATEGIES) { state.results[s.key] = runBacktest(s, state.events, state.priceWindow); } } function formatSpanDescription(first, last, withMonths = true) { if (!first || !last) return ""; const fmt = { month: "short", year: "numeric" }; const range = `${first.toLocaleDateString(undefined, fmt)} – ${last.toLocaleDateString(undefined, fmt)}`; if (!withMonths) return range; const months = (state.spanDays / 30).toFixed(1); return `${range} (${months} months)`; } function pluralize(n, word) { return n === 1 ? `1 ${word}` : `${n} ${word}s`; } function wireInteractions() { // Tabs el.tabResults.addEventListener("click", () => switchTab("results")); el.tabStrategies.addEventListener("click", () => switchTab("strategies")); // "Change strategy" button on results page -> jumps to strategies tab el.switchBtn.addEventListener("click", () => switchTab("strategies")); // Trade filter buttons document.querySelectorAll(".filter-btn").forEach(btn => { btn.addEventListener("click", () => { state.tradeFilter = btn.dataset.filter; document.querySelectorAll(".filter-btn").forEach(b => b.classList.toggle("active", b === btn)); renderTradeList(); }); }); // CSV download const dl = document.getElementById("csv-download"); if (dl) dl.addEventListener("click", (e) => { e.preventDefault(); downloadCsv(); }); // Bankroll selector el.bankrollChoices.addEventListener("click", (e) => { const btn = e.target.closest("button[data-bankroll]"); if (!btn) return; state.bankroll = parseInt(btn.dataset.bankroll, 10) || 1000; [...el.bankrollChoices.querySelectorAll("button")].forEach(b => b.classList.toggle("active", b === btn)); renderActiveStrategy(); renderStrategyGrid(); }); // Price-window selector const windowChoices = document.getElementById("window-choices"); windowChoices.addEventListener("click", (e) => { const btn = e.target.closest("button[data-window]"); if (!btn) return; state.priceWindow = btn.dataset.window; [...windowChoices.querySelectorAll("button")].forEach(b => b.classList.toggle("active", b === btn)); rerunBacktests(); renderActiveStrategy(); renderStrategyGrid(); }); // Modal close el.modal.addEventListener("click", (e) => { if (e.target.dataset?.close !== undefined) el.modal.hidden = true; }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") el.modal.hidden = true; }); } function switchTab(which) { const isResults = which === "results"; el.tabResults.classList.toggle("active", isResults); el.tabStrategies.classList.toggle("active", !isResults); el.panelResults.classList.toggle("active", isResults); el.panelStrategies.classList.toggle("active", !isResults); window.scrollTo({ top: 0, behavior: "smooth" }); } // ========================== RENDER: RESULTS TAB ========================= function renderActiveStrategy() { const strategy = STRATEGIES.find(s => s.key === state.activeKey); if (!strategy) return; const result = state.results[strategy.key]; el.activeLabel.textContent = "Active strategy"; el.activeName.textContent = strategy.name; el.activeDesc.textContent = strategy.oneLiner; renderVerdict(strategy, result); renderTradeList(); } function verdictClass(result) { const pnl = result.pnlAbs; if (Math.abs(pnl) < 0.005) return "flat"; return pnl > 0 ? "win" : "loss"; } function renderVerdict(strategy, result) { const cls = verdictClass(result); el.verdictCard.className = "verdict-card " + cls; el.verdictIcon.textContent = cls === "win" ? "✓" : cls === "loss" ? "✗" : "≈"; const { roi, trades, wins, losses, eventCount } = result; const totalPnl = roi * state.bankroll * trades; // bet $bankroll each trade, PnL per trade = roi*bankroll const months = (state.spanDays / 30).toFixed(1); const span = formatSpanDescription(state.spanFirst, state.spanLast, false); const firedN = pluralize(trades, "time"); if (trades === 0) { el.verdictLabel.textContent = "Strategy never triggered"; el.verdictDetail.textContent = `Over ${months} months of real Polymarket events (${span}), this strategy's rule never fired even once. Pure arbitrage on resting prices almost never exists — bots eat any gap in milliseconds.`; } else if (cls === "win") { el.verdictLabel.textContent = "Made money on this dataset"; el.verdictDetail.textContent = `Over ${months} months (${span}) this strategy fired ${firedN} across ${eventCount} events. ${wins} wins, ${losses} losses. At a $${state.bankroll.toLocaleString()} bankroll per trade, total profit was ${formatSignedDollar(totalPnl)}.`; } else if (cls === "loss") { el.verdictLabel.textContent = "Lost money on this dataset"; el.verdictDetail.textContent = `Over ${months} months (${span}) this strategy fired ${firedN} across ${eventCount} events. ${wins} wins, ${losses} losses. At a $${state.bankroll.toLocaleString()} bankroll per trade, total loss was ${formatSignedDollar(totalPnl)}.`; } else { el.verdictLabel.textContent = "Roughly break-even"; el.verdictDetail.textContent = `Over ${months} months (${span}) this strategy fired ${firedN} across ${eventCount} events. Total profit with a $${state.bankroll.toLocaleString()} bankroll was ${formatSignedDollar(totalPnl)} — essentially nothing.`; } el.vstatPnl.textContent = formatSignedDollar(totalPnl); el.vstatPnl.className = "vstat-val " + (totalPnl > 0.005 ? "pos" : totalPnl < -0.005 ? "neg" : ""); el.vstatPnlLbl.textContent = `Total profit at $${state.bankroll.toLocaleString()} per trade`; el.vstatRoi.textContent = trades > 0 ? formatSignedPct(roi) : "—"; el.vstatRoi.className = "vstat-val " + (roi > 0.0001 ? "pos" : roi < -0.0001 ? "neg" : ""); el.vstatTrades.textContent = `${trades} of ${eventCount}`; el.vstatTrades.className = "vstat-val"; el.vstatWinrate.textContent = trades > 0 ? `${(result.winRate * 100).toFixed(1)}%` : "—"; el.vstatWinrate.className = "vstat-val"; // Annualized profit: scale the total by (365 / span) const annualPnl = totalPnl * (365 / state.spanDays); el.vstatAnnual.textContent = trades > 0 ? formatSignedDollar(annualPnl) : "—"; el.vstatAnnual.className = "vstat-val " + (annualPnl > 0.005 ? "pos" : annualPnl < -0.005 ? "neg" : ""); el.verdictExplainer.innerHTML = strategy.why; } function renderTradeList() { const strategy = STRATEGIES.find(s => s.key === state.activeKey); const result = state.results[strategy.key]; const all = result.rows; const filters = { all: (r) => true, trades: (r) => r.result.action === "trade", wins: (r) => r.result.action === "trade" && r.pnl > 0, losses: (r) => r.result.action === "trade" && r.pnl < 0, skipped: (r) => r.result.action === "skip", }; const filtered = all.filter(filters[state.tradeFilter]); // counts el.cntAll.textContent = all.length; el.cntTrades.textContent = all.filter(filters.trades).length; el.cntWins.textContent = all.filter(filters.wins).length; el.cntLosses.textContent = all.filter(filters.losses).length; el.cntSkipped.textContent = all.filter(filters.skipped).length; // sort: trades first (by |pnl| desc), then skipped filtered.sort((a, b) => { const aAct = a.result.action === "trade" ? 0 : 1; const bAct = b.result.action === "trade" ? 0 : 1; if (aAct !== bAct) return aAct - bAct; return Math.abs(b.pnl) - Math.abs(a.pnl); }); el.tradeList.innerHTML = ""; if (!filtered.length) { const empty = document.createElement("div"); empty.className = "trade-show-more"; empty.style.cursor = "default"; empty.textContent = "No trades match this filter."; el.tradeList.appendChild(empty); return; } // Show every row. If you claim N trades, you show N trades. for (const r of filtered) { el.tradeList.appendChild(renderTradeRow(r)); } const footer = document.createElement("div"); footer.className = "trade-count-footer"; footer.innerHTML = `Showing all ${filtered.length} ${filtered.length === 1 ? "row" : "rows"} · download as CSV`; el.tradeList.appendChild(footer); const dl = document.getElementById("csv-download"); if (dl) dl.addEventListener("click", (e) => { e.preventDefault(); downloadCsv(); }); } function downloadCsv() { const strategy = STRATEGIES.find(s => s.key === state.activeKey); const result = state.results[strategy.key]; const rows = [["event_title", "neg_risk", "num_outcomes", "action", "cost", "payout", "pnl", "note"]]; for (const r of result.rows) { rows.push([ r.event.title, String(r.event.neg_risk), String(r.event.num_outcomes), r.result.action, (r.result.cost || 0).toFixed(4), (r.result.payout || 0).toFixed(4), r.pnl.toFixed(4), (r.result.note || "").replace(/[\r\n]+/g, " "), ]); } const csv = rows.map(row => row.map(v => { const s = String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }).join(",")).join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `polymarket-backtest-${strategy.key}.csv`; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); document.body.removeChild(a); }, 0); } function renderTradeRow(r) { const row = document.createElement("div"); const didTrade = r.result.action === "trade"; const cls = didTrade ? (r.pnl > 0 ? "win" : r.pnl < 0 ? "loss" : "skip") : "skip"; row.className = "trade-row " + cls; // Scale by bankroll: if backtest cost was $0.40 for one share, and bankroll is // $1000, the trader would buy $1000/$0.40 = 2500 units — scaled pnl = roi * bankroll. const unitRoi = r.result.cost > 0 ? (r.pnl / r.result.cost) : 0; const scaledCost = didTrade ? state.bankroll : 0; const scaledPayout = didTrade ? state.bankroll * (1 + unitRoi) : 0; const scaledPnl = scaledPayout - scaledCost; const meta = didTrade ? `paid $${scaledCost.toLocaleString(undefined, {maximumFractionDigits:2})} → got back $${scaledPayout.toLocaleString(undefined, {maximumFractionDigits:2})}` : (r.result.note || "Strategy did not trade this event."); const resultCell = didTrade ? (scaledPnl > 0.005 ? `+$${scaledPnl.toLocaleString(undefined, {maximumFractionDigits:2})}` : scaledPnl < -0.005 ? `-$${Math.abs(scaledPnl).toLocaleString(undefined, {maximumFractionDigits:2})}` : `$0.00`) : `skipped`; row.innerHTML = `
${escapeHtml(s.oneLiner)}
${s.why}