cr
This commit is contained in:
+52
-52
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user