366 lines
14 KiB
TypeScript
366 lines
14 KiB
TypeScript
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);
|