Files
iddaai-be/src/scripts/populate-feature-store.ts
T
2026-04-16 17:21:48 +03:00

889 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<string, { overall: number; home: number; away: number; form: number }>
> {
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<string, Map<string, TeamFormState>> {
// matchId -> teamId -> form state AT THAT POINT (before match)
const teamStates = new Map<string, TeamFormState>();
const formIndex = new Map<string, Map<string, TeamFormState>>();
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<string, TeamFormState>();
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<Map<string, OddsData>> {
const oddsIndex = new Map<string, OddsData>();
// 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<string, H2HState> {
// Key: "teamA_teamB" sorted alphabetically
const h2hMap = new Map<string, H2HState>();
// matchId -> h2h state BEFORE that match
const h2hIndex = new Map<string, H2HState>();
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<string, LeagueStats> {
const leagueMap = new Map<string, LeagueStats>();
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<Map<string, RefereeStats>> {
// Build match scores lookup
const matchScores = new Map<string, { home: number; away: number }>();
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<string, RefereeStats>();
// Group by referee
const refMatchMap = new Map<string, string[]>();
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<string, string>();
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<string, RefereeStats>();
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<void> {
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<void> {
// 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);