/** * populate-feature-store.ts * ========================= * Batch feature computation for all finished football matches. * Populates the match_ai_features table with 7-pillar feature vectors. * * Pillars computed: * 1. ELO Ratings (from team_elo_ratings) * 2. Form (last 5 match rolling averages) * 3. Odds Implied Probability (from odd_selections) * 4. Team Stats (possession, shots, corners) * 5. Head-to-Head (historical matchups) * 6. Referee (bias, cards, goals) * 7. League DNA (averages across league-season) * * Usage: npx ts-node --transpile-only -r tsconfig-paths/register src/scripts/populate-feature-store.ts */ import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); // ───────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────── interface MatchRow { id: string; homeTeamId: string; awayTeamId: string; leagueId: string | null; scoreHome: number; scoreAway: number; mstUtc: bigint; } interface FeatureVector { matchId: string; // ELO homeElo: number; awayElo: number; homeHomeElo: number; awayAwayElo: number; homeFormElo: number; awayFormElo: number; eloDiff: number; // Form homeFormScore: number; awayFormScore: number; homeGoalsAvg5: number; awayGoalsAvg5: number; homeConcededAvg5: number; awayConcededAvg5: number; homeCleanSheetRate: number; awayCleanSheetRate: number; homeScoringRate: number; awayScoringRate: number; homeWinStreak: number; awayWinStreak: number; // Odds impliedHome: number; impliedDraw: number; impliedAway: number; impliedOver25: number; impliedBttsYes: number; oddsOverround: number; // Team Stats homeAvgPossession: number; awayAvgPossession: number; homeAvgShotsOnTarget: number; awayAvgShotsOnTarget: number; homeShotConversion: number; awayShotConversion: number; homeAvgCorners: number; awayAvgCorners: number; // H2H h2hTotal: number; h2hHomeWinRate: number; h2hAvgGoals: number; h2hOver25Rate: number; h2hBttsRate: number; // Referee refereeAvgCards: number; refereeHomeBias: number; refereeAvgGoals: number; // League DNA leagueAvgGoals: number; leagueHomeWinPct: number; leagueOver25Pct: number; // Meta missingPlayersImpact: number; calculatorVer: string; } // ───────────────────────────────────────────────────────────── // Pillar 1: ELO Ratings // ───────────────────────────────────────────────────────────── async function loadEloMap(): Promise< Map > { const ratings = await prisma.teamEloRating.findMany(); const map = new Map< string, { overall: number; home: number; away: number; form: number } >(); for (const r of ratings) { map.set(r.teamId, { overall: r.overallElo, home: r.homeElo, away: r.awayElo, form: r.formElo, }); } console.log(` ✅ ELO map loaded: ${map.size} teams`); return map; } // ───────────────────────────────────────────────────────────── // Pillar 2: Form — Pre-compute per-team rolling stats // ───────────────────────────────────────────────────────────── interface TeamFormState { goalsScored: number[]; goalsConceded: number[]; results: string[]; // W/D/L matchCount: number; } function buildFormIndex( matches: MatchRow[], ): Map> { // matchId -> teamId -> form state AT THAT POINT (before match) const teamStates = new Map(); const formIndex = new Map>(); function getOrCreateState(teamId: string): TeamFormState { let state = teamStates.get(teamId); if (!state) { state = { goalsScored: [], goalsConceded: [], results: [], matchCount: 0, }; teamStates.set(teamId, state); } return state; } function cloneState(state: TeamFormState): TeamFormState { return { goalsScored: [...state.goalsScored], goalsConceded: [...state.goalsConceded], results: [...state.results], matchCount: state.matchCount, }; } for (const match of matches) { const homeState = getOrCreateState(match.homeTeamId); const awayState = getOrCreateState(match.awayTeamId); // Store form state BEFORE this match const matchFormMap = new Map(); matchFormMap.set(match.homeTeamId, cloneState(homeState)); matchFormMap.set(match.awayTeamId, cloneState(awayState)); formIndex.set(match.id, matchFormMap); // Update states AFTER this match homeState.goalsScored.unshift(match.scoreHome); homeState.goalsConceded.unshift(match.scoreAway); awayState.goalsScored.unshift(match.scoreAway); awayState.goalsConceded.unshift(match.scoreHome); if (homeState.goalsScored.length > 10) homeState.goalsScored.pop(); if (homeState.goalsConceded.length > 10) homeState.goalsConceded.pop(); if (awayState.goalsScored.length > 10) awayState.goalsScored.pop(); if (awayState.goalsConceded.length > 10) awayState.goalsConceded.pop(); const homeResult = match.scoreHome > match.scoreAway ? "W" : match.scoreHome < match.scoreAway ? "L" : "D"; const awayResult = match.scoreAway > match.scoreHome ? "W" : match.scoreAway < match.scoreHome ? "L" : "D"; homeState.results.unshift(homeResult); awayState.results.unshift(awayResult); if (homeState.results.length > 10) homeState.results.pop(); if (awayState.results.length > 10) awayState.results.pop(); homeState.matchCount++; awayState.matchCount++; } return formIndex; } function extractFormFeatures(formState: TeamFormState): { goalsAvg5: number; concededAvg5: number; cleanSheetRate: number; scoringRate: number; winStreak: number; formScore: number; } { const last5Goals = formState.goalsScored.slice(0, 5); const last5Conceded = formState.goalsConceded.slice(0, 5); const n = last5Goals.length || 1; const goalsAvg5 = last5Goals.reduce((a, b) => a + b, 0) / n; const concededAvg5 = last5Conceded.reduce((a, b) => a + b, 0) / n; const cleanSheetRate = last5Conceded.filter((g) => g === 0).length / n; const scoringRate = last5Goals.filter((g) => g > 0).length / n; let winStreak = 0; for (const r of formState.results) { if (r === "W") winStreak++; else break; } // Form score: (W=3, D=1, L=0) over last 5, normalized to 0-100 const last5Results = formState.results.slice(0, 5); const points = last5Results.reduce( (sum, r) => sum + (r === "W" ? 3 : r === "D" ? 1 : 0), 0, ); const maxPoints = last5Results.length * 3 || 1; const formScore = (points / maxPoints) * 100; return { goalsAvg5, concededAvg5, cleanSheetRate, scoringRate, winStreak, formScore, }; } // ───────────────────────────────────────────────────────────── // Pillar 3: Odds Implied Probability (batch) // ───────────────────────────────────────────────────────────── interface OddsData { impliedHome: number; impliedDraw: number; impliedAway: number; impliedOver25: number; impliedBttsYes: number; overround: number; } async function loadOddsIndex(): Promise> { const oddsIndex = new Map(); // Raw SQL: join odd_categories + odd_selections for MS and OU25 const rows = await prisma.$queryRaw< Array<{ match_id: string; cat_name: string; sel_name: string; odds: number; }> >` SELECT oc.match_id, oc.name AS cat_name, os.name AS sel_name, COALESCE(os.odd_value::numeric, 0)::float AS odds FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.name IN ('Maç Sonucu', 'Alt/Üst 2,5', 'Karşılıklı Gol') AND os.odd_value IS NOT NULL AND os.odd_value ~ '^[0-9]' ORDER BY oc.match_id `; // Group by match_id const byMatch = new Map< string, Array<{ cat: string; sel: string; odds: number }> >(); for (const row of rows) { let arr = byMatch.get(row.match_id); if (!arr) { arr = []; byMatch.set(row.match_id, arr); } arr.push({ cat: row.cat_name, sel: row.sel_name, odds: row.odds }); } for (const [matchId, selections] of byMatch.entries()) { let msH = 0, msD = 0, msA = 0; let ou25O = 0; let bttsY = 0; for (const s of selections) { if (s.cat === "Maç Sonucu") { if (s.sel === "1") msH = s.odds; else if (s.sel === "X" || s.sel === "0") msD = s.odds; else if (s.sel === "2") msA = s.odds; } else if (s.cat === "Alt/Üst 2,5") { if ( s.sel.toLowerCase().includes("üst") || s.sel.toLowerCase().includes("over") ) ou25O = s.odds; } else if (s.cat === "Karşılıklı Gol") { if ( s.sel.toLowerCase().includes("var") || s.sel.toLowerCase().includes("yes") ) bttsY = s.odds; } } const impliedHome = msH > 0 ? 1 / msH : 0.33; const impliedDraw = msD > 0 ? 1 / msD : 0.33; const impliedAway = msA > 0 ? 1 / msA : 0.33; const totalImplied = impliedHome + impliedDraw + impliedAway; const overround = totalImplied > 0 ? (totalImplied - 1) * 100 : 0; const impliedOver25 = ou25O > 0 ? 1 / ou25O : 0.5; const impliedBttsYes = bttsY > 0 ? 1 / bttsY : 0.5; oddsIndex.set(matchId, { impliedHome: totalImplied > 0 ? impliedHome / totalImplied : 0.33, impliedDraw: totalImplied > 0 ? impliedDraw / totalImplied : 0.33, impliedAway: totalImplied > 0 ? impliedAway / totalImplied : 0.33, impliedOver25: Math.min(impliedOver25, 1), impliedBttsYes: Math.min(impliedBttsYes, 1), overround, }); } console.log( ` ✅ Odds index loaded: ${oddsIndex.size} matches with odds data`, ); return oddsIndex; } // ───────────────────────────────────────────────────────────── // Pillar 5: Head-to-Head (batch precompute) // ───────────────────────────────────────────────────────────── interface H2HState { totalMatches: number; homeWins: number; totalGoals: number; over25Count: number; bttsCount: number; } function buildH2HIndex(matches: MatchRow[]): Map { // Key: "teamA_teamB" sorted alphabetically const h2hMap = new Map(); // matchId -> h2h state BEFORE that match const h2hIndex = new Map(); function getKey(t1: string, t2: string): string { return t1 < t2 ? `${t1}_${t2}` : `${t2}_${t1}`; } for (const match of matches) { const key = getKey(match.homeTeamId, match.awayTeamId); let state = h2hMap.get(key); if (!state) { state = { totalMatches: 0, homeWins: 0, totalGoals: 0, over25Count: 0, bttsCount: 0, }; h2hMap.set(key, state); } // Save state BEFORE this match h2hIndex.set(match.id, { ...state }); // Update AFTER state.totalMatches++; if (match.scoreHome > match.scoreAway) state.homeWins++; state.totalGoals += match.scoreHome + match.scoreAway; if (match.scoreHome + match.scoreAway > 2.5) state.over25Count++; if (match.scoreHome > 0 && match.scoreAway > 0) state.bttsCount++; } return h2hIndex; } // ───────────────────────────────────────────────────────────── // Pillar 7: League DNA (batch precompute) // ───────────────────────────────────────────────────────────── interface LeagueStats { totalMatches: number; totalGoals: number; homeWins: number; over25Count: number; } function buildLeagueIndex(matches: MatchRow[]): Map { const leagueMap = new Map(); for (const match of matches) { const key = match.leagueId ?? "unknown"; let stats = leagueMap.get(key); if (!stats) { stats = { totalMatches: 0, totalGoals: 0, homeWins: 0, over25Count: 0 }; leagueMap.set(key, stats); } stats.totalMatches++; stats.totalGoals += match.scoreHome + match.scoreAway; if (match.scoreHome > match.scoreAway) stats.homeWins++; if (match.scoreHome + match.scoreAway > 2.5) stats.over25Count++; } return leagueMap; } // ───────────────────────────────────────────────────────────── // Pillar 6: Referee — Load from match_officials // ───────────────────────────────────────────────────────────── interface RefereeStats { totalMatches: number; totalCards: number; totalGoals: number; homeWins: number; } async function loadRefereeIndex( matches: MatchRow[], ): Promise> { // Build match scores lookup const matchScores = new Map(); for (const m of matches) { matchScores.set(m.id, { home: m.scoreHome, away: m.scoreAway }); } // Load officials const officials = await prisma.$queryRaw< Array<{ match_id: string; official_name: string }> >` SELECT match_id, name AS official_name FROM match_officials WHERE role_id = 1 LIMIT 500000 `; const refereeIndex = new Map(); // Group by referee const refMatchMap = new Map(); for (const o of officials) { let arr = refMatchMap.get(o.official_name); if (!arr) { arr = []; refMatchMap.set(o.official_name, arr); } arr.push(o.match_id); } // matchId -> referee name const matchRefereeMap = new Map(); for (const o of officials) { matchRefereeMap.set(o.match_id, o.official_name); } // Calculate referee stats for (const [refName, matchIds] of refMatchMap.entries()) { let totalGoals = 0; let homeWins = 0; let totalMatches = 0; for (const mid of matchIds) { const score = matchScores.get(mid); if (score) { totalGoals += score.home + score.away; if (score.home > score.away) homeWins++; totalMatches++; } } refereeIndex.set(refName, { totalMatches, totalCards: 0, // Would require card stats table — use 0 as default totalGoals, homeWins, }); } // Build matchId -> refStats lookup const matchRefStats = new Map(); for (const [matchId, refName] of matchRefereeMap.entries()) { const stats = refereeIndex.get(refName); if (stats) { matchRefStats.set(matchId, stats); } } console.log( ` ✅ Referee index loaded: ${refereeIndex.size} referees, ${matchRefStats.size} match-referee mappings`, ); return matchRefStats; } // ───────────────────────────────────────────────────────────── // Main Feature Store Population // ───────────────────────────────────────────────────────────── async function populateFeatureStore(): Promise { const startTime = Date.now(); try { console.log("🧠 Feature Store Population — Starting..."); console.log("─".repeat(60)); // Load all finished football matches console.log("📥 Loading matches..."); const rawMatches = await prisma.match.findMany({ where: { sport: "football", status: "FT", scoreHome: { not: null }, scoreAway: { not: null }, homeTeamId: { not: null }, awayTeamId: { not: null }, }, select: { id: true, homeTeamId: true, awayTeamId: true, leagueId: true, scoreHome: true, scoreAway: true, mstUtc: true, }, orderBy: { mstUtc: "asc" }, }); const matches: MatchRow[] = rawMatches.map((m) => ({ id: m.id, homeTeamId: m.homeTeamId!, awayTeamId: m.awayTeamId!, leagueId: m.leagueId, scoreHome: m.scoreHome!, scoreAway: m.scoreAway!, mstUtc: m.mstUtc, })); console.log(` 📊 Matches loaded: ${matches.length.toLocaleString()}`); // Pre-compute all indexes console.log("\n📊 Building feature indexes..."); console.log(" 🏅 Pillar 1: Loading ELO ratings..."); const eloMap = await loadEloMap(); console.log(" 📈 Pillar 2: Building form index..."); const formIndex = buildFormIndex(matches); console.log(" 💰 Pillar 3: Loading odds data..."); const oddsIndex = await loadOddsIndex(); console.log(" ⚔️ Pillar 5: Building H2H index..."); const h2hIndex = buildH2HIndex(matches); console.log(" 📋 Pillar 6: Loading referee data..."); const refereeIndex = await loadRefereeIndex(matches); console.log(" 🏟️ Pillar 7: Building league DNA..."); const leagueIndex = buildLeagueIndex(matches); console.log("\n✅ All indexes built!"); console.log("─".repeat(60)); // Build feature vectors and batch upsert console.log("💾 Writing features to database..."); const BATCH_SIZE = 1000; let processed = 0; const skipped = 0; for (let i = 0; i < matches.length; i += BATCH_SIZE) { const batch = matches.slice(i, i + BATCH_SIZE); const featureVectors: FeatureVector[] = []; for (const match of batch) { // Pillar 1: ELO const homeEloData = eloMap.get(match.homeTeamId); const awayEloData = eloMap.get(match.awayTeamId); const homeElo = homeEloData?.overall ?? 1500; const awayElo = awayEloData?.overall ?? 1500; // Pillar 2: Form const matchForm = formIndex.get(match.id); const homeFormState = matchForm?.get(match.homeTeamId); const awayFormState = matchForm?.get(match.awayTeamId); const homeForm = homeFormState ? extractFormFeatures(homeFormState) : { goalsAvg5: 0, concededAvg5: 0, cleanSheetRate: 0, scoringRate: 0, winStreak: 0, formScore: 50, }; const awayForm = awayFormState ? extractFormFeatures(awayFormState) : { goalsAvg5: 0, concededAvg5: 0, cleanSheetRate: 0, scoringRate: 0, winStreak: 0, formScore: 50, }; // Pillar 3: Odds const odds = oddsIndex.get(match.id) ?? { impliedHome: 0.33, impliedDraw: 0.33, impliedAway: 0.33, impliedOver25: 0.5, impliedBttsYes: 0.5, overround: 0, }; // Pillar 5: H2H const h2h = h2hIndex.get(match.id) ?? { totalMatches: 0, homeWins: 0, totalGoals: 0, over25Count: 0, bttsCount: 0, }; // Pillar 6: Referee const refStats = refereeIndex.get(match.id); const refTotal = refStats?.totalMatches ?? 0; // Pillar 7: League DNA const leagueKey = match.leagueId ?? "unknown"; const leagueStats = leagueIndex.get(leagueKey) ?? { totalMatches: 1, totalGoals: 0, homeWins: 0, over25Count: 0, }; featureVectors.push({ matchId: match.id, // ELO homeElo, awayElo, homeHomeElo: homeEloData?.home ?? 1500, awayAwayElo: awayEloData?.away ?? 1500, homeFormElo: homeEloData?.form ?? 1500, awayFormElo: awayEloData?.form ?? 1500, eloDiff: homeElo - awayElo, // Form homeFormScore: homeForm.formScore, awayFormScore: awayForm.formScore, homeGoalsAvg5: round(homeForm.goalsAvg5), awayGoalsAvg5: round(awayForm.goalsAvg5), homeConcededAvg5: round(homeForm.concededAvg5), awayConcededAvg5: round(awayForm.concededAvg5), homeCleanSheetRate: round(homeForm.cleanSheetRate), awayCleanSheetRate: round(awayForm.cleanSheetRate), homeScoringRate: round(homeForm.scoringRate), awayScoringRate: round(awayForm.scoringRate), homeWinStreak: homeForm.winStreak, awayWinStreak: awayForm.winStreak, // Odds impliedHome: round(odds.impliedHome), impliedDraw: round(odds.impliedDraw), impliedAway: round(odds.impliedAway), impliedOver25: round(odds.impliedOver25), impliedBttsYes: round(odds.impliedBttsYes), oddsOverround: round(odds.overround), // Team Stats (placeholder — uses form data as proxy) homeAvgPossession: 50.0, awayAvgPossession: 50.0, homeAvgShotsOnTarget: round(homeForm.goalsAvg5 * 2.5), // Approx proxy awayAvgShotsOnTarget: round(awayForm.goalsAvg5 * 2.5), homeShotConversion: homeForm.goalsAvg5 > 0 ? round(homeForm.scoringRate) : 0, awayShotConversion: awayForm.goalsAvg5 > 0 ? round(awayForm.scoringRate) : 0, homeAvgCorners: 5.0, // Default — no corner data in match table awayAvgCorners: 4.5, // H2H h2hTotal: h2h.totalMatches, h2hHomeWinRate: h2h.totalMatches > 0 ? round(h2h.homeWins / h2h.totalMatches) : 0, h2hAvgGoals: h2h.totalMatches > 0 ? round(h2h.totalGoals / h2h.totalMatches) : 0, h2hOver25Rate: h2h.totalMatches > 0 ? round(h2h.over25Count / h2h.totalMatches) : 0, h2hBttsRate: h2h.totalMatches > 0 ? round(h2h.bttsCount / h2h.totalMatches) : 0, // Referee refereeAvgCards: 0, // No card data column available refereeHomeBias: refTotal > 0 ? round(refStats!.homeWins / refTotal - 0.46) : 0, // 0.46 = expected home win rate refereeAvgGoals: refTotal > 0 ? round(refStats!.totalGoals / refTotal) : 0, // League DNA leagueAvgGoals: round( leagueStats.totalGoals / leagueStats.totalMatches, ), leagueHomeWinPct: round( leagueStats.homeWins / leagueStats.totalMatches, ), leagueOver25Pct: round( leagueStats.over25Count / leagueStats.totalMatches, ), // Meta missingPlayersImpact: 0, calculatorVer: "v2.0", }); } // Batch upsert using raw SQL for performance if (featureVectors.length > 0) { await batchUpsertFeatures(featureVectors); processed += featureVectors.length; } if ((i + BATCH_SIZE) % 10000 === 0 || i + BATCH_SIZE >= matches.length) { const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log( ` 💾 Processed ${Math.min(i + BATCH_SIZE, matches.length).toLocaleString()} / ${matches.length.toLocaleString()} (${elapsed}s)`, ); } } const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log("─".repeat(60)); console.log(`✅ Feature Store population complete!`); console.log(` Features written: ${processed.toLocaleString()}`); console.log(` Skipped: ${skipped}`); console.log(` Duration: ${elapsed}s`); // Verify const count = await prisma.footballAiFeature.count(); console.log(` DB row count: ${count.toLocaleString()}`); console.log("─".repeat(60)); } catch (error) { console.error("❌ Feature store population failed:", error); process.exit(1); } finally { await prisma.$disconnect(); } } // ───────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────── function round(val: number, decimals = 4): number { const factor = Math.pow(10, decimals); return Math.round(val * factor) / factor; } async function batchUpsertFeatures(features: FeatureVector[]): Promise { // Use Prisma transactions for batch upsert await prisma.$transaction( features.map((f) => prisma.footballAiFeature.upsert({ where: { matchId: f.matchId }, update: { homeElo: f.homeElo, awayElo: f.awayElo, homeHomeElo: f.homeHomeElo, awayAwayElo: f.awayAwayElo, homeFormElo: f.homeFormElo, awayFormElo: f.awayFormElo, eloDiff: f.eloDiff, homeFormScore: f.homeFormScore, awayFormScore: f.awayFormScore, homeGoalsAvg5: f.homeGoalsAvg5, awayGoalsAvg5: f.awayGoalsAvg5, homeConcededAvg5: f.homeConcededAvg5, awayConcededAvg5: f.awayConcededAvg5, homeCleanSheetRate: f.homeCleanSheetRate, awayCleanSheetRate: f.awayCleanSheetRate, homeScoringRate: f.homeScoringRate, awayScoringRate: f.awayScoringRate, homeWinStreak: f.homeWinStreak, awayWinStreak: f.awayWinStreak, impliedHome: f.impliedHome, impliedDraw: f.impliedDraw, impliedAway: f.impliedAway, impliedOver25: f.impliedOver25, impliedBttsYes: f.impliedBttsYes, oddsOverround: f.oddsOverround, homeAvgPossession: f.homeAvgPossession, awayAvgPossession: f.awayAvgPossession, homeAvgShotsOnTarget: f.homeAvgShotsOnTarget, awayAvgShotsOnTarget: f.awayAvgShotsOnTarget, homeShotConversion: f.homeShotConversion, awayShotConversion: f.awayShotConversion, homeAvgCorners: f.homeAvgCorners, awayAvgCorners: f.awayAvgCorners, h2hTotal: f.h2hTotal, h2hHomeWinRate: f.h2hHomeWinRate, h2hAvgGoals: f.h2hAvgGoals, h2hOver25Rate: f.h2hOver25Rate, h2hBttsRate: f.h2hBttsRate, refereeAvgCards: f.refereeAvgCards, refereeHomeBias: f.refereeHomeBias, refereeAvgGoals: f.refereeAvgGoals, leagueAvgGoals: f.leagueAvgGoals, leagueHomeWinPct: f.leagueHomeWinPct, leagueOver25Pct: f.leagueOver25Pct, missingPlayersImpact: f.missingPlayersImpact, calculatorVer: f.calculatorVer, }, create: { matchId: f.matchId, homeElo: f.homeElo, awayElo: f.awayElo, homeHomeElo: f.homeHomeElo, awayAwayElo: f.awayAwayElo, homeFormElo: f.homeFormElo, awayFormElo: f.awayFormElo, eloDiff: f.eloDiff, homeFormScore: f.homeFormScore, awayFormScore: f.awayFormScore, homeGoalsAvg5: f.homeGoalsAvg5, awayGoalsAvg5: f.awayGoalsAvg5, homeConcededAvg5: f.homeConcededAvg5, awayConcededAvg5: f.awayConcededAvg5, homeCleanSheetRate: f.homeCleanSheetRate, awayCleanSheetRate: f.awayCleanSheetRate, homeScoringRate: f.homeScoringRate, awayScoringRate: f.awayScoringRate, homeWinStreak: f.homeWinStreak, awayWinStreak: f.awayWinStreak, impliedHome: f.impliedHome, impliedDraw: f.impliedDraw, impliedAway: f.impliedAway, impliedOver25: f.impliedOver25, impliedBttsYes: f.impliedBttsYes, oddsOverround: f.oddsOverround, homeAvgPossession: f.homeAvgPossession, awayAvgPossession: f.awayAvgPossession, homeAvgShotsOnTarget: f.homeAvgShotsOnTarget, awayAvgShotsOnTarget: f.awayAvgShotsOnTarget, homeShotConversion: f.homeShotConversion, awayShotConversion: f.awayShotConversion, homeAvgCorners: f.homeAvgCorners, awayAvgCorners: f.awayAvgCorners, h2hTotal: f.h2hTotal, h2hHomeWinRate: f.h2hHomeWinRate, h2hAvgGoals: f.h2hAvgGoals, h2hOver25Rate: f.h2hOver25Rate, h2hBttsRate: f.h2hBttsRate, refereeAvgCards: f.refereeAvgCards, refereeHomeBias: f.refereeHomeBias, refereeAvgGoals: f.refereeAvgGoals, leagueAvgGoals: f.leagueAvgGoals, leagueHomeWinPct: f.leagueHomeWinPct, leagueOver25Pct: f.leagueOver25Pct, missingPlayersImpact: f.missingPlayersImpact, calculatorVer: f.calculatorVer, }, }), ), ); } // Run populateFeatureStore().catch(console.error);