third place fix und polymarket

This commit is contained in:
2026-06-27 14:03:11 -05:00
parent a721de5b23
commit 2b3cadfcb5
4 changed files with 361 additions and 43 deletions

View File

@@ -234,6 +234,7 @@ interface PmMarket {
}
export interface ParsedOdds {
slug: string;
homeCode: string;
awayCode: string;
homeName: string;
@@ -241,6 +242,7 @@ export interface ParsedOdds {
pHome: number;
pDraw: number;
pAway: number;
startTime: string | null;
}
// Normalisiert Teamcodes zwischen Polymarket-Slug und Feed (3-Buchstaben).
@@ -290,10 +292,12 @@ export async function fetchOdds(): Promise<ParsedOdds[]> {
let pHome = 0, pDraw = 0, pAway = 0;
let homeName = "", awayName = "";
let marketGameStartTime: string | null = null;
for (const mk of ev.markets ?? []) {
if (mk.sportsMarketType && mk.sportsMarketType !== "moneyline") continue;
if (mk.closed) continue;
if (!marketGameStartTime) marketGameStartTime = (mk as any).gameStartTime ?? null;
const prices = safeParse<string[]>(mk.outcomePrices, []).map(Number);
if (prices.length < 2) continue;
const yes = prices[0]; // erster Preis = "Yes"-Wahrscheinlichkeit
@@ -317,10 +321,22 @@ export async function fetchOdds(): Promise<ParsedOdds[]> {
}
if (pHome > 0 || pDraw > 0 || pAway > 0) {
parsed.push({ homeCode, awayCode, homeName, awayName, pHome, pDraw, pAway });
const rawStartDate = (ev as any).startDate ?? null;
const rawEndDate = (ev as any).endDate ?? null;
const rawGameStartTime = (ev as any).gameStartTime ?? null;
const startTime = marketGameStartTime ?? rawGameStartTime ?? rawEndDate ?? rawStartDate;
console.log(" [PM-RAW-ML]", ev.slug,
"| active:", (ev as any).active, "| closed:", (ev as any).closed,
"| startDate:", rawStartDate,
"| gameStartTime:", rawGameStartTime,
"| mkGameStartTime:", marketGameStartTime,
"| endDate:", rawEndDate);
parsed.push({ slug: ev.slug, homeCode, awayCode, homeName, awayName, pHome, pDraw, pAway, startTime });
}
}
console.log("[PM-RAW]", new Date().toISOString(), "| events gesamt:", allEvents.length);
console.log("[PM-LOAD]", new Date().toISOString(), "| moneyline-events:", parsed.length, "| events gesamt:", allEvents.length);
console.log("[polymarket] spiele:", parsed.length);
return parsed;
});
@@ -360,10 +376,23 @@ export function attachOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]):
oddsMatch.set(o, { homeId, awayId });
} else {
oddsMatch.set(o, null);
const feedMatchesWithTheseTeams = matches.filter(m =>
m.homeTeamId && m.awayTeamId &&
((m.homeTeamId === homeId && m.awayTeamId === awayId) ||
(m.homeTeamId === awayId && m.awayTeamId === homeId)),
);
console.log("[PM-NOMATCH]", o.homeName, "vs", o.awayName, "| slug:", o.slug);
console.log("[PM-NOMATCH-WHY]", o.homeName, "vs", o.awayName,
"| homeId gefunden:", !!homeId,
"| awayId gefunden:", !!awayId,
"| homeCode:", o.homeCode, "| awayCode:", o.awayCode,
"| feed-matches mit diesen teams:", feedMatchesWithTheseTeams.length);
}
}
return matches.map((m) => {
const attachedOdds = new Set<ParsedOdds>();
const result = matches.map((m) => {
if (!m.homeTeamId || !m.awayTeamId) return m;
// Suche in oddsMatch nach einer Kombination die beide Team-IDs matcht
for (const [o, ids] of oddsMatch) {
@@ -372,6 +401,11 @@ export function attachOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]):
if ((ids.homeId === m.homeTeamId && ids.awayId === m.awayTeamId) ||
(ids.homeId === m.awayTeamId && ids.awayId === m.homeTeamId)) {
const swapped = ids.homeId === m.awayTeamId;
attachedOdds.add(o);
console.log("[PM-ATTACH]", o.homeName, "vs", o.awayName,
"| match gefunden:", true,
"| match.status:", m.status,
"| prob gesetzt:", true);
return {
...m,
prob: {
@@ -384,6 +418,252 @@ export function attachOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]):
}
return m;
});
for (const [o, ids] of oddsMatch) {
if (ids && !attachedOdds.has(o)) {
const matchingFeedMatch = matches.find(m =>
m.homeTeamId && m.awayTeamId &&
((m.homeTeamId === ids.homeId && m.awayTeamId === ids.awayId) ||
(m.homeTeamId === ids.awayId && m.awayTeamId === ids.homeId)),
);
console.log("[PM-NOMATCH]", o.homeName, "vs", o.awayName, "| slug:", o.slug);
console.log("[PM-NOMATCH-WHY]", o.homeName, "vs", o.awayName,
"| team-ids gematcht:", ids.homeId, "vs", ids.awayId,
"| feed-match mit diesen teams existiert:", !!matchingFeedMatch,
"| feed-match status:", matchingFeedMatch?.status ?? "—",
"| prob bereits gesetzt:", matchingFeedMatch?.prob ? "ja" : "nein");
}
}
return result;
}
// ----------------------------------------------------------------------------
// KO-Odds-Fallback: löst R32-Paarungen auf und verknüpft Polymarket-Events
// mit K.o.-Feed-Matches, die noch keine Team-IDs im Feed haben.
// ----------------------------------------------------------------------------
// Löst für jeden R32-Slot (73-88) die aktuellen Team-Paarungen auf Basis der
// Gruppentabellen und Annex-C-Zuordnung. Gibt eine Map matchNumber → Paarung
// zurück, NUR wenn beide Team-IDs eindeutig bekannt sind (kein Raten).
function resolveR32Pairings(matches: Match[], teams: Team[]): Map<number, { homeTeamId: string; awayTeamId: string }> {
const tables = computeGroupTables(teams, matches);
const thirds = computeThirdPlaceTable(tables);
const qGroups = qualifiedThirdGroups(thirds);
const annex = qGroups.length === 8 ? resolveAnnexC(qGroups) : null;
const pairings = new Map<number, { homeTeamId: string; awayTeamId: string }>();
for (const slot of R32) {
const homeGroup = slot.home.type === "W" ? slot.home.group : undefined;
const awayGroup = slot.away.type === "W" ? slot.away.group : undefined;
const wg = (homeGroup ?? awayGroup) as GroupId | undefined;
let hid: string | null = null;
if (slot.home.type === "W" && slot.home.group) {
hid = tables.find(t => t.group === slot.home.group)?.rows.find(r => r.rank === 1)?.teamId ?? null;
} else if (slot.home.type === "R" && slot.home.group) {
hid = tables.find(t => t.group === slot.home.group)?.rows.find(r => r.rank === 2)?.teamId ?? null;
} else if (slot.home.type === "3" && annex && wg) {
const tg = annex[wg];
if (tg) hid = thirds.find(r => r.group === tg && r.qualifies)?.teamId ?? null;
}
let aid: string | null = null;
if (slot.away.type === "W" && slot.away.group) {
aid = tables.find(t => t.group === slot.away.group)?.rows.find(r => r.rank === 1)?.teamId ?? null;
} else if (slot.away.type === "R" && slot.away.group) {
aid = tables.find(t => t.group === slot.away.group)?.rows.find(r => r.rank === 2)?.teamId ?? null;
} else if (slot.away.type === "3" && annex && wg) {
const tg = annex[wg];
if (tg) aid = thirds.find(r => r.group === tg && r.qualifies)?.teamId ?? null;
}
if (hid && aid) {
pairings.set(slot.matchNumber, { homeTeamId: hid, awayTeamId: aid });
}
}
console.log("[R32-PAIRS] aufgelöste Paarungen:", pairings.size);
for (const [num, p] of pairings) {
const h = teams.find(t=>t.id===p.homeTeamId)?.name ?? p.homeTeamId;
const a = teams.find(t=>t.id===p.awayTeamId)?.name ?? p.awayTeamId;
console.log(" [R32-PAIR]", "matchNumber:", num, "|", h, "vs", a);
}
return pairings;
}
// Extrahiert das Datum (YYYY-MM-DD) aus einem Polymarket-Slug.
// Slug-Format: fifwc-{home}-{away}-{yyyy}-{mm}-{dd}
function slugToDate(slug: string): string | null {
const parts = slug.split("-");
if (parts.length < 3) return null;
const y = parts[parts.length - 3];
const m = parts[parts.length - 2];
const d = parts[parts.length - 1];
if (/^\d{4}$/.test(y) && /^\d{2}$/.test(m) && /^\d{2}$/.test(d)) {
return `${y}-${m}-${d}`;
}
return null;
}
// Zweiter Durchlauf für attachOdds: ordnet Polymarket-Events K.o.-Feed-Matches
// zu, die (noch) keine Team-IDs im Feed haben. Arbeitet direkt auf den
// aufgelösten R32-Paarungen (resolveR32Pairings). Identifiziert das Ziel-
// Feed-Match über die Slot-matchNumber (Stufe 1) oder Datums-Lookup aus dem
// Slug (Stufe 2) — KEINE chronologische Nummernvergabe.
// Mutiert matches in-place, setzt nur bei Matches OHNE bestehende prob.
export function attachKOOdds(matches: Match[], teams: Team[], odds: ParsedOdds[]): Match[] {
const koOhneProb = matches.filter(m => m.group == null && m.prob == null).length;
console.log("[KO-ODDS] start | odds-events:", odds?.length ?? "undefined",
"| ko-matches ohne prob:", koOhneProb);
const pairings = resolveR32Pairings(matches, teams);
if (pairings.size === 0) return matches;
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);
}
const idByCode = new Map(teams.map((t) => [t.code.toLowerCase(), t.id]));
console.log("[KO-LOOP] start | pairings:", pairings.size, "| odds-events:", odds?.length ?? "undef");
for (const [matchNum, pairing] of pairings) {
if (!pairing.homeTeamId || !pairing.awayTeamId) continue;
const hName = teams.find(t => t.id === pairing.homeTeamId)?.name ?? "?";
const aName = teams.find(t => t.id === pairing.awayTeamId)?.name ?? "?";
// Finde Polymarket-Event für dieses Team-Paar
let matchedEvent: ParsedOdds | null = 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) continue;
if ((homeId === pairing.homeTeamId && awayId === pairing.awayTeamId) ||
(homeId === pairing.awayTeamId && awayId === pairing.homeTeamId)) {
matchedEvent = o;
break;
}
}
if (!matchedEvent) {
console.log("[KO-TRY]", "matchNumber:", matchNum,
"| teams:", `${hName} vs ${aName}`,
"| polymarket-event gefunden:", false,
"| ziel-feedmatch gefunden:", false,
"| stufe:", "keine");
continue;
}
// Ziel-Feed-Match identifizieren
let targetMatch: Match | undefined;
let stufe: string | undefined;
// Stufe 1: bereits nummeriertes R32-Match (via assignKONumbersBySlots Pass 1)
targetMatch = matches.find(m =>
m.stage === "R32" && m.group == null && m.matchNumber === matchNum,
);
if (targetMatch) stufe = "1 (nummeriert)";
// Stufe 2: Datums-Lookup aus Polymarket-Slug für unnummerierte Matches
if (!targetMatch) {
const slugDate = slugToDate(matchedEvent.slug);
if (slugDate) {
const candidates = matches.filter(m =>
m.stage === "R32" && m.group == null && m.matchNumber === 0 &&
m.utcDate.slice(0, 10) === slugDate,
);
if (candidates.length === 1) {
targetMatch = candidates[0];
targetMatch.matchNumber = matchNum;
stufe = "2 (datum)";
} else if (candidates.length > 1) {
// Tie-breaker: falls Polymarket-Event eine Startzeit hat, nächstgelegenes Feed-Match
if (matchedEvent.startTime) {
const eventTime = new Date(matchedEvent.startTime).getTime();
let best: Match | undefined;
let bestDiff = Infinity;
for (const c of candidates) {
const diff = Math.abs(new Date(c.utcDate).getTime() - eventTime);
if (diff < bestDiff) { bestDiff = diff; best = c; }
}
// Nur zuordnen, wenn Zeitdifferenz plausibel (<4h)
if (best && bestDiff < 4 * 60 * 60 * 1000) {
targetMatch = best;
targetMatch.matchNumber = matchNum;
stufe = "2 (datum+tie)";
}
}
console.log("[KO-TIME]", "slot:", matchNum,
"| teams:", `${hName} vs ${aName}`,
"| pm-gameStartTime(UTC):", matchedEvent.startTime,
"| feed-kandidaten:", candidates.map(c =>
`${c.id}(${c.utcDate})`,
).join(", "),
"| gewählt:", targetMatch?.id ?? "keins");
if (!targetMatch) {
console.log("[KO-TIE]", "matchNumber:", matchNum,
"| slug:", matchedEvent.slug,
"| slugDate:", slugDate,
"| startTime:", matchedEvent.startTime,
"| candidates:", candidates.map(c =>
`${c.id}(${c.utcDate})`,
).join(", "));
}
}
}
}
if (!targetMatch) {
console.log("[KO-TRY]", "matchNumber:", matchNum,
"| teams:", `${hName} vs ${aName}`,
"| polymarket-event gefunden:", true,
"| ziel-feedmatch gefunden:", false,
"| stufe:", stufe ?? "keine");
continue;
}
// prob nicht überschreiben, falls schon gesetzt
if (targetMatch.prob) {
console.log("[KO-TRY]", "matchNumber:", matchNum,
"| teams:", `${hName} vs ${aName}`,
"| polymarket-event gefunden:", true,
"| ziel-feedmatch gefunden:", true,
"| stufe:", stufe,
"| prob bereits gesetzt:", true);
continue;
}
const homeIdFromEvent = idByName.get(normName(matchedEvent.homeName)) ??
idByCode.get(matchedEvent.homeCode);
const swapped = homeIdFromEvent === pairing.awayTeamId;
targetMatch.prob = {
home: swapped ? matchedEvent.pAway : matchedEvent.pHome,
draw: matchedEvent.pDraw,
away: swapped ? matchedEvent.pHome : matchedEvent.pAway,
};
console.log("[PM-ATTACH]", matchedEvent.homeName, "vs", matchedEvent.awayName,
"| match gefunden via R32:", true,
"| match.status:", targetMatch.status,
"| matchNumber:", targetMatch.matchNumber,
"| prob gesetzt:", true);
console.log("[KO-TRY]", "matchNumber:", matchNum,
"| teams:", `${hName} vs ${aName}`,
"| polymarket-event gefunden:", !!matchedEvent,
"| ziel-feedmatch gefunden:", !!targetMatch,
"| stufe:", stufe);
}
return matches;
}
// ----------------------------------------------------------------------------