115 lines
3.7 KiB
TypeScript
Executable File
115 lines
3.7 KiB
TypeScript
Executable File
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<void> {
|
||
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<number> {
|
||
const row = await this.prisma.teamEloRating.findUnique({
|
||
where: { teamId },
|
||
select: { overallElo: true },
|
||
});
|
||
return row?.overallElo ?? 1500.0;
|
||
}
|
||
}
|