mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-24 13:08:10 +00:00
Launch bounded sports contest exploration
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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" } }]));
|
||||
|
||||
Reference in New Issue
Block a user