Files
iddaai-be/analyze-reversal-fast.ts
T
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

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);