Files
iddaai-be/analyze-reversal-matches.ts
fahricansecer 7814e0bc6b
Deploy Iddaai Backend / build-and-deploy (push) Failing after 4s
first (part 1: root files)
2026-04-16 15:09:10 +03:00

366 lines
14 KiB
TypeScript
Raw Permalink 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.
import { PrismaClient } from '@prisma/client';
import * as dotenv from 'dotenv';
dotenv.config();
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};
const prisma = new PrismaClient();
async function analyzeReversalMatches() {
console.log('🔍 ANALYZING HT/FT REVERSAL MATCHES (1/2 & 2/1)');
console.log('='.repeat(80));
// Fetch all completed matches with HT and FT scores
const matches = await prisma.match.findMany({
where: {
status: 'FT',
htScoreHome: { not: null },
htScoreAway: { not: null },
scoreHome: { not: null },
scoreAway: { not: null },
oddCategories: { some: {} }
},
include: {
homeTeam: true,
awayTeam: true,
league: true,
oddCategories: { include: { selections: true } }
},
orderBy: { mstUtc: 'desc' }
});
console.log(`📊 Total completed matches with odds: ${matches.length}`);
// Analyze HT/FT results
const reversalMatches: any[] = [];
let totalMatches = 0;
let htftCounts: Record<string, number> = {
'1/1': 0, '1/X': 0, '1/2': 0,
'X/1': 0, 'X/X': 0, 'X/2': 0,
'2/1': 0, '2/X': 0, '2/2': 0
};
for (const match of matches) {
const htHome = match.htScoreHome!;
const htAway = match.htScoreAway!;
const ftHome = match.scoreHome!;
const ftAway = match.scoreAway!;
const htResult = htHome > htAway ? '1' : htHome === htAway ? 'X' : '2';
const ftResult = ftHome > ftAway ? '1' : ftHome === ftAway ? 'X' : '2';
const htft = `${htResult}/${ftResult}`;
htftCounts[htft] = (htftCounts[htft] || 0) + 1;
totalMatches++;
if (htft === '1/2' || htft === '2/1') {
// Extract odds
let msHomeOdds: number | null = null;
let msDrawOdds: number | null = null;
let msAwayOdds: number | null = null;
let htHomeOdds: number | null = null;
let htDrawOdds: number | null = null;
let htAwayOdds: number | null = null;
for (const cat of match.oddCategories) {
const catName = (cat.name || '').toLowerCase();
const isHT = catName.includes('1.yarı');
for (const sel of cat.selections) {
const selName = (sel.name || '').toLowerCase();
if (!sel.oddValue) continue;
const odd = parseFloat(sel.oddValue.toString());
if (catName.includes('maç sonucu') || catName.includes('1.yarı sonucu')) {
if (selName === '1') { if (isHT) htHomeOdds = odd; else msHomeOdds = odd; }
else if (selName === 'x' || selName === '0') { if (isHT) htDrawOdds = odd; else msDrawOdds = odd; }
else if (selName === '2') { if (isHT) htAwayOdds = odd; else msAwayOdds = odd; }
}
}
}
if (!match.homeTeam || !match.awayTeam || !match.league) continue;
reversalMatches.push({
id: match.id,
homeTeam: match.homeTeam.name,
awayTeam: match.awayTeam.name,
league: match.league.name,
htHome, htAway, ftHome, ftAway,
htft,
msHomeOdds, msDrawOdds, msAwayOdds,
htHomeOdds, htDrawOdds, htAwayOdds,
date: match.mstUtc,
});
}
}
// Print HT/FT distribution
console.log('\n📊 HT/FT DISTRIBUTION:');
for (const [key, count] of Object.entries(htftCounts)) {
const pct = (count / totalMatches * 100).toFixed(2);
const marker = (key === '1/2' || key === '2/1') ? ' ⚠️ REVERSAL' : '';
console.log(` ${key}: ${count} (${pct}%)${marker}`);
}
console.log(`\n⚠️ TOTAL REVERSAL MATCHES: ${reversalMatches.length} (${(reversalMatches.length / totalMatches * 100).toFixed(2)}%)`);
// ANALYSIS 1: League distribution
console.log('\n📈 ANALYSIS 1: LEAGUE DISTRIBUTION OF REVERSALS');
console.log('-'.repeat(80));
const leagueCounts: Record<string, { total: number, reversal: number }> = {};
for (const match of matches) {
if (!match.league) continue;
const htHome = match.htScoreHome!;
const htAway = match.htScoreAway!;
const ftHome = match.scoreHome!;
const ftAway = match.scoreAway!;
const htResult = htHome > htAway ? '1' : htHome === htAway ? 'X' : '2';
const ftResult = ftHome > ftAway ? '1' : ftHome === ftAway ? 'X' : '2';
const htft = `${htResult}/${ftResult}`;
const league = match.league.name;
if (!leagueCounts[league]) leagueCounts[league] = { total: 0, reversal: 0 };
leagueCounts[league].total++;
if (htft === '1/2' || htft === '2/1') leagueCounts[league].reversal++;
}
const leagueSorted = Object.entries(leagueCounts)
.filter(([_, v]) => v.reversal > 0 && v.total >= 50)
.sort((a, b) => (b[1].reversal / b[1].total) - (a[1].reversal / a[1].total))
.slice(0, 20);
console.log('\nTop 20 leagues by reversal rate (min 50 matches):');
for (const [league, data] of leagueSorted) {
const rate = (data.reversal / data.total * 100).toFixed(2);
console.log(` ${league}: ${data.reversal}/${data.total} (${rate}%)`);
}
// ANALYSIS 2: Odds patterns
console.log('\n📈 ANALYSIS 2: ODDS PATTERNS IN REVERSAL MATCHES');
console.log('-'.repeat(80));
const ms1_2 = reversalMatches.filter(m => m.htft === '1/2');
const ms2_1 = reversalMatches.filter(m => m.htft === '2/1');
console.log(`\n1/2 Reversals: ${ms1_2.length}`);
console.log(`2/1 Reversals: ${ms2_1.length}`);
// MS odds analysis for 1/2
const ms1_2_withOdds = ms1_2.filter(m => m.msHomeOdds && m.msAwayOdds);
if (ms1_2_withOdds.length > 0) {
const avgHomeOdd = ms1_2_withOdds.reduce((sum, m) => sum + m.msHomeOdds!, 0) / ms1_2_withOdds.length;
const avgAwayOdd = ms1_2_withOdds.reduce((sum, m) => sum + m.msAwayOdds!, 0) / ms1_2_withOdds.length;
const avgDrawOdd = ms1_2_withOdds.filter(m => m.msDrawOdds).reduce((sum, m) => sum + m.msDrawOdds!, 0) / ms1_2_withOdds.filter(m => m.msDrawOdds).length || 0;
console.log(`\n 1/2 Matches - Average MS Odds:`);
console.log(` Home Win: ${avgHomeOdd.toFixed(2)} (HT was WINNING!)`);
console.log(` Draw: ${avgDrawOdd.toFixed(2)}`);
console.log(` Away Win: ${avgAwayOdd.toFixed(2)} (but AWAY won FT!)`);
// Favorite analysis
let favoriteWon = 0;
let underdogWon = 0;
let noFavorite = 0;
for (const m of ms1_2_withOdds) {
if (m.msHomeOdds! < m.msAwayOdds!) {
// Home was favorite, but away won = UNDERDOG
underdogWon++;
} else if (m.msAwayOdds! < m.msHomeOdds!) {
// Away was favorite and won = FAVORITE
favoriteWon++;
} else {
noFavorite++;
}
}
console.log(`\n 1/2 - Who was favored vs who won:`);
console.log(` Favorite won (Away was fav): ${favoriteWon} (${(favoriteWon / ms1_2_withOdds.length * 100).toFixed(1)}%)`);
console.log(` Underdog won (Home was fav): ${underdogWon} (${(underdogWon / ms1_2_withOdds.length * 100).toFixed(1)}%) ⚠️`);
}
// MS odds analysis for 2/1
const ms2_1_withOdds = ms2_1.filter(m => m.msHomeOdds && m.msAwayOdds);
if (ms2_1_withOdds.length > 0) {
const avgHomeOdd = ms2_1_withOdds.reduce((sum, m) => sum + m.msHomeOdds!, 0) / ms2_1_withOdds.length;
const avgAwayOdd = ms2_1_withOdds.reduce((sum, m) => sum + m.msAwayOdds!, 0) / ms2_1_withOdds.length;
const avgDrawOdd = ms2_1_withOdds.filter(m => m.msDrawOdds).reduce((sum, m) => sum + m.msDrawOdds!, 0) / ms2_1_withOdds.filter(m => m.msDrawOdds).length || 0;
console.log(`\n 2/1 Matches - Average MS Odds:`);
console.log(` Home Win: ${avgHomeOdd.toFixed(2)} (HOME won FT!)`);
console.log(` Draw: ${avgDrawOdd.toFixed(2)}`);
console.log(` Away Win: ${avgAwayOdd.toFixed(2)} (Away was WINNING at HT!)`);
let favoriteWon = 0;
let underdogWon = 0;
for (const m of ms2_1_withOdds) {
if (m.msAwayOdds! < m.msHomeOdds!) {
// Away was favorite at HT, but home won = UNDERDOG
underdogWon++;
} else if (m.msHomeOdds! < m.msAwayOdds!) {
// Home was favorite and won = FAVORITE
favoriteWon++;
}
}
console.log(`\n 2/1 - Who was favored vs who won:`);
console.log(` Favorite won (Home was fav): ${favoriteWon} (${(favoriteWon / ms2_1_withOdds.length * 100).toFixed(1)}%)`);
console.log(` Underdog won (Away was fav): ${underdogWon} (${(underdogWon / ms2_1_withOdds.length * 100).toFixed(1)}%) ⚠️`);
}
// ANALYSIS 3: Suspicious patterns
console.log('\n📈 ANALYSIS 3: SUSPICIOUS PATTERNS');
console.log('-'.repeat(80));
// Pattern 1: Heavy favorite loses after leading (1/2 with low home odds)
const suspicious_1_2 = ms1_2_withOdds.filter(m => m.msHomeOdds! < 1.5);
console.log(`\n⚠️ PATTERN 1: Heavy Home Favorite loses after HT lead (MS Home Odds < 1.5):`);
console.log(` Count: ${suspicious_1_2.length}`);
if (suspicious_1_2.length > 0) {
const avgOdd = suspicious_1_2.reduce((sum, m) => sum + m.msHomeOdds!, 0) / suspicious_1_2.length;
console.log(` Avg Home Odds: ${avgOdd.toFixed(2)}`);
console.log(` Sample matches:`);
suspicious_1_2.slice(0, 5).forEach(m => {
console.log(` ${m.league}: ${m.homeTeam} (${m.msHomeOdds}) vs ${m.awayTeam} (${m.msAwayOdds}) => HT: ${m.htHome}-${m.htAway}, FT: ${m.ftHome}-${m.ftAway}`);
});
}
// Pattern 2: Heavy away favorite loses after leading (2/1 with low away odds)
const suspicious_2_1 = ms2_1_withOdds.filter(m => m.msAwayOdds! < 1.5);
console.log(`\n⚠️ PATTERN 2: Heavy Away Favorite loses after HT lead (MS Away Odds < 1.5):`);
console.log(` Count: ${suspicious_2_1.length}`);
if (suspicious_2_1.length > 0) {
const avgOdd = suspicious_2_1.reduce((sum, m) => sum + m.msAwayOdds!, 0) / suspicious_2_1.length;
console.log(` Avg Away Odds: ${avgOdd.toFixed(2)}`);
console.log(` Sample matches:`);
suspicious_2_1.slice(0, 5).forEach(m => {
console.log(` ${m.league}: ${m.homeTeam} (${m.msHomeOdds}) vs ${m.awayTeam} (${m.msAwayOdds}) => HT: ${m.htHome}-${m.htAway}, FT: ${m.ftHome}-${m.ftAway}`);
});
}
// ANALYSIS 4: HT Odds vs MS Odds correlation
console.log('\n📈 ANALYSIS 4: HT ODDS CORRELATION');
console.log('-'.repeat(80));
const withHTOdds = reversalMatches.filter(m => m.htHomeOdds && m.htAwayOdds);
if (withHTOdds.length > 0) {
console.log(`\n Matches with HT odds: ${withHTOdds.length}`);
let htCorrectlyPredicted = 0;
for (const m of withHTOdds) {
const htFav = m.htHomeOdds! < m.htAwayOdds! ? '1' : m.htAwayOdds! < m.htHomeOdds! ? '2' : 'X';
const htActual = m.htHome > m.htAway ? '1' : m.htAway > m.htHome ? '2' : 'X';
if (htFav === htActual) htCorrectlyPredicted++;
}
console.log(` HT Favorite correctly led at HT: ${htCorrectlyPredicted}/${withHTOdds.length} (${(htCorrectlyPredicted / withHTOdds.length * 100).toFixed(1)}%)`);
// How often did HT favorite lose FT?
let htFavoriteLostFT = 0;
for (const m of withHTOdds) {
const htFav = m.htHomeOdds! < m.htAwayOdds! ? '1' : m.htAwayOdds! < m.htHomeOdds! ? '2' : 'X';
const ftActual = m.ftHome > m.ftAway ? '1' : m.ftAway > m.ftHome ? '2' : 'X';
if (htFav !== ftActual) htFavoriteLostFT++;
}
console.log(` HT Favorite lost FT: ${htFavoriteLostFT}/${withHTOdds.length} (${(htFavoriteLostFT / withHTOdds.length * 100).toFixed(1)}%) ⚠️`);
}
// ANALYSIS 5: Score patterns
console.log('\n📈 ANALYSIS 5: SCORE PATTERNS IN REVERSALS');
console.log('-'.repeat(80));
// HT score distribution for reversals
const htScores: Record<string, number> = {};
for (const m of reversalMatches) {
const key = `${m.htHome}-${m.htAway}`;
htScores[key] = (htScores[key] || 0) + 1;
}
console.log('\nMost common HT scores in reversal matches:');
Object.entries(htScores)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.forEach(([score, count]) => {
console.log(` HT ${score}: ${count} matches`);
});
// FT score distribution
const ftScores: Record<string, number> = {};
for (const m of reversalMatches) {
const key = `${m.ftHome}-${m.ftAway}`;
ftScores[key] = (ftScores[key] || 0) + 1;
}
console.log('\nMost common FT scores in reversal matches:');
Object.entries(ftScores)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.forEach(([score, count]) => {
console.log(` FT ${score}: ${count} matches`);
});
// ANALYSIS 6: Goal difference patterns
console.log('\n📈 ANALYSIS 6: COMEBACK MAGNITUDE');
console.log('-'.repeat(80));
let comebackBy1 = 0;
let comebackBy2 = 0;
let comebackBy3Plus = 0;
for (const m of reversalMatches) {
const htDiff = Math.abs(m.htHome - m.htAway);
const ftDiff = Math.abs(m.ftHome - m.ftAway);
if (m.htft === '1/2') {
// Home was leading, away won
const margin = (m.ftAway - m.ftHome);
if (margin === 1) comebackBy1++;
else if (margin === 2) comebackBy2++;
else comebackBy3Plus++;
} else {
// Away was leading, home won
const margin = (m.ftHome - m.ftAway);
if (margin === 1) comebackBy1++;
else if (margin === 2) comebackBy2++;
else comebackBy3Plus++;
}
}
console.log(`\n Comeback by 1 goal: ${comebackBy1} (${(comebackBy1 / reversalMatches.length * 100).toFixed(1)}%)`);
console.log(` Comeback by 2 goals: ${comebackBy2} (${(comebackBy2 / reversalMatches.length * 100).toFixed(1)}%)`);
console.log(` Comeback by 3+ goals: ${comebackBy3Plus} (${(comebackBy3Plus / reversalMatches.length * 100).toFixed(1)}%) ⚠️`);
// Show extreme comebacks
const extremeComebacks = reversalMatches
.filter(m => {
if (m.htft === '1/2') return (m.ftAway - m.ftHome) >= 2;
return (m.ftHome - m.ftAway) >= 2;
})
.sort((a, b) => {
const diffA = a.htft === '1/2' ? (a.ftAway - a.ftHome) : (a.ftHome - a.ftAway);
const diffB = b.htft === '1/2' ? (b.ftAway - b.ftHome) : (b.ftHome - b.ftAway);
return diffB - diffA;
})
.slice(0, 10);
console.log('\nTop 10 most extreme comebacks:');
extremeComebacks.forEach(m => {
const diff = m.htft === '1/2' ? (m.ftAway - m.ftHome) : (m.ftHome - m.ftAway);
console.log(` ${m.league}: ${m.homeTeam} vs ${m.awayTeam} | HT: ${m.htHome}-${m.htAway} => FT: ${m.ftHome}-${m.ftAway} (Diff: ${diff})`);
});
console.log('\n' + '='.repeat(80));
console.log('✅ ANALYSIS COMPLETE');
console.log('='.repeat(80));
await prisma.$disconnect();
}
analyzeReversalMatches().catch(console.error);