Scan live deadline dominance bundles

This commit is contained in:
Theodore Song
2026-08-21 15:01:12 -04:00
parent 32f5d78374
commit 03dbed706a
4 changed files with 172 additions and 28 deletions
+61 -3
View File
@@ -48,6 +48,9 @@ const THRESHOLD_PATTERNS = [
{ direction: "above", outcomeMode: "yes-no", regex: /(\b(?:above|over|at least|higher than|greater than)\s*(?:[$€£]\s*)?)([0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?)\s*(k|m|b|%|bps)?\b/i },
{ direction: "below", outcomeMode: "yes-no", regex: /(\b(?:below|(?<!\/)under|at most|lower than|less than)\s*(?:[$€£]\s*)?)([0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?)\s*(k|m|b|%|bps)?\b/i },
];
const MONTHS = Object.freeze({ january: 1, february: 2, march: 3, april: 4, may: 5, june: 6,
july: 7, august: 8, september: 9, october: 10, november: 11, december: 12 });
const DEADLINE_PATTERN = /\b((?:on\s+or\s+)?(?:by|before)\s+(?:the\s+)?)(january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{1,2})(?:st|nd|rd|th)?(?:,?\s+(\d{4}))?\b/i;
function parseThreshold(question) {
const text = String(question || "").trim();
@@ -66,6 +69,17 @@ function parseThreshold(question) {
return null;
}
function parseDeadline(question) {
const text = String(question || "").trim(), match = DEADLINE_PATTERN.exec(text);
if (!match) return null;
const month = MONTHS[String(match[2]).toLowerCase()], day = Number(match[3]), year = match[4] ? Number(match[4]) : null;
if (!month || !Number.isInteger(day) || day < 1 || day > new Date(Date.UTC(year || 2024, month, 0)).getUTCDate()) return null;
const dateStart = match.index + match[1].length, dateEnd = match.index + match[0].length;
const stem = `${text.slice(0, dateStart)}{deadline}${text.slice(dateEnd)}`.toLowerCase().replace(/\s+/g, " ").trim();
return { direction: "deadline", value: year ? year * 10000 + month * 100 + day : month * 100 + day,
yearMode: year ? "explicit-year" : "implicit-year", stem };
}
function quoteMarket(raw, event) {
const threshold = parseThreshold(raw.question);
const tokens = parseJson(raw.clobTokenIds).map(String), prices = parseJson(raw.outcomePrices).map(number);
@@ -111,16 +125,60 @@ function evaluateEvent(event) {
return candidates;
}
function evaluateDeadlineEvent(event) {
const quotes = (Array.isArray(event.markets) ? event.markets : []).map((raw) => {
const deadline = parseDeadline(raw.question), tokens = parseJson(raw.clobTokenIds).map(String);
const prices = parseJson(raw.outcomePrices).map(number), outcomes = parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const bid = number(raw.bestBid), ask = number(raw.bestAsk), liquidity = number(raw.liquidityNum || raw.liquidity) || 0;
if (!deadline || tokens.length !== 2 || prices.length !== 2 || outcomes[0] !== "yes" || outcomes[1] !== "no" || !tokens[0] || !tokens[1]
|| bid == null || ask == null || bid < 0 || ask > 1 || ask < bid || liquidity < MIN_LIQUIDITY
|| raw.closed || raw.active === false || raw.acceptingOrders === false) return null;
return { ...deadline, marketId: String(raw.id || ""), question: raw.question || "", yesBid: bid, yesAsk: ask,
yesMid: prices[0], noMid: prices[1], yesToken: tokens[0], noToken: tokens[1], liquidity,
url: event.slug ? `https://polymarket.com/event/${event.slug}` : "" };
}).filter(Boolean);
const groups = new Map();
for (const quote of quotes) {
const key = `${quote.yearMode}|${quote.stem}`, group = groups.get(key) || [];
group.push(quote); groups.set(key, group);
}
const candidates = [];
for (const group of groups.values()) {
if (group.length < 2 || new Set(group.map((quote) => quote.value)).size !== group.length) continue;
const ordered = [...group].sort((a, b) => a.value - b.value);
for (let left = 0; left < ordered.length - 1; left++) {
for (let right = left + 1; right < ordered.length; right++) {
const earlier = ordered[left], later = ordered[right];
const noEntry = 1 - earlier.yesBid + COST_CENTS / 100, yesEntry = later.yesAsk + COST_CENTS / 100;
const cost = noEntry + yesEntry, profit = 1 - cost, netReturn = cost > 0 ? profit / cost : 0;
candidates.push({ eventId: String(event.id || ""), title: event.title || "", direction: "deadline",
earlierDeadline: earlier.value, laterDeadline: later.value, cost, payout: 1, profit, netReturn,
minimumLiquidity: Math.min(earlier.liquidity, later.liquidity), url: earlier.url,
legs: [{ marketId: earlier.marketId, question: earlier.question, side: "NO", entry: noEntry },
{ marketId: later.marketId, question: later.question, side: "YES", entry: yesEntry }] });
}
}
}
return candidates;
}
const events = await fetchEvents(EVENT_LIMIT);
const candidates = events.flatMap(evaluateEvent).sort((a, b) => b.netReturn - a.netReturn);
const thresholdCandidates = events.flatMap(evaluateEvent), deadlineCandidates = events.flatMap(evaluateDeadlineEvent);
const candidates = [...thresholdCandidates, ...deadlineCandidates].sort((a, b) => b.netReturn - a.netReturn);
const actionable = candidates.filter((candidate) => candidate.profit >= MIN_NET_PROFIT && candidate.netReturn >= MIN_NET_RETURN);
const actionableThresholds = actionable.filter((candidate) => candidate.direction !== "deadline");
const actionableDeadlines = actionable.filter((candidate) => candidate.direction === "deadline");
const compact = (candidate) => ({ eventId: candidate.eventId, title: candidate.title, direction: candidate.direction,
supersetThreshold: candidate.supersetThreshold, subsetThreshold: candidate.subsetThreshold,
earlierDeadline: candidate.earlierDeadline, laterDeadline: candidate.laterDeadline,
cost: +candidate.cost.toFixed(4), payout: candidate.payout, profit: +candidate.profit.toFixed(4),
netReturn: +candidate.netReturn.toFixed(4), minimumLiquidity: +candidate.minimumLiquidity.toFixed(2),
url: candidate.url, legs: candidate.legs.map((leg) => ({ ...leg, entry: +leg.entry.toFixed(4) })) });
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedEvents: EVENT_LIMIT, fetchedEvents: events.length,
eligibleDominancePairs: candidates.length, actionablePairs: actionable.length, estimatedCostCentsPerLeg: COST_CENTS,
eligibleDominancePairs: candidates.length, thresholdPairs: thresholdCandidates.length, deadlinePairs: deadlineCandidates.length,
actionablePairs: actionable.length, actionableThresholdPairs: actionableThresholds.length, actionableDeadlinePairs: actionableDeadlines.length,
estimatedCostCentsPerLeg: COST_CENTS,
minimumLiquidityPerLeg: MIN_LIQUIDITY, minimumNetProfitPerPair: MIN_NET_PROFIT, minimumNetReturn: MIN_NET_RETURN,
actionable: actionable.slice(0, 50).map(compact), bestObserved: candidates.slice(0, 20).map(compact) }, null, 2));
actionable: actionable.slice(0, 50).map(compact), bestObserved: candidates.slice(0, 20).map(compact),
bestDeadlineObserved: deadlineCandidates.sort((a, b) => b.netReturn - a.netReturn).slice(0, 20).map(compact) }, null, 2));