polymarket slug
This commit is contained in:
193
lib/feeds.ts
193
lib/feeds.ts
@@ -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_SERIES = "11433"; // WM 2026 Serie
|
||||
|
||||
interface PmEvent {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
markets?: 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;
|
||||
groupItemTitle: string;
|
||||
outcomes: string; // JSON-String ["Yes","No"]
|
||||
outcomePrices: string; // JSON-String ["0.385","0.615"]
|
||||
sportsMarketType?: string;
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
// Map: normalisierter Teamname-Schlüssel -> Wahrscheinlichkeit.
|
||||
export interface OddsEntry {
|
||||
outcomes: string[];
|
||||
prices: number[];
|
||||
question: string;
|
||||
export interface ParsedOdds {
|
||||
homeCode: string;
|
||||
awayCode: string;
|
||||
homeName: 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 {
|
||||
try { return JSON.parse(s) as T; } catch { return fallback; }
|
||||
}
|
||||
|
||||
// Holt WM-bezogene Märkte über das Tag/Slug der Polymarket-WM-Kollektion.
|
||||
// Hinweis: Der exakte Slug kann sich ändern; per ENV überschreibbar.
|
||||
export async function fetchOdds(): Promise<OddsEntry[]> {
|
||||
const slug = process.env.POLYMARKET_WC_SLUG || "world-cup-2026";
|
||||
// Holt alle WM-Spiele über den Series-Endpoint mit Pagination.
|
||||
// Parst pro Event die drei Moneyline-Märkte (Heim/Draw/Auswärts).
|
||||
export async function fetchOdds(): Promise<ParsedOdds[]> {
|
||||
return cached("pm:odds", 120_000, async () => {
|
||||
const res = await fetch(
|
||||
`${PM_BASE}/events?slug=${encodeURIComponent(slug)}`,
|
||||
{ cache: "no-store", headers: { "User-Agent": "wm2026-board/1.0" } },
|
||||
);
|
||||
if (!res.ok) throw new Error(`polymarket ${res.status}`);
|
||||
const events = (await res.json()) as Array<{ markets?: PmMarket[] }>;
|
||||
const entries: OddsEntry[] = [];
|
||||
for (const ev of events) {
|
||||
const allEvents: PmEvent[] = [];
|
||||
for (let offset = 0; ; offset += 100) {
|
||||
const url = `${PM_BASE}/events?series_id=${PM_SERIES}&active=true&closed=false&limit=100&offset=${offset}`;
|
||||
const res = await fetch(url, {
|
||||
cache: "no-store",
|
||||
headers: { "User-Agent": "wm2026-board/1.0" },
|
||||
});
|
||||
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 ?? []) {
|
||||
if (mk.sportsMarketType && mk.sportsMarketType !== "moneyline") continue;
|
||||
if (mk.closed) continue;
|
||||
const outcomes = safeParse<string[]>(mk.outcomes, []);
|
||||
const prices = safeParse<string[]>(mk.outcomePrices, []).map(Number);
|
||||
if (outcomes.length && outcomes.length === prices.length) {
|
||||
entries.push({ outcomes, prices, question: mk.question });
|
||||
if (prices.length < 2) continue;
|
||||
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.
|
||||
export function attachOdds(matches: Match[], teams: Team[], odds: OddsEntry[]): Match[] {
|
||||
const nameById = new Map(teams.map((t) => [t.id, t.name.toLowerCase()]));
|
||||
// Normalisiert Teamnamen für Matching (lowercase, diakritische Zeichen, aliase).
|
||||
function normName(n: string): string {
|
||||
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) => {
|
||||
if (!m.homeTeamId || !m.awayTeamId) return m;
|
||||
const hn = nameById.get(m.homeTeamId);
|
||||
const an = nameById.get(m.awayTeamId);
|
||||
if (!hn || !an) return m;
|
||||
const hit = odds.find(
|
||||
(o) => o.question.toLowerCase().includes(hn) && o.question.toLowerCase().includes(an),
|
||||
);
|
||||
if (!hit) return m;
|
||||
// Heuristik: 3 Outcomes = Heim/Unentschieden/Auswärts; 2 = Heim/Auswärts.
|
||||
const idxHome = hit.outcomes.findIndex((o) => o.toLowerCase().includes(hn));
|
||||
const idxAway = hit.outcomes.findIndex((o) => o.toLowerCase().includes(an));
|
||||
const idxDraw = hit.outcomes.findIndex((o) => /draw|unentschieden|tie/i.test(o));
|
||||
if (idxHome < 0 || idxAway < 0) return m;
|
||||
return {
|
||||
...m,
|
||||
prob: {
|
||||
home: hit.prices[idxHome] ?? 0,
|
||||
draw: idxDraw >= 0 ? (hit.prices[idxDraw] ?? null) : null,
|
||||
away: hit.prices[idxAway] ?? 0,
|
||||
},
|
||||
};
|
||||
// Suche in oddsMatch nach einer Kombination die beide Team-IDs matcht
|
||||
for (const [o, ids] of oddsMatch) {
|
||||
if (!ids) continue;
|
||||
// Beide Richtungen prüfen
|
||||
if ((ids.homeId === m.homeTeamId && ids.awayId === m.awayTeamId) ||
|
||||
(ids.homeId === m.awayTeamId && ids.awayId === m.homeTeamId)) {
|
||||
const swapped = ids.homeId === m.awayTeamId;
|
||||
return {
|
||||
...m,
|
||||
prob: {
|
||||
home: swapped ? o.pAway : o.pHome,
|
||||
draw: o.pDraw,
|
||||
away: swapped ? o.pHome : o.pAway,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return m;
|
||||
});
|
||||
}
|
||||
@@ -23,18 +23,45 @@ export function saveOverrides(overrides: SimOverrides): void {
|
||||
} catch { /* quota exceeded */ }
|
||||
}
|
||||
|
||||
// Plausibles Standardergebnis aus Polymarket-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).
|
||||
// Plausibles Standardergebnis aus Polymarket-3-Wege-Wahrscheinlichkeiten.
|
||||
//
|
||||
// Schwellen (zentral):
|
||||
// Draw am höchsten ODER max(home,away) < 0.40 → 0:0 (Gruppe) / 1:0 Heim (K.o.)
|
||||
// Favorit 0.40–0.60 → 1:0
|
||||
// Favorit 0.60–0.78 → 2:0
|
||||
// Favorit > 0.78 → 3:0
|
||||
//
|
||||
// Beispiele:
|
||||
// Schweiz–Kanada (0.385/0.315/0.295) → 0:0
|
||||
// Schottland–Brasilien (0.10/0.17/0.75) → 0:2
|
||||
// Marokko–Haiti (0.83/0.13/0.05) → 3:0
|
||||
export function defaultScore(match: Match): { homeScore: number; awayScore: number } | null {
|
||||
const prob = match.prob;
|
||||
if (!prob) {
|
||||
if (match.stage === "GROUP") return { homeScore: 1, awayScore: 1 };
|
||||
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 favProb = Math.max(prob.home, prob.away);
|
||||
const goals = favProb >= 0.65 ? 2 : 1;
|
||||
let goals: number;
|
||||
if (maxFA > 0.78) {
|
||||
goals = 3;
|
||||
} else if (maxFA >= 0.60) {
|
||||
goals = 2;
|
||||
} else {
|
||||
goals = 1; // 0.40–0.60
|
||||
}
|
||||
|
||||
if (homeFav) return { homeScore: goals, awayScore: 0 };
|
||||
return { homeScore: 0, awayScore: goals };
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface Match {
|
||||
homeScore: number | null;
|
||||
awayScore: number | null;
|
||||
// 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
|
||||
attendance?: number | null; // Zuschauerzahl, falls verfügbar
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user