This commit is contained in:
+248
@@ -0,0 +1,248 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
import { GeminiService } from '../../gemini/gemini.service';
|
||||
|
||||
export type PredictionRiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'EXTREME';
|
||||
export type PredictionDataQuality = 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
export type BetGrade = 'A' | 'B' | 'C' | 'PASS';
|
||||
|
||||
export interface PredictionPickRow {
|
||||
market: string;
|
||||
pick: string;
|
||||
probability: number;
|
||||
confidence: number;
|
||||
odds: number;
|
||||
raw_confidence: number;
|
||||
calibrated_confidence: number;
|
||||
min_required_confidence: number;
|
||||
edge: number;
|
||||
play_score: number;
|
||||
playable: boolean;
|
||||
bet_grade: BetGrade;
|
||||
stake_units: number;
|
||||
decision_reasons: string[];
|
||||
}
|
||||
|
||||
export interface PredictionBetSummaryRow {
|
||||
market: string;
|
||||
pick: string;
|
||||
raw_confidence: number;
|
||||
calibrated_confidence: number;
|
||||
bet_grade: BetGrade;
|
||||
playable: boolean;
|
||||
stake_units: number;
|
||||
play_score: number;
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
export interface SingleMatchPredictionPackage {
|
||||
model_version: string;
|
||||
match_info: {
|
||||
match_id: string;
|
||||
match_name: string;
|
||||
home_team: string;
|
||||
away_team: string;
|
||||
league: string;
|
||||
match_date_ms: number;
|
||||
};
|
||||
data_quality: {
|
||||
label: PredictionDataQuality;
|
||||
score: number;
|
||||
flags: string[];
|
||||
home_lineup_count: number;
|
||||
away_lineup_count: number;
|
||||
};
|
||||
risk: {
|
||||
level: PredictionRiskLevel;
|
||||
score: number;
|
||||
is_surprise_risk: boolean;
|
||||
surprise_type: string | null;
|
||||
warnings: string[];
|
||||
};
|
||||
engine_breakdown: {
|
||||
team: number;
|
||||
player: number;
|
||||
odds: number;
|
||||
referee: number;
|
||||
};
|
||||
main_pick: PredictionPickRow | null;
|
||||
value_pick: PredictionPickRow | null;
|
||||
bet_advice: {
|
||||
playable: boolean;
|
||||
suggested_stake_units: number;
|
||||
reason: string;
|
||||
};
|
||||
bet_summary: PredictionBetSummaryRow[];
|
||||
supporting_picks: PredictionPickRow[];
|
||||
aggressive_pick: {
|
||||
market: string;
|
||||
pick: string;
|
||||
probability: number;
|
||||
confidence: number;
|
||||
odds: number | null;
|
||||
} | null;
|
||||
scenario_top5: Array<{
|
||||
score: string;
|
||||
prob: number;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
score_prediction: {
|
||||
ft: string;
|
||||
ht: string;
|
||||
xg_home: number;
|
||||
xg_away: number;
|
||||
xg_total: number;
|
||||
};
|
||||
market_board: Record<string, unknown>;
|
||||
reasoning_factors: string[];
|
||||
ai_commentary?: string | null;
|
||||
}
|
||||
|
||||
export interface SmartCouponResult {
|
||||
strategy: string;
|
||||
generated_at: string;
|
||||
match_count: number;
|
||||
bets: Array<{
|
||||
match_id: string;
|
||||
match_name: string;
|
||||
market: string;
|
||||
pick: string;
|
||||
probability: number;
|
||||
confidence: number;
|
||||
odds: number;
|
||||
risk_level: PredictionRiskLevel;
|
||||
data_quality: PredictionDataQuality;
|
||||
}>;
|
||||
total_odds: number;
|
||||
expected_win_rate: number;
|
||||
rejected_matches: Array<{
|
||||
match_id: string;
|
||||
reason: string;
|
||||
threshold?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SmartCouponService {
|
||||
private readonly logger = new Logger(SmartCouponService.name);
|
||||
private readonly aiEngineUrl: string;
|
||||
|
||||
constructor(private readonly geminiService: GeminiService) {
|
||||
this.aiEngineUrl = process.env.AI_ENGINE_URL || 'http://ai-engine:8000';
|
||||
}
|
||||
|
||||
async analyzeMatch(matchId: string): Promise<SingleMatchPredictionPackage> {
|
||||
let prediction: SingleMatchPredictionPackage;
|
||||
try {
|
||||
const response = await axios.post<SingleMatchPredictionPackage>(
|
||||
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
|
||||
);
|
||||
prediction = response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const detail = error.response?.data?.detail || error.message;
|
||||
throw new HttpException(
|
||||
`AI analyze failed: ${detail}`,
|
||||
error.response?.status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
throw new HttpException(
|
||||
'AI analyze failed',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
|
||||
// Generate AI commentary (non-blocking — fail-safe)
|
||||
prediction.ai_commentary = await this.generateMatchCommentary(prediction);
|
||||
return prediction;
|
||||
}
|
||||
|
||||
private async generateMatchCommentary(
|
||||
prediction: SingleMatchPredictionPackage,
|
||||
): Promise<string | null> {
|
||||
if (!this.geminiService.isAvailable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.geminiService.generateText(
|
||||
JSON.stringify(prediction, null, 2),
|
||||
{
|
||||
model: 'gemini-2.0-flash',
|
||||
temperature: 0.7,
|
||||
maxTokens: 600,
|
||||
systemPrompt: MATCH_COMMENTARY_SYSTEM_PROMPT,
|
||||
},
|
||||
);
|
||||
return result.text || null;
|
||||
} catch (error) {
|
||||
this.logger.warn('AI commentary generation failed, skipping', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async generateDailyBankoCoupon(
|
||||
matchIds: string[],
|
||||
): Promise<SmartCouponResult | null> {
|
||||
if (matchIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getSmartCoupon(matchIds, 'SAFE', {
|
||||
maxMatches: 2,
|
||||
minConfidence: 78,
|
||||
});
|
||||
}
|
||||
|
||||
async getSmartCoupon(
|
||||
matchIds: string[],
|
||||
strategy:
|
||||
| 'SAFE'
|
||||
| 'BALANCED'
|
||||
| 'AGGRESSIVE'
|
||||
| 'VALUE'
|
||||
| 'MIRACLE' = 'BALANCED',
|
||||
options: { maxMatches?: number; minConfidence?: number } = {},
|
||||
): Promise<SmartCouponResult> {
|
||||
try {
|
||||
const response = await axios.post<SmartCouponResult>(
|
||||
`${this.aiEngineUrl}/v20plus/coupon`,
|
||||
{
|
||||
match_ids: matchIds,
|
||||
strategy,
|
||||
max_matches: options.maxMatches,
|
||||
min_confidence: options.minConfidence,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate smart coupon', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
const detail = error.response?.data?.detail || error.message;
|
||||
throw new HttpException(
|
||||
`Coupon generation failed: ${detail}`,
|
||||
error.response?.status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
throw new HttpException(
|
||||
'Coupon generation failed',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
Kurallar:
|
||||
- Max 3-4 kısa paragraf, gereksiz uzatma
|
||||
- Playable olan marketleri ve nedenlerini açıkla
|
||||
- Edge pozitif olan marketleri vurgula (bahisçiden daha iyi biliyoruz)
|
||||
- Tüm edge'ler negatifse "trap maç" olarak uyar
|
||||
- xG ve skor senaryolarına göre strateji öner
|
||||
- Bahis grade'lerini açıkla: A = güvenli, B = iyi, PASS = oynama
|
||||
- Data quality ve risk seviyesini yorumla (kadro onaylı mı, probable XI mi)
|
||||
- "Ben olsam..." formatında kişisel tavsiye ver
|
||||
- Emoji kullan: ⚽ ✅ ⚠️ 🎯 ❌ 💰
|
||||
- Markdown formatı KULLANMA, düz metin yaz
|
||||
- Bahis terminolojisi kullan: edge, value, implied odds, xG`;
|
||||
Reference in New Issue
Block a user