This commit is contained in:
2026-04-16 17:21:48 +03:00
parent c8fa4c442d
commit c8e7e4e927
116 changed files with 3720 additions and 4197 deletions
+52 -52
View File
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import { Injectable, Logger } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { ConfigService } from "@nestjs/config";
import { firstValueFrom } from "rxjs";
export interface AIPredictionResult {
matchId: string;
@@ -46,7 +46,7 @@ export class AiService {
private readonly configService: ConfigService,
) {
this.pythonEngineUrl =
this.configService.get('AI_ENGINE_URL') || 'http://127.0.0.1:8000';
this.configService.get("AI_ENGINE_URL") || "http://127.0.0.1:8000";
}
/**
@@ -61,9 +61,9 @@ export class AiService {
_eventData: any[],
): Promise<AIPredictionResult | null> {
try {
const matchId = String(matchDetails?.matchId || '').trim();
const matchId = String(matchDetails?.matchId || "").trim();
if (!matchId) {
this.logger.warn('Skipping AI call: missing matchId');
this.logger.warn("Skipping AI call: missing matchId");
return null;
}
@@ -102,18 +102,18 @@ export class AiService {
.map((p: any) => `${p.market}: ${p.pick}`);
const mappedPredictions = picks.map((p: any) => ({
betType: String(p.market || ''),
prediction: String(p.pick || ''),
betType: String(p.market || ""),
prediction: String(p.pick || ""),
confidence: Number(p.calibrated_confidence ?? p.confidence ?? 0),
probabilities: {},
reasoning: Array.isArray(p.reasons)
? p.reasons.join(' | ')
? p.reasons.join(" | ")
: Array.isArray(p.decision_reasons)
? p.decision_reasons.join(' | ')
: '',
odd: typeof p.odds === 'number' ? p.odds : undefined,
? p.decision_reasons.join(" | ")
: "",
odd: typeof p.odds === "number" ? p.odds : undefined,
valueBet:
typeof p.edge === 'number'
typeof p.edge === "number"
? {
isValue: p.edge > 0,
edge: p.edge,
@@ -138,8 +138,8 @@ export class AiService {
: recommendedBets,
homeAnalysis: undefined,
awayAnalysis: undefined,
expertComment: data.ai_commentary || data.expert_comment || '',
modelVersion: data.model_version || 'v25.main',
expertComment: data.ai_commentary || data.expert_comment || "",
modelVersion: data.model_version || "v25.main",
confidenceScore:
confidenceScore > 1 ? confidenceScore : confidenceScore * 100,
expectedGoals: data?.score_prediction?.xg_total,
@@ -161,10 +161,10 @@ export class AiService {
prediction: p.pick,
confidence: p.calibrated_confidence ?? p.confidence ?? 0,
probabilities: {},
reasoning: Array.isArray(p.reasons) ? p.reasons.join(' | ') : '',
reasoning: Array.isArray(p.reasons) ? p.reasons.join(" | ") : "",
odd: p.odds || 0,
valueBet: {
is_value: typeof p.edge === 'number' ? p.edge > 0 : false,
is_value: typeof p.edge === "number" ? p.edge > 0 : false,
edge: p.edge || 0,
},
}));
@@ -176,21 +176,21 @@ export class AiService {
valueBets: allPredictions.filter((p: any) => p.valueBet?.is_value),
homeAnalysis: null,
awayAnalysis: null,
expertComment: pyData.ai_commentary || '',
winnerPrediction: firstPick?.prediction || 'N/A',
scorePrediction: pyData.score_prediction?.ft || '-',
expertComment: pyData.ai_commentary || "",
winnerPrediction: firstPick?.prediction || "N/A",
scorePrediction: pyData.score_prediction?.ft || "-",
confidenceScore:
typeof firstPick?.confidence === 'number' ? firstPick.confidence : 0,
modelVersion: pyData.model_version || 'v25.main',
typeof firstPick?.confidence === "number" ? firstPick.confidence : 0,
modelVersion: pyData.model_version || "v25.main",
expectedGoals: pyData.score_prediction?.xg_total || 0,
keyInsights: [
`Model: ${pyData.model_version || 'v25.main'}`,
`Risk: ${pyData.risk?.level || 'N/A'} (${pyData.risk?.score ?? 0})`,
`Data Quality: ${pyData.data_quality?.label || 'N/A'}`,
`Model: ${pyData.model_version || "v25.main"}`,
`Risk: ${pyData.risk?.level || "N/A"} (${pyData.risk?.score ?? 0})`,
`Data Quality: ${pyData.data_quality?.label || "N/A"}`,
`xG Beklentisi: ${
typeof pyData.score_prediction?.xg_total === 'number'
typeof pyData.score_prediction?.xg_total === "number"
? pyData.score_prediction.xg_total.toFixed(2)
: 'N/A'
: "N/A"
}`,
],
};
@@ -206,33 +206,33 @@ export class AiService {
// MS 1 oranını bul
const ms1 = odds.find(
(o: any) =>
o.category?.toLowerCase().includes('maç sonucu') && o.selection === '1',
o.category?.toLowerCase().includes("maç sonucu") && o.selection === "1",
);
// KG Var oranını bul
const kgVar = odds.find(
(o: any) =>
o.category?.toLowerCase().includes('karşılıklı gol') &&
o.selection?.toLowerCase() === 'var',
o.category?.toLowerCase().includes("karşılıklı gol") &&
o.selection?.toLowerCase() === "var",
);
// Alt 2.5 oranını bul
const alt25 = odds.find(
(o: any) =>
o.category?.toLowerCase().includes('alt/üst') &&
o.selection?.toLowerCase() === 'alt',
o.category?.toLowerCase().includes("alt/üst") &&
o.selection?.toLowerCase() === "alt",
);
// Tactic 1: Benzer MS oranları
if (ms1?.odd_value) {
tactics.push({
tacticName: 'Benzer Maç Sonucu Oranları',
tacticName: "Benzer Maç Sonucu Oranları",
description:
'Ev sahibi galibiyeti için benzer oran aralığındaki maçlar',
"Ev sahibi galibiyeti için benzer oran aralığındaki maçlar",
odds: [
{
categoryName: 'Maç Sonucu',
selectionName: '1',
categoryName: "Maç Sonucu",
selectionName: "1",
value: parseFloat(ms1.odd_value),
tolerance: 0.3,
},
@@ -243,18 +243,18 @@ export class AiService {
// Tactic 2: Benzer KG + AU oranları
if (kgVar?.odd_value && alt25?.odd_value) {
tactics.push({
tacticName: 'Benzer Gol Beklentisi',
description: 'Karşılıklı gol ve toplam gol benzerliği',
tacticName: "Benzer Gol Beklentisi",
description: "Karşılıklı gol ve toplam gol benzerliği",
odds: [
{
categoryName: 'Karşılıklı Gol',
selectionName: 'Var',
categoryName: "Karşılıklı Gol",
selectionName: "Var",
value: parseFloat(kgVar.odd_value),
tolerance: 0.4,
},
{
categoryName: '2,5 Alt/Üst',
selectionName: 'Alt',
categoryName: "2,5 Alt/Üst",
selectionName: "Alt",
value: parseFloat(alt25.odd_value),
tolerance: 0.3,
},
@@ -265,12 +265,12 @@ export class AiService {
// Tactic 3: Favori analizi
if (ms1?.odd_value && parseFloat(ms1.odd_value) < 1.8) {
tactics.push({
tacticName: 'Favori Takım Analizi',
description: 'Benzer şekilde favori olan ev sahibi takımların maçları',
tacticName: "Favori Takım Analizi",
description: "Benzer şekilde favori olan ev sahibi takımların maçları",
odds: [
{
categoryName: 'Maç Sonucu',
selectionName: '1',
categoryName: "Maç Sonucu",
selectionName: "1",
value: parseFloat(ms1.odd_value),
tolerance: 0.2,
},
@@ -291,7 +291,7 @@ export class AiService {
timeout: 5000,
}),
);
return response.data?.status === 'healthy';
return response.data?.status === "healthy";
} catch {
return false;
}
@@ -307,11 +307,11 @@ export class AiService {
valueBets: [],
homeAnalysis: null,
awayAnalysis: null,
expertComment: 'Analiz verisi alınamadı (Python Servis Hatası).',
winnerPrediction: 'N/A',
scorePrediction: '-',
expertComment: "Analiz verisi alınamadı (Python Servis Hatası).",
winnerPrediction: "N/A",
scorePrediction: "-",
confidenceScore: 0,
modelVersion: 'v25.main',
modelVersion: "v25.main",
expectedGoals: 0,
keyInsights: [],
};
+21 -21
View File
@@ -1,7 +1,7 @@
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../database/prisma.service';
import { ScraperService, ScrapedData } from './scraper.service';
import { AiService, AIPredictionResult } from './ai.service';
import { Injectable, Logger, BadRequestException } from "@nestjs/common";
import { PrismaService } from "../database/prisma.service";
import { ScraperService, ScrapedData } from "./scraper.service";
import { AiService, AIPredictionResult } from "./ai.service";
export interface AnalysisResult {
aiAnalysis: AIPredictionResult;
@@ -29,7 +29,7 @@ export class MatchAnalysisService {
// Phase 1: Parse URL
const { matchId, matchSlug, sport } = this.parseUrl(url);
if (!matchId) {
throw new BadRequestException('Invalid match URL');
throw new BadRequestException("Invalid match URL");
}
// Phase 2: Scrape data with retry
@@ -56,14 +56,14 @@ export class MatchAnalysisService {
}
if (attempt === retryDelays.length) {
throw new BadRequestException(
'Maç bilgileri çekilemedi. Lütfen sonra tekrar deneyiniz.',
"Maç bilgileri çekilemedi. Lütfen sonra tekrar deneyiniz.",
);
}
throw error;
}
}
this.logger.log('Phase 1 Complete: Data Scraped');
this.logger.log("Phase 1 Complete: Data Scraped");
// Phase 3: Call Python AI Engine
let pythonAnalysis: AIPredictionResult | null = null;
@@ -81,7 +81,7 @@ export class MatchAnalysisService {
this.logger.warn(`Python Engine error: ${err.message}`);
}
this.logger.log('Phase 2 Complete: Python Engine Consulted');
this.logger.log("Phase 2 Complete: Python Engine Consulted");
// Phase 4: Generate strategy
const matchDNA = await this.aggregateDataForAI(
@@ -121,7 +121,7 @@ export class MatchAnalysisService {
const aiAnalysis = await this.aiService.getPredictionForMatch(aiPayload);
this.logger.log('Phase 5 Complete: Analysis Generated');
this.logger.log("Phase 5 Complete: Analysis Generated");
// Phase 7: Save to DB if user provided
if (userId) {
@@ -146,18 +146,18 @@ export class MatchAnalysisService {
private parseUrl(url: string): {
matchId: string;
matchSlug: string;
sport: 'football' | 'basketball';
sport: "football" | "basketball";
} {
try {
const urlObj = new URL(url);
const pathParts = urlObj.pathname.split('/').filter((p) => p.length > 0);
let sport: 'football' | 'basketball' = 'football';
if (pathParts.includes('basketbol')) sport = 'basketball';
const pathParts = urlObj.pathname.split("/").filter((p) => p.length > 0);
let sport: "football" | "basketball" = "football";
if (pathParts.includes("basketbol")) sport = "basketball";
const lastPart = pathParts[pathParts.length - 1];
const slugPart = pathParts[pathParts.length - 2];
return { matchId: lastPart, matchSlug: slugPart, sport };
} catch {
return { matchId: '', matchSlug: '', sport: 'football' };
return { matchId: "", matchSlug: "", sport: "football" };
}
}
@@ -205,9 +205,9 @@ export class MatchAnalysisService {
{ homeTeamId, awayTeamId },
{ homeTeamId: awayTeamId, awayTeamId: homeTeamId },
],
state: 'Finished',
state: "Finished",
},
orderBy: { mstUtc: 'desc' },
orderBy: { mstUtc: "desc" },
take: 10,
select: {
id: true,
@@ -222,9 +222,9 @@ export class MatchAnalysisService {
const homeForm = await this.prisma.match.findMany({
where: {
OR: [{ homeTeamId }, { awayTeamId: homeTeamId }],
state: 'Finished',
state: "Finished",
},
orderBy: { mstUtc: 'desc' },
orderBy: { mstUtc: "desc" },
take: 5,
select: {
id: true,
@@ -239,9 +239,9 @@ export class MatchAnalysisService {
const awayForm = await this.prisma.match.findMany({
where: {
OR: [{ homeTeamId: awayTeamId }, { awayTeamId }],
state: 'Finished',
state: "Finished",
},
orderBy: { mstUtc: 'desc' },
orderBy: { mstUtc: "desc" },
take: 5,
select: {
id: true,
@@ -277,7 +277,7 @@ export class MatchAnalysisService {
scoreHome: { not: null },
},
take: 50,
orderBy: { mstUtc: 'desc' },
orderBy: { mstUtc: "desc" },
select: {
id: true,
matchName: true,
+15 -15
View File
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import * as cheerio from 'cheerio';
import { Injectable, Logger } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs";
import * as cheerio from "cheerio";
export interface ScrapedMatchDetails {
homeTeam: string;
@@ -44,9 +44,9 @@ export class ScraperService {
async scrapeMatchData(
matchId: string,
matchSlug: string,
sport: 'football' | 'basketball',
sport: "football" | "basketball",
): Promise<ScrapedData> {
const sportPath = sport === 'basketball' ? 'basketbol/mac' : 'mac';
const sportPath = sport === "basketball" ? "basketbol/mac" : "mac";
const oddsUrl = `https://www.mackolik.com/${sportPath}/${matchSlug}/iddaa/${matchId}`;
const infoUrl = `https://www.mackolik.com/${sportPath}/${matchSlug}/${matchId}`;
@@ -104,8 +104,8 @@ export class ScraperService {
const $ = cheerio.load(html);
const settings: any[] = [];
$('[data-settings]').each((i, elem) => {
const settingsJson = $(elem).attr('data-settings');
$("[data-settings]").each((i, elem) => {
const settingsJson = $(elem).attr("data-settings");
if (settingsJson) {
try {
settings.push(JSON.parse(settingsJson));
@@ -162,14 +162,14 @@ export class ScraperService {
const prf = dataLayer?.prf || {};
return {
homeTeam: matchInfo?.homeTeamName || prf.team1Name || 'Unknown',
awayTeam: matchInfo?.awayTeamName || prf.team2Name || 'Unknown',
homeTeamId: prf.team1Id?.toString() || '0',
awayTeamId: prf.team2Id?.toString() || '0',
league: prf.competitionName || 'Unknown',
homeTeam: matchInfo?.homeTeamName || prf.team1Name || "Unknown",
awayTeam: matchInfo?.awayTeamName || prf.team2Name || "Unknown",
homeTeamId: prf.team1Id?.toString() || "0",
awayTeamId: prf.team2Id?.toString() || "0",
league: prf.competitionName || "Unknown",
scoreHome: prf.team1Score != null ? parseInt(prf.team1Score) : null,
scoreAway: prf.team2Score != null ? parseInt(prf.team2Score) : null,
status: this.extractMatchStatus(settings) || 'NS',
status: this.extractMatchStatus(settings) || "NS",
date: matchHeader?.match?.startTime?.utc
? new Date(matchHeader.match.startTime.utc).toISOString()
: new Date().toISOString(),
@@ -184,7 +184,7 @@ export class ScraperService {
*/
private extractMatchStatus(settings: any[]): string {
const matchState = settings.find((s) => s.matchState || s.status);
return matchState?.matchState || matchState?.status || 'NS';
return matchState?.matchState || matchState?.status || "NS";
}
/**
+9 -9
View File
@@ -1,9 +1,9 @@
import { Module, Global } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { ScraperService } from './scraper.service';
import { AiService } from './ai.service';
import { DatabaseModule } from '../database/database.module';
import { MatchAnalysisService } from './match-analysis.service';
import { Module, Global } from "@nestjs/common";
import { HttpModule } from "@nestjs/axios";
import { ScraperService } from "./scraper.service";
import { AiService } from "./ai.service";
import { DatabaseModule } from "../database/database.module";
import { MatchAnalysisService } from "./match-analysis.service";
@Global()
@Module({
@@ -13,9 +13,9 @@ import { MatchAnalysisService } from './match-analysis.service';
timeout: 35000,
maxRedirects: 5,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept-Language': 'tr-TR,tr;q=0.9,en-US;q=0.8',
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8",
},
}),
],