polymarket slug

This commit is contained in:
2026-06-24 11:28:26 -05:00
parent 253b8cfbcd
commit ddaa1db876
4 changed files with 185 additions and 57 deletions

View File

@@ -16,12 +16,16 @@ export async function GET() {
try { try {
const odds = await fetchOdds(); const odds = await fetchOdds();
matches = attachOdds(rawMatches, teams, odds); matches = attachOdds(rawMatches, teams, odds);
} catch { } catch (err) {
// still ohne Wahrscheinlichkeiten console.error("[polymarket] fetchOdds fehlgeschlagen:", err);
} }
const groupTables = computeGroupTables(teams, matches); const groupTables = computeGroupTables(teams, matches);
const groupTablesLive = computeGroupTables(teams, matches, true); const groupTablesLive = computeGroupTables(teams, matches, true);
// prob-Feld normalisieren: immer null statt undefined, damit JSON konsistent ist
const normalizedMatches = matches.map((m) => ({ ...m, prob: m.prob ?? null }));
const thirdTable = computeThirdPlaceTable(groupTablesLive); const thirdTable = computeThirdPlaceTable(groupTablesLive);
const qGroups = qualifiedThirdGroups(thirdTable); const qGroups = qualifiedThirdGroups(thirdTable);
const annex = qGroups.length === 8 ? resolveAnnexC(qGroups) : null; const annex = qGroups.length === 8 ? resolveAnnexC(qGroups) : null;
@@ -29,7 +33,7 @@ export async function GET() {
return NextResponse.json({ return NextResponse.json({
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
teams, teams,
matches, matches: normalizedMatches,
groupTables, groupTables,
groupTablesLive, groupTablesLive,
thirdTable, thirdTable,

View File

@@ -159,79 +159,176 @@ export async function fetchMatchesAndTeams(): Promise<{ matches: Match[]; teams:
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Polymarket Gamma API: Wahrscheinlichkeiten pro Spiel // Polymarket Gamma API: Wahrscheinlichkeiten pro Spiel (Series soccer-fifwc)
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
const PM_BASE = "https://gamma-api.polymarket.com"; const PM_BASE = "https://gamma-api.polymarket.com";
const PM_SERIES = "11433"; // WM 2026 Serie
interface PmEvent {
id: string;
slug: string;
title: string;
markets?: PmMarket[];
}
interface PmMarket { interface PmMarket {
question: string;
outcomes: string; // JSON-String, z.B. "[\"Germany\",\"Draw\",\"Mexico\"]"
outcomePrices: string; // JSON-String, z.B. "[\"0.55\",\"0.25\",\"0.20\"]"
slug: string; slug: string;
groupItemTitle: string;
outcomes: string; // JSON-String ["Yes","No"]
outcomePrices: string; // JSON-String ["0.385","0.615"]
sportsMarketType?: string;
closed: boolean; closed: boolean;
} }
// Map: normalisierter Teamname-Schlüssel -> Wahrscheinlichkeit. export interface ParsedOdds {
export interface OddsEntry { homeCode: string;
outcomes: string[]; awayCode: string;
prices: number[]; homeName: string;
question: string; awayName: string;
pHome: number;
pDraw: number;
pAway: number;
}
// Normalisiert Teamcodes zwischen Polymarket-Slug und Feed (3-Buchstaben).
// Bekannte Abweichungen: kr/kor (Südkorea), nld/ned (Niederlande), cdr/cod (DR Kongo).
const CODE_NORM: Record<string, string> = {
kor: "kr",
nld: "ned",
cdr: "cod",
cvi: "cpv",
rsa: "rsa", // ok
swe: "swe", // ok
};
function normCode(c: string): string {
return (CODE_NORM[c] ?? c).toLowerCase();
} }
function safeParse<T>(s: string, fallback: T): T { function safeParse<T>(s: string, fallback: T): T {
try { return JSON.parse(s) as T; } catch { return fallback; } try { return JSON.parse(s) as T; } catch { return fallback; }
} }
// Holt WM-bezogene Märkte über das Tag/Slug der Polymarket-WM-Kollektion. // Holt alle WM-Spiele über den Series-Endpoint mit Pagination.
// Hinweis: Der exakte Slug kann sich ändern; per ENV überschreibbar. // Parst pro Event die drei Moneyline-Märkte (Heim/Draw/Auswärts).
export async function fetchOdds(): Promise<OddsEntry[]> { export async function fetchOdds(): Promise<ParsedOdds[]> {
const slug = process.env.POLYMARKET_WC_SLUG || "world-cup-2026";
return cached("pm:odds", 120_000, async () => { return cached("pm:odds", 120_000, async () => {
const res = await fetch( const allEvents: PmEvent[] = [];
`${PM_BASE}/events?slug=${encodeURIComponent(slug)}`, for (let offset = 0; ; offset += 100) {
{ cache: "no-store", headers: { "User-Agent": "wm2026-board/1.0" } }, const url = `${PM_BASE}/events?series_id=${PM_SERIES}&active=true&closed=false&limit=100&offset=${offset}`;
); const res = await fetch(url, {
if (!res.ok) throw new Error(`polymarket ${res.status}`); cache: "no-store",
const events = (await res.json()) as Array<{ markets?: PmMarket[] }>; headers: { "User-Agent": "wm2026-board/1.0" },
const entries: OddsEntry[] = []; });
for (const ev of events) { if (!res.ok) throw new Error(`polymarket ${res.status}`);
const page = (await res.json()) as PmEvent[];
allEvents.push(...page);
if (page.length < 100) break;
}
//console.log("[polymarket] events fetched:", allEvents.length);
const parsed: ParsedOdds[] = [];
for (const ev of allEvents) {
// Slug: fifwc-{home}-{away}-{yyyy}-{mm}-{dd}
const slugParts = ev.slug.replace("fifwc-", "").split("-");
if (slugParts.length < 5) continue;
const homeCode = normCode(slugParts[0]);
const awayCode = normCode(slugParts[1]);
let pHome = 0, pDraw = 0, pAway = 0;
let homeName = "", awayName = "";
for (const mk of ev.markets ?? []) { for (const mk of ev.markets ?? []) {
if (mk.sportsMarketType && mk.sportsMarketType !== "moneyline") continue;
if (mk.closed) continue; if (mk.closed) continue;
const outcomes = safeParse<string[]>(mk.outcomes, []);
const prices = safeParse<string[]>(mk.outcomePrices, []).map(Number); const prices = safeParse<string[]>(mk.outcomePrices, []).map(Number);
if (outcomes.length && outcomes.length === prices.length) { if (prices.length < 2) continue;
entries.push({ outcomes, prices, question: mk.question }); const yes = prices[0]; // erster Preis = "Yes"-Wahrscheinlichkeit
const gti = mk.groupItemTitle.toLowerCase();
if (mk.slug.endsWith("-draw") || gti.startsWith("draw")) {
pDraw = yes;
} else {
// Heuristik: match gegen groupItemTitle (Teamname)
// Prüfe ob der Titel den Home-Code oder Away-Code enthält
// Besser: erster Markt = Home, dritter = Away; Draw in der Mitte.
// Da die Reihenfolge nicht garantiert ist: über slug-Suffix matchen.
if (mk.slug.endsWith("-" + slugParts[0].toLowerCase())) {
pHome = yes;
homeName = mk.groupItemTitle;
} else if (mk.slug.endsWith("-" + slugParts[1].toLowerCase())) {
pAway = yes;
awayName = mk.groupItemTitle;
}
} }
} }
if (pHome > 0 || pDraw > 0 || pAway > 0) {
parsed.push({ homeCode, awayCode, homeName, awayName, pHome, pDraw, pAway });
}
} }
return entries;
console.log("[polymarket] spiele:", parsed.length);
return parsed;
}); });
} }
// Verknüpft Polymarket-Märkte mit Spielen anhand der Teamnamen im Markttitel. // Normalisiert Teamnamen für Matching (lowercase, diakritische Zeichen, aliase).
export function attachOdds(matches: Match[], teams: Team[], odds: OddsEntry[]): Match[] { function normName(n: string): string {
const nameById = new Map(teams.map((t) => [t.id, t.name.toLowerCase()])); return n
.toLowerCase()
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // diakritische Zeichen entfernen
.replace(/[^a-z0-9 ]/g, "")
.replace(/\s+/g, " ")
.trim();
}
// Verknüpft Polymarket-Odds mit Feed-Matches über Teamnamen (primär) oder Codes.
export function attachOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]): Match[] {
// Map: Team-ID → normierter Name
const nameById = new Map(teams.map((t) => [t.id, normName(t.name)]));
// Map: normierter Name → Team-ID (für Lookup)
const idByName = new Map<string, string>();
for (const t of teams) {
const nn = normName(t.name);
if (!idByName.has(nn)) idByName.set(nn, t.id);
}
// Map: normierter Code → Team-ID
const idByCode = new Map(teams.map((t) => [t.code.toLowerCase(), t.id]));
// Map: Polymarket ParsedOdds → { homeTeamId, awayTeamId }
const oddsMatch = new Map<ParsedOdds, { homeId: string; awayId: string } | null>();
for (const o of odds) {
const hn = normName(o.homeName);
const an = normName(o.awayName);
const homeId = idByName.get(hn) ?? idByCode.get(o.homeCode);
const awayId = idByName.get(an) ?? idByCode.get(o.awayCode);
if (homeId && awayId) {
oddsMatch.set(o, { homeId, awayId });
} else {
oddsMatch.set(o, null);
}
}
return matches.map((m) => { return matches.map((m) => {
if (!m.homeTeamId || !m.awayTeamId) return m; if (!m.homeTeamId || !m.awayTeamId) return m;
const hn = nameById.get(m.homeTeamId); // Suche in oddsMatch nach einer Kombination die beide Team-IDs matcht
const an = nameById.get(m.awayTeamId); for (const [o, ids] of oddsMatch) {
if (!hn || !an) return m; if (!ids) continue;
const hit = odds.find( // Beide Richtungen prüfen
(o) => o.question.toLowerCase().includes(hn) && o.question.toLowerCase().includes(an), if ((ids.homeId === m.homeTeamId && ids.awayId === m.awayTeamId) ||
); (ids.homeId === m.awayTeamId && ids.awayId === m.homeTeamId)) {
if (!hit) return m; const swapped = ids.homeId === m.awayTeamId;
// Heuristik: 3 Outcomes = Heim/Unentschieden/Auswärts; 2 = Heim/Auswärts. return {
const idxHome = hit.outcomes.findIndex((o) => o.toLowerCase().includes(hn)); ...m,
const idxAway = hit.outcomes.findIndex((o) => o.toLowerCase().includes(an)); prob: {
const idxDraw = hit.outcomes.findIndex((o) => /draw|unentschieden|tie/i.test(o)); home: swapped ? o.pAway : o.pHome,
if (idxHome < 0 || idxAway < 0) return m; draw: o.pDraw,
return { away: swapped ? o.pHome : o.pAway,
...m, },
prob: { };
home: hit.prices[idxHome] ?? 0, }
draw: idxDraw >= 0 ? (hit.prices[idxDraw] ?? null) : null, }
away: hit.prices[idxAway] ?? 0, return m;
},
};
}); });
} }

View File

@@ -23,18 +23,45 @@ export function saveOverrides(overrides: SimOverrides): void {
} catch { /* quota exceeded */ } } catch { /* quota exceeded */ }
} }
// Plausibles Standardergebnis aus Polymarket-Wahrscheinlichkeiten. // Plausibles Standardergebnis aus Polymarket-3-Wege-Wahrscheinlichkeiten.
// Favorit gewinnt 1:0; bei hoher W'keit (>=0.65) 2:0. //
// Ohne prob: Gruppe → 1:1, K.o. → null (Nutzer muss selbst eingeben). // Schwellen (zentral):
// Draw am höchsten ODER max(home,away) < 0.40 → 0:0 (Gruppe) / 1:0 Heim (K.o.)
// Favorit 0.400.60 → 1:0
// Favorit 0.600.78 → 2:0
// Favorit > 0.78 → 3:0
//
// Beispiele:
// SchweizKanada (0.385/0.315/0.295) → 0:0
// SchottlandBrasilien (0.10/0.17/0.75) → 0:2
// MarokkoHaiti (0.83/0.13/0.05) → 3:0
export function defaultScore(match: Match): { homeScore: number; awayScore: number } | null { export function defaultScore(match: Match): { homeScore: number; awayScore: number } | null {
const prob = match.prob; const prob = match.prob;
if (!prob) { if (!prob) {
if (match.stage === "GROUP") return { homeScore: 1, awayScore: 1 }; if (match.stage === "GROUP") return { homeScore: 1, awayScore: 1 };
return null; return null;
} }
const maxFA = Math.max(prob.home, prob.away);
const isDraw = prob.draw > prob.home && prob.draw > prob.away;
const isGroup = match.stage === "GROUP";
if (isDraw || maxFA < 0.40) {
if (isGroup) return { homeScore: 0, awayScore: 0 };
return { homeScore: 1, awayScore: 0 }; // K.o.: knapp Heim (kein Remis)
}
// Favorit bestimmen
const homeFav = prob.home >= prob.away; const homeFav = prob.home >= prob.away;
const favProb = Math.max(prob.home, prob.away); let goals: number;
const goals = favProb >= 0.65 ? 2 : 1; if (maxFA > 0.78) {
goals = 3;
} else if (maxFA >= 0.60) {
goals = 2;
} else {
goals = 1; // 0.400.60
}
if (homeFav) return { homeScore: goals, awayScore: 0 }; if (homeFav) return { homeScore: goals, awayScore: 0 };
return { homeScore: 0, awayScore: goals }; return { homeScore: 0, awayScore: goals };
} }

View File

@@ -33,7 +33,7 @@ export interface Match {
homeScore: number | null; homeScore: number | null;
awayScore: number | null; awayScore: number | null;
// Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden // Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden
prob?: { home: number; draw: number | null; away: number } | null; prob?: { home: number; draw: number; away: number } | null;
venue?: string | null; // Austragungsort venue?: string | null; // Austragungsort
attendance?: number | null; // Zuschauerzahl, falls verfügbar attendance?: number | null; // Zuschauerzahl, falls verfügbar
} }