463 lines
17 KiB
JavaScript
463 lines
17 KiB
JavaScript
const { PrismaClient } = require('@prisma/client');
|
||
const prisma = new PrismaClient();
|
||
|
||
/**
|
||
* 🎯 SÜRPRİZ AVCISI (Upset Hunter)
|
||
* ================================
|
||
* Favorilerin kaybettiği maçları analiz ederek,
|
||
* gelecekteki sürprizleri önceden tespit etmeye çalışıyoruz.
|
||
*/
|
||
|
||
async function upsetHunter() {
|
||
console.log('\n');
|
||
console.log(
|
||
'╔══════════════════════════════════════════════════════════════════╗',
|
||
);
|
||
console.log(
|
||
'║ 🎯 SÜRPRİZ AVCISI (Upset Hunter) ║',
|
||
);
|
||
console.log(
|
||
'║ Favorilerin kaybettiği maçların analizi ║',
|
||
);
|
||
console.log(
|
||
'╚══════════════════════════════════════════════════════════════════╝',
|
||
);
|
||
|
||
// ═════════════════════════════════════════════════════════════════
|
||
// BÖLÜM 1: FAVORİ KAYIPLARINI BUL
|
||
// ═════════════════════════════════════════════════════════════════
|
||
console.log('\n📊 BÖLÜM 1: FAVORİ KAYIPLARINI TESPİT ET');
|
||
console.log('─'.repeat(60));
|
||
|
||
// Favori: MS oranı 1.60'tan düşük olanlar
|
||
// Favori kaybı: Favori takımın kaybettiği maçlar
|
||
|
||
// Bitmiş maçları ve oranlarını al
|
||
const finishedMatches = await prisma.match.findMany({
|
||
where: {
|
||
sport: 'football',
|
||
state: 'postGame',
|
||
scoreHome: { not: null },
|
||
scoreAway: { not: null },
|
||
},
|
||
include: {
|
||
homeTeam: true,
|
||
awayTeam: true,
|
||
league: true,
|
||
oddCategories: {
|
||
include: { selections: true },
|
||
},
|
||
officials: { include: { role: true } },
|
||
},
|
||
take: 500,
|
||
orderBy: { mstUtc: 'desc' },
|
||
});
|
||
|
||
console.log(`\nAnaliz edilen maç sayısı: ${finishedMatches.length}`);
|
||
|
||
// Favori kayıplarını tespit et
|
||
const upsets = [];
|
||
const normalResults = [];
|
||
|
||
for (const match of finishedMatches) {
|
||
// MS oranlarını bul
|
||
let msHome = null,
|
||
msDraw = null,
|
||
msAway = null;
|
||
|
||
for (const cat of match.oddCategories) {
|
||
if (cat.name?.includes('Maç Sonucu') || cat.name === 'MS') {
|
||
for (const sel of cat.selections) {
|
||
if (sel.name === '1') msHome = parseFloat(sel.oddValue);
|
||
if (sel.name === 'X') msDraw = parseFloat(sel.oddValue);
|
||
if (sel.name === '2') msAway = parseFloat(sel.oddValue);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!msHome || !msDraw || !msAway) continue;
|
||
|
||
// Sonucu belirle
|
||
const result =
|
||
match.scoreHome > match.scoreAway
|
||
? '1'
|
||
: match.scoreHome < match.scoreAway
|
||
? '2'
|
||
: 'X';
|
||
|
||
// Favori kim?
|
||
const favorite = msHome < msAway ? '1' : msAway < msHome ? '2' : 'draw';
|
||
const favoriteOdds = favorite === '1' ? msHome : msAway;
|
||
|
||
// Sadece net favori olan maçları al (1.60 altı)
|
||
if (favoriteOdds < 1.6) {
|
||
// Favori kaybetti mi?
|
||
const isUpset =
|
||
(favorite === '1' && result === '2') ||
|
||
(favorite === '2' && result === '1');
|
||
|
||
// X de favori kaybı sayılır (favori kazanamadı)
|
||
const isDrawUpset = result === 'X';
|
||
|
||
if (isUpset || isDrawUpset) {
|
||
upsets.push({
|
||
match,
|
||
favorite,
|
||
favoriteOdds,
|
||
result,
|
||
msHome,
|
||
msDraw,
|
||
msAway,
|
||
isUpset,
|
||
isDrawUpset,
|
||
});
|
||
} else {
|
||
normalResults.push({
|
||
match,
|
||
favorite,
|
||
favoriteOdds,
|
||
result,
|
||
msHome,
|
||
msDraw,
|
||
msAway,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`\n📈 Sonuçlar:`);
|
||
console.log(` Favori kazandı: ${normalResults.length} maç`);
|
||
console.log(
|
||
` Favori kaybetti (SÜRPRİZ): ${upsets.filter((u) => u.isUpset).length} maç`,
|
||
);
|
||
console.log(
|
||
` Favori berabere: ${upsets.filter((u) => u.isDrawUpset).length} maç`,
|
||
);
|
||
|
||
// Sürpriz oranı
|
||
const totalFavMatches = normalResults.length + upsets.length;
|
||
const upsetRate = (
|
||
(upsets.filter((u) => u.isUpset).length / totalFavMatches) *
|
||
100
|
||
).toFixed(1);
|
||
console.log(`\n⚠️ Favori kayıp oranı: %${upsetRate}`);
|
||
|
||
// ═════════════════════════════════════════════════════════════════
|
||
// BÖLÜM 2: SÜRPRİZ MAÇLARIN ORTAK ÖZELLİKLERİ
|
||
// ═════════════════════════════════════════════════════════════════
|
||
console.log('\n\n🔍 BÖLÜM 2: SÜRPRİZ MAÇLARIN ORTAK ÖZELLİKLERİ');
|
||
console.log('─'.repeat(60));
|
||
|
||
// Sadece net sürprizleri (favori kaybı) analiz et
|
||
const realUpsets = upsets.filter((u) => u.isUpset);
|
||
|
||
// 2.1 ORAN ANALİZİ
|
||
console.log('\n📊 2.1 ORAN ANALİZİ:');
|
||
|
||
// Bookmaker margin analizi
|
||
let upsetMargins = [];
|
||
let normalMargins = [];
|
||
|
||
for (const upset of realUpsets) {
|
||
const margin = 1 / upset.msHome + 1 / upset.msDraw + 1 / upset.msAway - 1;
|
||
upsetMargins.push(margin);
|
||
}
|
||
|
||
for (const normal of normalResults) {
|
||
const margin =
|
||
1 / normal.msHome + 1 / normal.msDraw + 1 / normal.msAway - 1;
|
||
normalMargins.push(margin);
|
||
}
|
||
|
||
const avgUpsetMargin =
|
||
upsetMargins.length > 0
|
||
? (
|
||
(upsetMargins.reduce((a, b) => a + b, 0) / upsetMargins.length) *
|
||
100
|
||
).toFixed(1)
|
||
: 0;
|
||
const avgNormalMargin =
|
||
normalMargins.length > 0
|
||
? (
|
||
(normalMargins.reduce((a, b) => a + b, 0) / normalMargins.length) *
|
||
100
|
||
).toFixed(1)
|
||
: 0;
|
||
|
||
console.log(
|
||
` Sürpriz maçlarda ortalama bookmaker margin: %${avgUpsetMargin}`,
|
||
);
|
||
console.log(
|
||
` Normal maçlarda ortalama bookmaker margin: %${avgNormalMargin}`,
|
||
);
|
||
|
||
// Margin farkı yüksek mi?
|
||
if (parseFloat(avgUpsetMargin) > parseFloat(avgNormalMargin) + 2) {
|
||
console.log(` ⚠️ DİKKAT: Sürpriz maçlarda margin YÜKSEK! Şüpheli!`);
|
||
}
|
||
|
||
// 2.2 FAVORİ ORAN ARALIĞI
|
||
console.log('\n📊 2.2 FAVORİ ORAN ARALIĞI:');
|
||
|
||
const upsetOddsRanges = {
|
||
'1.10-1.20': 0,
|
||
'1.20-1.30': 0,
|
||
'1.30-1.40': 0,
|
||
'1.40-1.50': 0,
|
||
'1.50-1.60': 0,
|
||
};
|
||
const normalOddsRanges = {
|
||
'1.10-1.20': 0,
|
||
'1.20-1.30': 0,
|
||
'1.30-1.40': 0,
|
||
'1.40-1.50': 0,
|
||
'1.50-1.60': 0,
|
||
};
|
||
|
||
for (const upset of realUpsets) {
|
||
const odds = upset.favoriteOdds;
|
||
if (odds >= 1.1 && odds < 1.2) upsetOddsRanges['1.10-1.20']++;
|
||
else if (odds >= 1.2 && odds < 1.3) upsetOddsRanges['1.20-1.30']++;
|
||
else if (odds >= 1.3 && odds < 1.4) upsetOddsRanges['1.30-1.40']++;
|
||
else if (odds >= 1.4 && odds < 1.5) upsetOddsRanges['1.40-1.50']++;
|
||
else if (odds >= 1.5 && odds < 1.6) upsetOddsRanges['1.50-1.60']++;
|
||
}
|
||
|
||
for (const normal of normalResults) {
|
||
const odds = normal.favoriteOdds;
|
||
if (odds >= 1.1 && odds < 1.2) normalOddsRanges['1.10-1.20']++;
|
||
else if (odds >= 1.2 && odds < 1.3) normalOddsRanges['1.20-1.30']++;
|
||
else if (odds >= 1.3 && odds < 1.4) normalOddsRanges['1.30-1.40']++;
|
||
else if (odds >= 1.4 && odds < 1.5) normalOddsRanges['1.40-1.50']++;
|
||
else if (odds >= 1.5 && odds < 1.6) normalOddsRanges['1.50-1.60']++;
|
||
}
|
||
|
||
console.log('\n Sürpriz maçlarda favori oran dağılımı:');
|
||
for (const [range, count] of Object.entries(upsetOddsRanges)) {
|
||
const total = count + normalOddsRanges[range];
|
||
const pct = total > 0 ? ((count / total) * 100).toFixed(1) : 0;
|
||
console.log(
|
||
` ${range}: ${count} sürpriz / ${total} toplam → %${pct} sürpriz oranı`,
|
||
);
|
||
}
|
||
|
||
// 2.3 HAKEM ANALİZİ
|
||
console.log('\n📊 2.3 HAKEM ANALİZİ:');
|
||
|
||
const upsetReferees = {};
|
||
for (const upset of realUpsets) {
|
||
const referee = upset.match.officials?.find(
|
||
(o) => o.role?.name === 'Orta Hakem',
|
||
);
|
||
if (referee) {
|
||
if (!upsetReferees[referee.name]) {
|
||
upsetReferees[referee.name] = { upsets: 0, total: 0 };
|
||
}
|
||
upsetReferees[referee.name].upsets++;
|
||
}
|
||
}
|
||
|
||
// Tüm maçlarda bu hakemler
|
||
const allReferees = {};
|
||
for (const match of finishedMatches) {
|
||
const referee = match.officials?.find((o) => o.role?.name === 'Orta Hakem');
|
||
if (referee) {
|
||
if (!allReferees[referee.name]) {
|
||
allReferees[referee.name] = 0;
|
||
}
|
||
allReferees[referee.name]++;
|
||
}
|
||
}
|
||
|
||
// En çok sürprize sebep olan hakemler
|
||
const sortedUpsetReferees = Object.entries(upsetReferees)
|
||
.map(([name, data]) => ({
|
||
name,
|
||
upsets: data.upsets,
|
||
total: allReferees[name] || data.upsets,
|
||
rate: ((data.upsets / (allReferees[name] || data.upsets)) * 100).toFixed(
|
||
1,
|
||
),
|
||
}))
|
||
.filter((r) => r.total >= 3) // En az 3 maç yönetenler
|
||
.sort((a, b) => parseFloat(b.rate) - parseFloat(a.rate));
|
||
|
||
console.log('\n En çok sürpriz yaşatan hakemler:');
|
||
for (const ref of sortedUpsetReferees.slice(0, 5)) {
|
||
console.log(
|
||
` ${ref.name}: ${ref.upsets}/${ref.total} maç → %${ref.rate} sürpriz`,
|
||
);
|
||
}
|
||
|
||
// 2.4 LİG ANALİZİ
|
||
console.log('\n📊 2.4 LİG ANALİZİ:');
|
||
|
||
const upsetLeagues = {};
|
||
const allLeagues = {};
|
||
|
||
for (const upset of realUpsets) {
|
||
const leagueName = upset.match.league?.name || 'Bilinmeyen';
|
||
if (!upsetLeagues[leagueName]) upsetLeagues[leagueName] = 0;
|
||
upsetLeagues[leagueName]++;
|
||
}
|
||
|
||
for (const match of finishedMatches) {
|
||
const leagueName = match.league?.name || 'Bilinmeyen';
|
||
if (!allLeagues[leagueName]) allLeagues[leagueName] = 0;
|
||
allLeagues[leagueName]++;
|
||
}
|
||
|
||
const sortedUpsetLeagues = Object.entries(upsetLeagues)
|
||
.map(([name, count]) => ({
|
||
name,
|
||
upsets: count,
|
||
total: allLeagues[name] || count,
|
||
rate: ((count / (allLeagues[name] || count)) * 100).toFixed(1),
|
||
}))
|
||
.filter((l) => l.total >= 5)
|
||
.sort((a, b) => parseFloat(b.rate) - parseFloat(a.rate));
|
||
|
||
console.log('\n En çok sürpriz yaşanan ligler:');
|
||
for (const league of sortedUpsetLeagues.slice(0, 5)) {
|
||
console.log(
|
||
` ${league.name}: ${league.upsets}/${league.total} maç → %${league.rate} sürpriz`,
|
||
);
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════
|
||
// BÖLÜM 3: SÜRPRİZ TESPİT MODELİ
|
||
// ═════════════════════════════════════════════════════════════════
|
||
console.log('\n\n🎯 BÖLÜM 3: SÜRPRİZ TESPİT İŞARETLERİ');
|
||
console.log('─'.repeat(60));
|
||
|
||
console.log(`
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ SÜRPRİZ TESPİT İŞARETLERİ (Upset Indicators) │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 1. ⚠️ YÜKSEK MARGIN (%18+) │
|
||
│ → Bookmaker kendini koruyor, favori riskli │
|
||
│ │
|
||
│ 2. 👨⚖️ SÜRPRİZ HAKEM │
|
||
│ → Bazı hakemler favorilere karşı sert │
|
||
│ │
|
||
│ 3. 📉 ORAN HAREKETİ │
|
||
│ → Favori oranı yükseliyorsa, para dışarı akıyor │
|
||
│ │
|
||
│ 4. 🏆 DERBİ/ÖZEL MAÇ │
|
||
│ → Form tablosu işlemez, motivasyon farkı │
|
||
│ │
|
||
│ 5. 📊 ÇOK DÜŞÜK FAVORİ ORANI (<1.20) │
|
||
│ → "Çok iyi görünen" fırsatlar genelde tuzak │
|
||
│ │
|
||
│ 6. 🔄 H2H SÜRPRİZ GEÇMİŞİ │
|
||
│ → Geçmişte sürpriz olmuşsa tekrar edebilir │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
`);
|
||
|
||
// Real Madrid örneği
|
||
console.log('\n📌 REAL MADRID vs GETAFE ANALİZİ (Sürpriz Neden Oldu?):');
|
||
console.log('─'.repeat(60));
|
||
|
||
const realMadridMatch = finishedMatches.find(
|
||
(m) => m.id === '2m4sef2l4im49rda90k3p41lg',
|
||
);
|
||
if (realMadridMatch) {
|
||
let msH = null,
|
||
msD = null,
|
||
msA = null;
|
||
for (const cat of realMadridMatch.oddCategories) {
|
||
if (cat.name?.includes('Maç Sonucu') || cat.name === 'MS') {
|
||
for (const sel of cat.selections) {
|
||
if (sel.name === '1') msH = parseFloat(sel.oddValue);
|
||
if (sel.name === 'X') msD = parseFloat(sel.oddValue);
|
||
if (sel.name === '2') msA = parseFloat(sel.oddValue);
|
||
}
|
||
}
|
||
}
|
||
|
||
const margin =
|
||
msH && msD && msA
|
||
? ((1 / msH + 1 / msD + 1 / msA - 1) * 100).toFixed(1)
|
||
: 'N/A';
|
||
|
||
console.log(`\n 📊 Oranlar: 1=${msH} | X=${msD} | 2=${msA}`);
|
||
console.log(
|
||
` 📈 Bookmaker Margin: %${margin} ${parseFloat(margin) > 18 ? '⚠️ YÜKSEK!' : ''}`,
|
||
);
|
||
|
||
const referee = realMadridMatch.officials?.find(
|
||
(o) => o.role?.name === 'Orta Hakem',
|
||
);
|
||
if (referee) {
|
||
const refUpsetData = upsetReferees[referee.name];
|
||
console.log(` 👨⚖️ Hakem: ${referee.name}`);
|
||
if (refUpsetData) {
|
||
console.log(
|
||
` Bu hakemde sürpriz oranı: %${((refUpsetData.upsets / allReferees[referee.name]) * 100).toFixed(1)}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
console.log(`\n ✅ SÜRPRİZ İŞARETLERİ:`);
|
||
if (parseFloat(margin) > 18) {
|
||
console.log(` ⚠️ Margin yüksek → Bookmaker risk görüyordu`);
|
||
}
|
||
console.log(` 🏆 Madrid derbisi → Derbide form işlemez`);
|
||
console.log(` 📉 Favori oranı çok düşük (1.25) → "Tuzak" oranı`);
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════
|
||
// BÖLÜM 4: SÜRPRİZ TAHMİN FONKSİYONU
|
||
// ═════════════════════════════════════════════════════════════════
|
||
console.log('\n\n🎯 BÖLÜM 4: SÜRPRİZ TAHMİN FONKSİYONU');
|
||
console.log('─'.repeat(60));
|
||
|
||
console.log(`
|
||
// Sürpriz Skoru Hesaplama
|
||
function calculateUpsetScore(match, odds, referee, league) {
|
||
let score = 0;
|
||
|
||
// 1. Margin Kontrolü
|
||
const margin = (1/odds.ms_h + 1/odds.ms_d + 1/odds.ms_a) - 1;
|
||
if (margin > 0.20) score += 15; // Yüksek margin = risk
|
||
|
||
// 2. Hakem Faktörü
|
||
if (referee.upsetRate > 25) score += 20;
|
||
else if (referee.upsetRate > 20) score += 10;
|
||
|
||
// 3. Favori Oran Çok Düşük
|
||
if (odds.favorite < 1.20) score += 25; // Tuzak oranı
|
||
else if (odds.favorite < 1.30) score += 15;
|
||
|
||
// 4. Derbi/Özel Maç
|
||
if (isDerby(match)) score += 15;
|
||
|
||
// 5. H2H Sürpriz Geçmişi
|
||
if (h2h.upsetCount > 0) score += 10;
|
||
|
||
// 6. Form Farkı Çok Büyük
|
||
if (formDiff > 40) score += 10; // "Çok iyi" görünüyorsa risk
|
||
|
||
return score; // 0-100 arası
|
||
}
|
||
|
||
// EŞİK: 50+ = Sürpriz bekleniyor, Value bet var
|
||
`);
|
||
|
||
console.log('\n📌 ÖRNEK: Real Madrid vs Getafe için sürpriz skoru:');
|
||
console.log(' Margin (%20.1 > %18): +15');
|
||
console.log(' Favori oran (1.25 < 1.30): +15');
|
||
console.log(' Derbi maçı: +15');
|
||
console.log(' Hakem sürpriz oranı yüksek: +10');
|
||
console.log(' ─────────────────────────────');
|
||
console.log(' TOPLAM: 55 → ⚠️ SÜRPRİZ BEKLENİYOR!');
|
||
|
||
console.log('\n');
|
||
|
||
await prisma.$disconnect();
|
||
}
|
||
|
||
upsetHunter().catch(console.error);
|