gg
This commit is contained in:
@@ -4,6 +4,11 @@ import {
|
||||
AiEngineClient,
|
||||
AiEngineRequestError,
|
||||
} from "../../../common/utils/ai-engine-client";
|
||||
import {
|
||||
FrequencyEngineService,
|
||||
type MatchCandidate,
|
||||
type FrequencySignal,
|
||||
} from "./frequency-engine.service";
|
||||
|
||||
export type PredictionRiskLevel = "LOW" | "MEDIUM" | "HIGH" | "EXTREME";
|
||||
export type PredictionDataQuality = "HIGH" | "MEDIUM" | "LOW";
|
||||
@@ -131,7 +136,10 @@ export class SmartCouponService {
|
||||
private readonly aiEngineUrl: string;
|
||||
private readonly aiEngineClient: AiEngineClient;
|
||||
|
||||
constructor(private readonly geminiService: GeminiService) {
|
||||
constructor(
|
||||
private readonly geminiService: GeminiService,
|
||||
private readonly frequencyEngine: FrequencyEngineService,
|
||||
) {
|
||||
this.aiEngineUrl = process.env.AI_ENGINE_URL || "http://ai-engine:8000";
|
||||
this.aiEngineClient = new AiEngineClient({
|
||||
baseUrl: this.aiEngineUrl,
|
||||
@@ -244,6 +252,235 @@ export class SmartCouponService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// FREQUENCY-BASED COUPON ENGINE
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
async generateFrequencyBasedCoupon(options: {
|
||||
matchIds?: string[];
|
||||
maxMatches?: number;
|
||||
minSignal?: number;
|
||||
markets?: string[];
|
||||
}): Promise<FrequencyCouponResult> {
|
||||
const maxMatches = options.maxMatches ?? 3;
|
||||
const minSignal = options.minSignal ?? 0.70;
|
||||
const allowedMarkets = options.markets?.map((m) => m.toUpperCase()) || null;
|
||||
|
||||
this.logger.log(
|
||||
`[FrequencyCoupon] Starting — max=${maxMatches}, minSignal=${minSignal}`,
|
||||
);
|
||||
|
||||
// 1. Yaklaşan maçları oranlarıyla getir
|
||||
const upcomingRows = await this.frequencyEngine.getUpcomingMatchesWithOdds(
|
||||
options.matchIds,
|
||||
80,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`[FrequencyCoupon] Found ${upcomingRows.length} upcoming matches with odds`,
|
||||
);
|
||||
|
||||
if (upcomingRows.length === 0) {
|
||||
return {
|
||||
strategy: "FREQUENCY",
|
||||
generated_at: new Date().toISOString(),
|
||||
bets: [],
|
||||
total_odds: 0,
|
||||
expected_hit_rate: 0,
|
||||
expected_value: 0,
|
||||
ev_positive: false,
|
||||
reasoning: ["Bültende uygun maç bulunamadı."],
|
||||
rejected_matches: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Her maç için frekans sinyallerini hesapla (paralel)
|
||||
const candidatePromises = upcomingRows.map((row) =>
|
||||
this.frequencyEngine.buildMatchCandidate(row).then((candidate) => ({
|
||||
candidate,
|
||||
row,
|
||||
})),
|
||||
);
|
||||
const candidateResults = await Promise.all(candidatePromises);
|
||||
|
||||
// 3. Sinyali olan adayları filtrele
|
||||
const allCandidates: Array<{
|
||||
candidate: MatchCandidate;
|
||||
row: (typeof upcomingRows)[0];
|
||||
}> = [];
|
||||
const rejected: FrequencyCouponResult["rejected_matches"] = [];
|
||||
|
||||
for (const { candidate, row } of candidateResults) {
|
||||
if (!candidate) {
|
||||
rejected.push({
|
||||
match_id: row.match_id,
|
||||
match_name: `${row.home_team_name} vs ${row.away_team_name}`,
|
||||
reason: `Yetersiz geçmiş veri (min ${3} maç gerekli)`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Market filtresi uygula
|
||||
let filteredSignals = candidate.signals;
|
||||
if (allowedMarkets) {
|
||||
filteredSignals = filteredSignals.filter((s) =>
|
||||
allowedMarkets.some((m) => s.market.includes(m)),
|
||||
);
|
||||
}
|
||||
|
||||
// Min signal filtresi
|
||||
filteredSignals = filteredSignals.filter(
|
||||
(s) => s.combinedSignal >= minSignal,
|
||||
);
|
||||
|
||||
if (filteredSignals.length === 0) {
|
||||
rejected.push({
|
||||
match_id: row.match_id,
|
||||
match_name: `${row.home_team_name} vs ${row.away_team_name}`,
|
||||
reason: `Kombinasyon sinyali ${(minSignal * 100).toFixed(0)}% eşiğinin altında`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// En güçlü sinyali seç
|
||||
candidate.signals = filteredSignals;
|
||||
candidate.bestSignal = filteredSignals[0];
|
||||
allCandidates.push({ candidate, row });
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[FrequencyCoupon] ${allCandidates.length} candidates passed filters, ${rejected.length} rejected`,
|
||||
);
|
||||
|
||||
// 4. En güçlü sinyale göre sırala
|
||||
allCandidates.sort(
|
||||
(a, b) =>
|
||||
(b.candidate.bestSignal?.confidence ?? 0) -
|
||||
(a.candidate.bestSignal?.confidence ?? 0),
|
||||
);
|
||||
|
||||
// 5. Çeşitlilik: aynı ligden max 2 maç
|
||||
const selected: typeof allCandidates = [];
|
||||
const leagueCount = new Map<string, number>();
|
||||
|
||||
for (const entry of allCandidates) {
|
||||
if (selected.length >= maxMatches) break;
|
||||
|
||||
const lid = entry.candidate.leagueId;
|
||||
const currentCount = leagueCount.get(lid) || 0;
|
||||
if (currentCount >= 2) {
|
||||
rejected.push({
|
||||
match_id: entry.candidate.matchId,
|
||||
match_name: `${entry.candidate.homeTeamName} vs ${entry.candidate.awayTeamName}`,
|
||||
reason: `Aynı ligden zaten 2 maç seçildi (${entry.candidate.leagueName})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
selected.push(entry);
|
||||
leagueCount.set(lid, currentCount + 1);
|
||||
}
|
||||
|
||||
// 6. Sonucu oluştur
|
||||
const bets: FrequencyCouponResult["bets"] = [];
|
||||
let totalOdds = 1;
|
||||
let combinedHitRate = 1;
|
||||
const reasoning: string[] = [];
|
||||
|
||||
for (const { candidate, row } of selected) {
|
||||
const signal = candidate.bestSignal!;
|
||||
const betOdds = this.frequencyEngine.getMarketOdds(row, signal.market);
|
||||
|
||||
if (betOdds <= 0) continue;
|
||||
|
||||
const homeBand = this.frequencyEngine.getOddsBand(candidate.homeOdds);
|
||||
const awayBand = this.frequencyEngine.getOddsBand(candidate.awayOdds);
|
||||
|
||||
// Lig profili belirle
|
||||
let leagueProfile = "NORMAL";
|
||||
if (signal.leagueBonus > 0.02) leagueProfile = "GOLCU";
|
||||
else if (signal.leagueBonus < -0.02) leagueProfile = "DEFANSIF";
|
||||
|
||||
bets.push({
|
||||
match_id: candidate.matchId,
|
||||
match_name: `${candidate.homeTeamName} vs ${candidate.awayTeamName}`,
|
||||
league: candidate.leagueName,
|
||||
market: signal.market,
|
||||
pick: signal.pick,
|
||||
home_signal: parseFloat(signal.homeSignal.toFixed(3)),
|
||||
away_signal: parseFloat(signal.awaySignal.toFixed(3)),
|
||||
combined_signal: parseFloat(signal.combinedSignal.toFixed(3)),
|
||||
league_profile: leagueProfile,
|
||||
historical_hit_rate: parseFloat(signal.combinedSignal.toFixed(3)),
|
||||
odds: betOdds,
|
||||
home_odds_band: homeBand,
|
||||
away_odds_band: awayBand,
|
||||
home_match_count: signal.homeMatchCount,
|
||||
away_match_count: signal.awayMatchCount,
|
||||
});
|
||||
|
||||
totalOdds *= betOdds;
|
||||
combinedHitRate *= signal.combinedSignal;
|
||||
|
||||
reasoning.push(
|
||||
`${candidate.homeTeamName} vs ${candidate.awayTeamName}: ` +
|
||||
`${signal.pick} — Ev(${homeBand}): ${(signal.homeSignal * 100).toFixed(0)}% (${signal.homeMatchCount} maç), ` +
|
||||
`Dep(${awayBand}): ${(signal.awaySignal * 100).toFixed(0)}% (${signal.awayMatchCount} maç)`,
|
||||
);
|
||||
}
|
||||
|
||||
totalOdds = parseFloat(totalOdds.toFixed(2));
|
||||
const expectedValue = parseFloat((combinedHitRate * totalOdds).toFixed(3));
|
||||
|
||||
return {
|
||||
strategy: "FREQUENCY",
|
||||
generated_at: new Date().toISOString(),
|
||||
bets,
|
||||
total_odds: totalOdds,
|
||||
expected_hit_rate: parseFloat(combinedHitRate.toFixed(4)),
|
||||
expected_value: expectedValue,
|
||||
ev_positive: expectedValue > 1.0,
|
||||
reasoning,
|
||||
rejected_matches: rejected,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Frequency Coupon Result Interface
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface FrequencyCouponResult {
|
||||
strategy: "FREQUENCY";
|
||||
generated_at: string;
|
||||
bets: Array<{
|
||||
match_id: string;
|
||||
match_name: string;
|
||||
league: string;
|
||||
market: string;
|
||||
pick: string;
|
||||
home_signal: number;
|
||||
away_signal: number;
|
||||
combined_signal: number;
|
||||
league_profile: string;
|
||||
historical_hit_rate: number;
|
||||
odds: number;
|
||||
home_odds_band: string;
|
||||
away_odds_band: string;
|
||||
home_match_count: number;
|
||||
away_match_count: number;
|
||||
}>;
|
||||
total_odds: number;
|
||||
expected_hit_rate: number;
|
||||
expected_value: number;
|
||||
ev_positive: boolean;
|
||||
reasoning: string[];
|
||||
rejected_matches: Array<{
|
||||
match_id: string;
|
||||
match_name: string;
|
||||
reason: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const MATCH_COMMENTARY_SYSTEM_PROMPT = `Sen uzman bir futbol bahis analistisin. Sana verilen model çıktısını analiz edip kısa, net ve aksiyon odaklı Türkçe bir yorum yaz.
|
||||
|
||||
Reference in New Issue
Block a user