213 lines
7.0 KiB
TypeScript
213 lines
7.0 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 main() {
|
|
console.log('🔍 ANALYZING HT/FT REVERSAL MATCHES (1/2 & 2/1)');
|
|
console.log('='.repeat(80));
|
|
|
|
// Use raw SQL for performance
|
|
const matches: any[] = await prisma.$queryRaw`
|
|
SELECT
|
|
m.id, m.ht_score_home, m.ht_score_away, m.score_home, m.score_away, m.mst_utc,
|
|
ht.name as home_team, at.name as away_team, l.name as league
|
|
FROM matches m
|
|
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
|
LEFT JOIN teams at ON at.id = m.away_team_id
|
|
LEFT JOIN leagues l ON l.id = m.league_id
|
|
WHERE m.status = 'FT'
|
|
AND m.ht_score_home IS NOT NULL
|
|
AND m.ht_score_away IS NOT NULL
|
|
AND m.score_home IS NOT NULL
|
|
AND m.score_away IS NOT NULL
|
|
ORDER BY m.mst_utc DESC
|
|
`;
|
|
|
|
console.log(`📊 Total completed matches: ${matches.length}`);
|
|
|
|
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
|
|
};
|
|
|
|
const reversals: any[] = [];
|
|
|
|
for (const m of matches) {
|
|
const htH = m.ht_score_home;
|
|
const htA = m.ht_score_away;
|
|
const ftH = m.score_home;
|
|
const ftA = m.score_away;
|
|
|
|
const htR = htH > htA ? '1' : htH === htA ? 'X' : '2';
|
|
const ftR = ftH > ftA ? '1' : ftH === ftA ? 'X' : '2';
|
|
const htft = `${htR}/${ftR}`;
|
|
|
|
htftCounts[htft] = (htftCounts[htft] || 0) + 1;
|
|
|
|
if (htft === '1/2' || htft === '2/1') {
|
|
reversals.push({ ...m, htft, htH, htA, ftH, ftA });
|
|
}
|
|
}
|
|
|
|
const total = matches.length;
|
|
console.log('\n📊 HT/FT DISTRIBUTION:');
|
|
for (const [key, count] of Object.entries(htftCounts)) {
|
|
const pct = (count / total * 100).toFixed(2);
|
|
const marker = (key === '1/2' || key === '2/1') ? ' ⚠️ REVERSAL' : '';
|
|
console.log(` ${key}: ${count} (${pct}%)${marker}`);
|
|
}
|
|
|
|
console.log(`\n⚠️ TOTAL REVERSALS: ${reversals.length} (${(reversals.length / total * 100).toFixed(2)}%)`);
|
|
|
|
// ANALYSIS 1: By League
|
|
console.log('\n📈 LEAGUE DISTRIBUTION (min 100 matches):');
|
|
const leagueMap: Record<string, { total: number, rev: number }> = {};
|
|
for (const m of matches) {
|
|
const league = m.league || 'Unknown';
|
|
if (!leagueMap[league]) leagueMap[league] = { total: 0, rev: 0 };
|
|
leagueMap[league].total++;
|
|
|
|
const htH = m.ht_score_home;
|
|
const htA = m.ht_score_away;
|
|
const ftH = m.score_home;
|
|
const ftA = m.score_away;
|
|
const htR = htH > htA ? '1' : htH === htA ? 'X' : '2';
|
|
const ftR = ftH > ftA ? '1' : ftH === ftA ? 'X' : '2';
|
|
if ((htR === '1' && ftR === '2') || (htR === '2' && ftR === '1')) {
|
|
leagueMap[league].rev++;
|
|
}
|
|
}
|
|
|
|
const topLeagues = Object.entries(leagueMap)
|
|
.filter(([_, v]) => v.total >= 100 && v.rev > 0)
|
|
.sort((a, b) => (b[1].rev / b[1].total) - (a[1].rev / a[1].total))
|
|
.slice(0, 15);
|
|
|
|
console.log('\nTop 15 leagues by reversal rate:');
|
|
for (const [league, data] of topLeagues) {
|
|
const rate = (data.rev / data.total * 100).toFixed(2);
|
|
console.log(` ${league}: ${data.rev}/${data.total} (${rate}%)`);
|
|
}
|
|
|
|
// ANALYSIS 2: Score patterns
|
|
console.log('\n📈 HT SCORE PATTERNS IN REVERSALS:');
|
|
const htScoreMap: Record<string, number> = {};
|
|
for (const m of reversals) {
|
|
const key = `${m.htH}-${m.htA}`;
|
|
htScoreMap[key] = (htScoreMap[key] || 0) + 1;
|
|
}
|
|
|
|
Object.entries(htScoreMap)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 10)
|
|
.forEach(([score, count]) => {
|
|
console.log(` HT ${score}: ${count} matches`);
|
|
});
|
|
|
|
console.log('\n📈 FT SCORE PATTERNS IN REVERSALS:');
|
|
const ftScoreMap: Record<string, number> = {};
|
|
for (const m of reversals) {
|
|
const key = `${m.ftH}-${m.ftA}`;
|
|
ftScoreMap[key] = (ftScoreMap[key] || 0) + 1;
|
|
}
|
|
|
|
Object.entries(ftScoreMap)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 10)
|
|
.forEach(([score, count]) => {
|
|
console.log(` FT ${score}: ${count} matches`);
|
|
});
|
|
|
|
// ANALYSIS 3: Comeback magnitude
|
|
console.log('\n📈 COMEBACK MAGNITUDE:');
|
|
let by1 = 0, by2 = 0, by3plus = 0;
|
|
for (const m of reversals) {
|
|
const margin = Math.abs((m.ftH - m.ftA));
|
|
if (margin === 1) by1++;
|
|
else if (margin === 2) by2++;
|
|
else by3plus++;
|
|
}
|
|
|
|
console.log(` By 1 goal: ${by1} (${(by1/reversals.length*100).toFixed(1)}%)`);
|
|
console.log(` By 2 goals: ${by2} (${(by2/reversals.length*100).toFixed(1)}%)`);
|
|
console.log(` By 3+ goals: ${by3plus} (${(by3plus/reversals.length*100).toFixed(1)}%) ⚠️`);
|
|
|
|
// Show extreme comebacks
|
|
const extreme = reversals
|
|
.filter(m => Math.abs(m.ftH - m.ftA) >= 2)
|
|
.sort((a, b) => Math.abs(b.ftH - b.ftA) - Math.abs(a.ftH - a.ftA))
|
|
.slice(0, 10);
|
|
|
|
console.log('\nTop 10 extreme comebacks (2+ goal margin):');
|
|
for (const m of extreme) {
|
|
const diff = Math.abs(m.ftH - m.ftA);
|
|
console.log(` ${m.league}: ${m.home_team} vs ${m.away_team} | HT: ${m.htH}-${m.htA} => FT: ${m.ftH}-${m.ftA} (margin: ${diff})`);
|
|
}
|
|
|
|
// ANALYSIS 4: 1/2 vs 2/1 split
|
|
const rev_1_2 = reversals.filter(m => m.htft === '1/2');
|
|
const rev_2_1 = reversals.filter(m => m.htft === '2/1');
|
|
|
|
console.log('\n📈 REVERSAL TYPE SPLIT:');
|
|
console.log(` 1/2 (Home leads HT, Away wins FT): ${rev_1_2.length} (${(rev_1_2.length/reversals.length*100).toFixed(1)}%)`);
|
|
console.log(` 2/1 (Away leads HT, Home wins FT): ${rev_2_1.length} (${(rev_2_1.length/reversals.length*100).toFixed(1)}%)`);
|
|
|
|
// Get odds for a sample of reversals
|
|
console.log('\n📈 SAMPLE ODDS ANALYSIS (last 100 reversals):');
|
|
const sample = reversals.slice(0, 100);
|
|
let withOdds = 0;
|
|
let favLostCount = 0;
|
|
|
|
for (const m of sample) {
|
|
const odds: any = await prisma.$queryRaw`
|
|
SELECT oc.name, os.name as selection, os.odd_value
|
|
FROM odd_categories oc
|
|
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id
|
|
WHERE oc.match_id = ${m.id}
|
|
`;
|
|
|
|
if (odds.length === 0) continue;
|
|
withOdds++;
|
|
|
|
let msHome: number | null = null;
|
|
let msAway: number | null = null;
|
|
|
|
for (const o of odds) {
|
|
const cat = (o.name || '').toLowerCase();
|
|
if (cat.includes('maç sonucu')) {
|
|
const sel = (o.selection || '').toLowerCase();
|
|
if (sel === '1') msHome = parseFloat(o.odd_value.toString());
|
|
else if (sel === '2') msAway = parseFloat(o.odd_value.toString());
|
|
}
|
|
}
|
|
|
|
if (msHome && msAway) {
|
|
const favWasHome = msHome < msAway;
|
|
const actualWinner = m.ftH > m.ftA ? '1' : m.ftA > m.ftH ? '2' : 'X';
|
|
|
|
if ((favWasHome && actualWinner === '2') || (!favWasHome && actualWinner === '1')) {
|
|
favLostCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(` Reversals with odds: ${withOdds}/${sample.length}`);
|
|
if (withOdds > 0) {
|
|
console.log(` Favorite lost: ${favLostCount}/${withOdds} (${(favLostCount/withOdds*100).toFixed(1)}%) ⚠️`);
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(80));
|
|
console.log('✅ ANALYSIS COMPLETE');
|
|
console.log('='.repeat(80));
|
|
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
main().catch(console.error);
|