import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../../database/prisma.service'; @Injectable() export class AiFeatureStoreService { private readonly logger = new Logger(AiFeatureStoreService.name); constructor(private readonly prisma: PrismaService) {} /** * Bir maç için AI özelliklerini hesaplar ve 'match_ai_features' tablosuna yazar. * Bu metod Feeder yeni veri çektiğinde tetiklenmelidir. */ async calculateAndSaveFeatures(matchId: string): Promise { const match = await this.prisma.match.findUnique({ where: { id: matchId }, include: { homeTeam: { include: { homeMatches: { take: 5, orderBy: { mstUtc: 'desc' } } }, }, awayTeam: { include: { awayMatches: { take: 5, orderBy: { mstUtc: 'desc' } } }, }, }, }); if (!match || !match.homeTeam || !match.awayTeam) return; // 1. Form Score Calculation (0-100) // Son 5 maçtaki galibiyet, beraberlik ve atılan gollerin ağırlıklı ortalaması const homeForm = this.calculateFormScore(match.homeTeam.homeMatches); const awayForm = this.calculateFormScore(match.awayTeam.awayMatches); // 2. ELO — Read from team_elo_ratings table (populated by AI Engine compute_elo.py) const homeElo = match.homeTeamId ? await this.getTeamElo(match.homeTeamId) : 1500.0; const awayElo = match.awayTeamId ? await this.getTeamElo(match.awayTeamId) : 1500.0; // 3. Missing Player Impact (Sakat/Cezalı etkisi) // Feeder'dan gelen lineups verisindeki eksik as oyuncuları analiz etmeliyiz. // Şimdilik 0.0 (Etkisiz) olarak set ediyoruz, ilerde Lineup analizi buraya eklenecek. const missingImpact = 0.0; // 4. Save to Feature Store await this.prisma.footballAiFeature.upsert({ where: { matchId }, update: { homeElo, awayElo, homeFormScore: homeForm, awayFormScore: awayForm, missingPlayersImpact: missingImpact, updatedAt: new Date(), }, create: { matchId, homeElo, awayElo, homeFormScore: homeForm, awayFormScore: awayForm, missingPlayersImpact: missingImpact, }, }); this.logger.debug( `Features calculated for match ${matchId} (Home Form: ${homeForm}, Away Form: ${awayForm})`, ); } /** * Form Puanı Hesaplama Algoritması (V17 Simplified) * W=30, D=10, L=0 puan. + Gol başına 5 puan (max 15). * Toplam skor 0-100 arasına normalize edilir. */ private calculateFormScore(matches: any[]): number { if (!matches || matches.length === 0) return 50; // Nötr form let totalPoints = 0; const maxPoints = matches.length * 45; // Max olası puan (30win + 15goal) for (const m of matches) { // Skor kontrolü (bazı maçlar oynanmamış olabilir) if (m.scoreHome === null || m.scoreAway === null) continue; const isWin = m.scoreHome > m.scoreAway; // Home team context const isDraw = m.scoreHome === m.scoreAway; if (isWin) totalPoints += 30; else if (isDraw) totalPoints += 10; const goals = Math.min(m.scoreHome, 3); // Max 3 gol katkısı totalPoints += goals * 5; } // Normalize to 0-100 // Eğer hiç maç oynanmadıysa yine 50 dön. return matches.length > 0 ? (totalPoints / maxPoints) * 100 : 50; } /** * team_elo_ratings tablosundan takımın güncel ELO puanını okur. * Kayıt yoksa varsayılan 1500.0 döner. */ private async getTeamElo(teamId: string): Promise { const row = await this.prisma.teamEloRating.findUnique({ where: { teamId }, select: { overallElo: true }, }); return row?.overallElo ?? 1500.0; } }