Launch bounded sports contest exploration

This commit is contained in:
Theodore Song
2026-08-21 21:33:56 -04:00
parent c065ab5496
commit 066cad13da
8 changed files with 327 additions and 143 deletions
+49 -14
View File
@@ -1,10 +1,14 @@
const GAMMA = "https://gamma-api.polymarket.com";
const CLOB = "https://clob.polymarket.com";
const MARKET_LIMIT = Math.max(500, Math.min(5000, Number(process.env.CALIBRATION_MARKETS || 5000)));
const MARKET_SKIP = Math.max(0, Math.min(10000, Number(process.env.CALIBRATION_SKIP || 0)));
const CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.CALIBRATION_CONCURRENCY || 10)));
const COST = Math.max(0, Math.min(0.10, Number(process.env.CALIBRATION_COST_CENTS || 1) / 100));
const EXACT_GAMMA_FEES = process.env.CALIBRATION_EXACT_GAMMA_FEES === "1";
const HORIZONS = [...new Set(String(process.env.CALIBRATION_HORIZONS || "1,3,7,14,30")
.split(",").map(Number).filter((value) => Number.isFinite(value) && value >= 1 && value <= 180))].sort((a, b) => a - b);
const TARGET_RULE_IDS = String(process.env.CALIBRATION_TARGET_RULES || "3d_no_0.03-0.97_sports")
.split(",").map((value) => value.trim()).filter(Boolean);
const DAY = 86400;
function parseJson(value) {
@@ -29,6 +33,28 @@ function categoryOf(raw) {
return "Other";
}
function feeScheduleOf(raw) {
const rate = Number(raw?.feeSchedule?.rate), exponent = Number(raw?.feeSchedule?.exponent);
return Number.isFinite(rate) && rate >= 0 && Number.isFinite(exponent) && exponent > 0
? { rate, exponent } : null;
}
function takerFeePerShare(schedule, price) {
const p = Number(price), rate = Number(schedule?.rate), exponent = Number(schedule?.exponent);
if (!(p > 0 && p < 1) || !Number.isFinite(rate) || rate < 0 || !Number.isFinite(exponent) || exponent <= 0) return null;
return rate * Math.pow(p * (1 - p), exponent);
}
function sportsContestKey(raw, gameStartAt) {
const slug = String(raw?.slug || "").toLowerCase();
const datedPrefix = slug.match(/^(.+?-\d{4}-\d{2}-\d{2})(?:-|$)/)?.[1];
if (datedPrefix) return `sports:${datedPrefix}`;
const start = Number.isFinite(gameStartAt) ? String(gameStartAt) : "unknown-start";
const title = String(raw?.question || "").toLowerCase().split(":")[0]
.replace(/\b(will|win|exact score|leading at halftime|to score first)\b/g, " ").replace(/[^a-z0-9]+/g, " ").trim();
return `sports:${start}:${title || raw?.id || "unknown"}`;
}
async function fetchJson(url, attempts = 4) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
@@ -57,10 +83,11 @@ async function mapLimit(items, limit, task) {
return output;
}
async function fetchResolvedMarkets(limit) {
async function fetchResolvedMarkets(limit, skip = 0) {
const markets = [], seen = new Set();
const targetCount = limit + skip;
let cursor = "";
while (markets.length < limit) {
while (markets.length < targetCount) {
const params = new URLSearchParams({ closed: "true", order: "closedTime", ascending: "false", limit: "100", include_tag: "true" });
if (cursor) params.set("after_cursor", cursor);
const payload = await fetchJson(`${GAMMA}/markets/keyset?${params}`), page = payload?.markets;
@@ -69,17 +96,19 @@ async function fetchResolvedMarkets(limit) {
const id = String(raw.id || ""), labels = parseJson(raw.outcomes).map((value) => String(value).trim().toLowerCase());
const outcomes = parseJson(raw.outcomePrices).map(Number), tokens = parseJson(raw.clobTokenIds).map(String);
const finalYes = outcomes[0] >= 0.99 && outcomes[1] <= 0.01 ? 1 : outcomes[1] >= 0.99 && outcomes[0] <= 0.01 ? 0 : null;
const closedAt = timestamp(raw.closedTime || raw.endDate), createdAt = timestamp(raw.createdAt);
const closedAt = timestamp(raw.closedTime || raw.endDate), createdAt = timestamp(raw.createdAt), gameStartAt = timestamp(raw.gameStartTime);
if (!id || seen.has(id) || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2 || finalYes == null || !closedAt || !createdAt) continue;
seen.add(id);
markets.push({ id, question: raw.question || "", eventKey: String(raw.events?.[0]?.id || id), tokenId: tokens[0],
finalYes, closedAt, createdAt, category: categoryOf(raw) });
if (markets.length >= limit) break;
const category = categoryOf(raw), eventKey = category === "Sports" ? sportsContestKey(raw, gameStartAt) : String(raw.events?.[0]?.id || id);
markets.push({ id, question: raw.question || "", eventKey, tokenId: tokens[0],
finalYes, closedAt, createdAt, gameStartAt, decisionAnchor:category === "Sports" && gameStartAt ? gameStartAt : closedAt,
category, feeSchedule: feeScheduleOf(raw) });
if (markets.length >= targetCount) break;
}
if (page.length < 100 || !payload.next_cursor || payload.next_cursor === cursor) break;
cursor = payload.next_cursor;
}
return markets;
return markets.slice(skip, skip + limit);
}
function atOrBefore(points, target) {
@@ -94,7 +123,7 @@ function atOrBefore(points, target) {
function observations(market, points) {
return HORIZONS.flatMap((horizonDays) => {
const decisionAt = market.closedAt - horizonDays * DAY, point = atOrBefore(points, decisionAt);
const decisionAt = market.decisionAnchor - horizonDays * DAY, point = atOrBefore(points, decisionAt);
const recent = points.filter((candidate) => candidate.t >= decisionAt - 7 * DAY && candidate.t <= decisionAt);
const recentRange = recent.length ? Math.max(...recent.map((candidate) => candidate.p)) - Math.min(...recent.map((candidate) => candidate.p)) : 0;
if (!point || decisionAt < market.createdAt + DAY || decisionAt - point.t > 36 * 3600
@@ -102,9 +131,12 @@ function observations(market, points) {
return ["YES", "NO"].map((side) => {
const entry = side === "YES" ? point.p : 1 - point.p;
const won = side === (market.finalYes ? "YES" : "NO");
const fee = EXACT_GAMMA_FEES ? takerFeePerShare(market.feeSchedule, entry) : null;
const entryCost = COST + (EXACT_GAMMA_FEES && Number.isFinite(fee) ? fee : 0);
return { marketId: market.id, eventKey: market.eventKey, question: market.question, category: market.category,
closedAt: market.closedAt, decisionAt, horizonDays, side, entry, favorite: entry >= 0.5, won,
netReturn: (won ? 1 : 0) / entry - 1 - COST / entry };
closedAt: market.closedAt,gameStartAt:market.gameStartAt,decisionAt, horizonDays, side, entry, favorite: entry >= 0.5, won,
entryCost, exactFeeSchedule: EXACT_GAMMA_FEES && market.feeSchedule != null,
netReturn: (won ? 1 : 0) / entry - 1 - entryCost / entry };
});
});
}
@@ -176,7 +208,7 @@ function stabilityWindows(rows) {
&& (index === 3 || row.closedAt < cuts[index + 1])));
}
const markets = await fetchResolvedMarkets(MARKET_LIMIT);
const markets = await fetchResolvedMarkets(MARKET_LIMIT, MARKET_SKIP);
const histories = await mapLimit(markets, CONCURRENCY, async (market) => {
const data = await fetchJson(`${CLOB}/prices-history?market=${encodeURIComponent(market.tokenId)}&interval=max&fidelity=1440`);
const points = (data.history || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
@@ -205,14 +237,17 @@ const candidate = (row) => ({ rule: row.rule, train: compact(row.train), validat
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, resolvedMarkets: markets.length,
historiesWithData: usable.length, failures: histories.filter((row) => row?.error).length, observations: rows.length / 2,
methodology: { selection: "most recently closed eligible Yes/No markets", horizonsDays: HORIZONS,
historyFidelityMinutes: 1440, maximumPriceStalenessHours: 36, modeledCostCents: COST * 100,
selectionOffset: MARKET_SKIP,historyFidelityMinutes: 1440, maximumPriceStalenessHours: 36,
modeledEntrySlippageCents: COST * 100,exactGammaEntryFeeSchedules:EXACT_GAMMA_FEES,settlementRedemptionExitFeeCents:0,
split: "60% train / 20% validation / 20% untouched holdout", confidence: "event-clustered 95% lower bound",
stability: "positive event mean in each of four chronological windows; at most one highest-entry market per event",
stability: "positive event mean in each of four chronological windows; at most one highest-entry market per underlying event or sports contest",
sportsTiming:"published gameStartTime minus horizon; sports contracts sharing the dated contest slug prefix form one cluster",
activityGate: "market open for at least 24h with at least two recent observations and a 0.5-cent seven-day price range", testedRules: rules.length,
note: "No final volume or settlement outcome enters rule features. Market selection remains a recent-closure cohort, and midpoint-plus-cost is still an execution approximation." },
note: "No final volume or settlement outcome enters rule features. Market selection remains a closure-time cohort, and midpoint-plus-fee-plus-slippage is still an execution approximation." },
partitionRows: { train: partitions.train.length / 2, validation: partitions.validation.length / 2, holdout: partitions.holdout.length / 2 },
trainPassed: evaluated.filter((row) => row.trainPassed).length, validationSelected: selected.length,
holdoutPassed: selected.filter((row) => row.passesHoldout).length, candidates: selected.slice(0, 25).map(candidate),
targets: evaluated.filter((row) => TARGET_RULE_IDS.includes(row.rule.id)).map(candidate),
holdoutExamples: (selected.find((row) => row.passesHoldout)?.holdoutRows || []).slice(0, 15)
.map((row) => ({ marketId: row.marketId, question: row.question, category: row.category, side: row.side,
entry: +row.entry.toFixed(4), won: row.won, netReturn: +row.netReturn.toFixed(4),
+1 -1
View File
@@ -128,7 +128,7 @@ async function main() {
const branch = process.env.RUNTIME_BRANCH || "runtime-state";
const pathname = process.env.RUNTIME_STATE_PATH || "runtime/state.json";
const arenaUrl = (process.env.ARENA_URL || "https://polymarket-site-eta.vercel.app").replace(/\/$/, "");
const expectedBuild = Number(required("EXPECTED_BUILD", "105"));
const expectedBuild = Number(required("EXPECTED_BUILD", "106"));
await ensureRuntimeBranch(repository, branch);
const prior = await readRuntimeFile(repository, branch, pathname);
if (prior.snapshot) validateRuntimeSnapshot(prior.snapshot, 0, { allowIncomplete: true });
+14 -2
View File
@@ -7,9 +7,10 @@ const api = fs.readFileSync(new URL("../api/state.js", import.meta.url), "utf8")
const workflow = fs.readFileSync(new URL("../.github/workflows/autonomous-cycle.yml", import.meta.url), "utf8");
const runner = fs.readFileSync(new URL("./run-autonomous-cycle.mjs", import.meta.url), "utf8");
const resolutionAudit = JSON.parse(fs.readFileSync(new URL("../research/resolution-week-no-audit.json", import.meta.url), "utf8"));
const sportsContestAudit = JSON.parse(fs.readFileSync(new URL("../research/sports-contest-no-exploration-audit.json", import.meta.url), "utf8"));
const build = Number(index.match(/const BUILD_VERSION = (\d+);/)?.[1]);
assert.equal(build, 105);
assert.equal(build, 106);
assert.deepEqual([...ALLOWED_RUNTIME_KEYS].sort(), ["pma_agents_v2", "pma_suggestions_v5"]);
assert.match(index, /function collectPublicRuntimeItems\(\)/);
assert.match(index, /const PUBLIC_RUNTIME_KEYS=Object\.freeze\(\[AGENTS_KEY,SUG_KEY\]\)/);
@@ -26,7 +27,7 @@ assert.match(api, /Buffer\.from\(file\.content/);
assert.match(api, /searchParams\.set\("runtime", `\$\{Date\.now\(\)\}/);
assert.match(workflow, /cron: "2,7,12,17,22,27,32,37,42,47,52,57 \* \* \* \*"/);
assert.match(workflow, /contents: write/);
assert.match(workflow, /EXPECTED_BUILD: "105"/);
assert.match(workflow, /EXPECTED_BUILD: "106"/);
assert.match(index, /saveSuggestions\(sugs,markets\.length,analysisMarkets\.length,bundleAudit\)/);
assert.match(index, /const NEG_RISK_EVENT_SCAN_LIMIT=1000;/);
assert.match(index, /bundleOpportunityTelemetry:true/);
@@ -43,6 +44,17 @@ assert.equal(resolutionAudit.disjoint_holdout.passed_strict_gate, false);
assert.ok(resolutionAudit.disjoint_holdout.holdout_event_lower_90 < 0);
assert.equal(resolutionAudit.production_constraints.capital_enabled, false);
assert.equal(resolutionAudit.production_constraints.promotion_events, 40);
assert.equal(sportsContestAudit.production_strategy, 62);
assert.equal(sportsContestAudit.status, "bounded-paper-exploration-not-proven");
assert.equal(sportsContestAudit.corrected_5000_market_result.independent_contests, 76);
assert.equal(sportsContestAudit.corrected_5000_market_result.passed_strict_gate, false);
assert.ok(sportsContestAudit.corrected_5000_market_result.validation.lower_95 < 0);
assert.equal(sportsContestAudit.production_constraints.initial_position_pct, 0.5);
assert.equal(sportsContestAudit.production_constraints.total_lane_cap_pct, 3);
assert.equal(sportsContestAudit.production_constraints.exact_entry_fee_required, true);
assert.match(index, /function sportsContestKey\(m\)/);
assert.match(index, /function sportsContestNoSuggestions\(markets\)/);
assert.match(index, /const SPORTS_FAVORITE_MAX_NEW_PER_CYCLE=1;/);
const agentIds = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"];
const agents = Object.fromEntries(agentIds.map(id => [id, { cash: 10000, positions: [], lastDecision: { mode: "test" } }]));