mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-19 18:48:10 +00:00
Block audited losing signal cohorts
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
const GAMMA = "https://gamma-api.polymarket.com";
|
||||
const EVENT_LIMIT = Math.max(20, Math.min(1000, Number(process.env.NEG_RISK_EVENTS || 300)));
|
||||
const COST_CENTS = Math.max(0, Math.min(5, Number(process.env.NEG_RISK_COST_CENTS || 0.5)));
|
||||
const MIN_LIQUIDITY = Math.max(0, Number(process.env.NEG_RISK_MIN_LIQUIDITY || 1000));
|
||||
|
||||
async function fetchJson(url, attempts = 3) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(20000), headers: { accept: "application/json" } });
|
||||
if (response.ok) return response.json();
|
||||
lastError = new Error(`${response.status} ${response.statusText}`);
|
||||
if (response.status !== 429 && response.status < 500) break;
|
||||
} catch (error) { lastError = error; }
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
|
||||
}
|
||||
throw lastError || new Error("request failed");
|
||||
}
|
||||
|
||||
function parseJson(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
try { return JSON.parse(value || "[]"); } catch { return []; }
|
||||
}
|
||||
|
||||
function number(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
async function fetchEvents(limit) {
|
||||
const events = [], pageSize = 100;
|
||||
for (let offset = 0; offset < limit; offset += pageSize) {
|
||||
const size = Math.min(pageSize, limit - offset);
|
||||
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false",
|
||||
limit: String(size), offset: String(offset), order: "volume24hr", ascending: "false" });
|
||||
const page = await fetchJson(`${GAMMA}/events?${params}`);
|
||||
if (!Array.isArray(page) || !page.length) break;
|
||||
events.push(...page);
|
||||
if (page.length < size) break;
|
||||
}
|
||||
return events.slice(0, limit);
|
||||
}
|
||||
|
||||
function evaluateEvent(event) {
|
||||
if (!event?.negRisk || event.enableNegRisk === false) return null;
|
||||
const allMarkets = Array.isArray(event.markets) ? event.markets : [];
|
||||
if (allMarkets.length < 2 || allMarkets.some((market) => market.closed || market.active === false || market.acceptingOrders === false)) return null;
|
||||
const legs = allMarkets.map((market) => {
|
||||
const prices = parseJson(market.outcomePrices).map(number);
|
||||
return { id: String(market.id || ""), question: market.question || "", yes: prices[0],
|
||||
bid: number(market.bestBid), ask: number(market.bestAsk), liquidity: number(market.liquidityNum || market.liquidity) || 0 };
|
||||
});
|
||||
if (legs.some((leg) => !leg.id || leg.yes == null || leg.bid == null || leg.ask == null
|
||||
|| leg.bid < 0 || leg.ask > 1 || leg.ask < leg.bid || leg.liquidity < MIN_LIQUIDITY)) return null;
|
||||
const count = legs.length, costPerLeg = COST_CENTS / 100;
|
||||
const yesCost = legs.reduce((sum, leg) => sum + leg.ask, 0);
|
||||
const yesProfit = 1 - yesCost - count * costPerLeg;
|
||||
const noCost = count - legs.reduce((sum, leg) => sum + leg.bid, 0);
|
||||
const noProfit = count - 1 - noCost - count * costPerLeg;
|
||||
const yesReturn = yesCost > 0 ? yesProfit / yesCost : 0;
|
||||
const noReturn = noCost > 0 ? noProfit / noCost : 0;
|
||||
const side = yesReturn >= noReturn ? "YES_BUNDLE" : "NO_BUNDLE";
|
||||
return { eventId: String(event.id || ""), title: event.title || "", slug: event.slug || "", markets: count,
|
||||
minimumLiquidity: Math.min(...legs.map((leg) => leg.liquidity)), side,
|
||||
executableCost: side === "YES_BUNDLE" ? yesCost : noCost,
|
||||
worstCasePayout: side === "YES_BUNDLE" ? 1 : count - 1,
|
||||
netProfitPerBundle: side === "YES_BUNDLE" ? yesProfit : noProfit,
|
||||
netReturn: side === "YES_BUNDLE" ? yesReturn : noReturn,
|
||||
theoreticalYesSum: legs.reduce((sum, leg) => sum + leg.yes, 0), legs };
|
||||
}
|
||||
|
||||
const events = await fetchEvents(EVENT_LIMIT);
|
||||
const evaluated = events.map(evaluateEvent).filter(Boolean).sort((a, b) => b.netReturn - a.netReturn);
|
||||
const actionable = evaluated.filter((event) => event.netProfitPerBundle > 0);
|
||||
const compact = (event) => ({ eventId: event.eventId, title: event.title, markets: event.markets, side: event.side,
|
||||
executableCost: +event.executableCost.toFixed(4), worstCasePayout: event.worstCasePayout,
|
||||
netProfitPerBundle: +event.netProfitPerBundle.toFixed(4), netReturn: +event.netReturn.toFixed(4),
|
||||
minimumLiquidity: +event.minimumLiquidity.toFixed(2), theoreticalYesSum: +event.theoreticalYesSum.toFixed(4),
|
||||
url: event.slug ? `https://polymarket.com/event/${event.slug}` : "" });
|
||||
|
||||
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedEvents: EVENT_LIMIT,
|
||||
fetchedEvents: events.length, eligibleNegativeRiskEvents: evaluated.length, actionableBundles: actionable.length,
|
||||
estimatedCostCentsPerLeg: COST_CENTS, minimumLiquidityPerLeg: MIN_LIQUIDITY,
|
||||
actionable: actionable.slice(0, 50).map(compact), bestObserved: evaluated.slice(0, 20).map(compact) }, null, 2));
|
||||
Reference in New Issue
Block a user