first (part 3: src directory)
Deploy Iddaai Backend / build-and-deploy (push) Successful in 33s

This commit is contained in:
2026-04-16 15:12:27 +03:00
parent 2f0b85a0c7
commit 182f4aae16
125 changed files with 22552 additions and 0 deletions
+888
View File
@@ -0,0 +1,888 @@
/**
* 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);