This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user