cr
This commit is contained in:
@@ -1,11 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export type SignalTier =
|
||||
| 'CORE'
|
||||
| 'VALUE'
|
||||
| 'LEAN'
|
||||
| 'LONGSHOT'
|
||||
| 'PASS';
|
||||
export type SignalTier = "CORE" | "VALUE" | "LEAN" | "LONGSHOT" | "PASS";
|
||||
|
||||
export class MatchInfoDto {
|
||||
@ApiProperty()
|
||||
@@ -34,14 +29,14 @@ export class MatchInfoDto {
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
enum: ['football', 'basketball'],
|
||||
enum: ["football", "basketball"],
|
||||
})
|
||||
sport?: 'football' | 'basketball';
|
||||
sport?: "football" | "basketball";
|
||||
}
|
||||
|
||||
export class DataQualityDto {
|
||||
@ApiProperty({ enum: ['HIGH', 'MEDIUM', 'LOW'] })
|
||||
label: 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
@ApiProperty({ enum: ["HIGH", "MEDIUM", "LOW"] })
|
||||
label: "HIGH" | "MEDIUM" | "LOW";
|
||||
|
||||
@ApiProperty()
|
||||
score: number;
|
||||
@@ -52,7 +47,7 @@ export class DataQualityDto {
|
||||
@ApiProperty()
|
||||
away_lineup_count: number;
|
||||
|
||||
@ApiProperty({ required: false, default: 'none' })
|
||||
@ApiProperty({ required: false, default: "none" })
|
||||
lineup_source?: string;
|
||||
|
||||
@ApiProperty({ type: [String] })
|
||||
@@ -69,16 +64,16 @@ export class ConfidenceIntervalDto {
|
||||
@ApiProperty()
|
||||
width: number;
|
||||
|
||||
@ApiProperty({ enum: ['HIGH', 'MEDIUM', 'LOW'] })
|
||||
band: 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
@ApiProperty({ enum: ["HIGH", "MEDIUM", "LOW"] })
|
||||
band: "HIGH" | "MEDIUM" | "LOW";
|
||||
|
||||
@ApiProperty()
|
||||
threshold_met: boolean;
|
||||
}
|
||||
|
||||
export class RiskDto {
|
||||
@ApiProperty({ enum: ['LOW', 'MEDIUM', 'HIGH', 'EXTREME'] })
|
||||
level: 'LOW' | 'MEDIUM' | 'HIGH' | 'EXTREME';
|
||||
@ApiProperty({ enum: ["LOW", "MEDIUM", "HIGH", "EXTREME"] })
|
||||
level: "LOW" | "MEDIUM" | "HIGH" | "EXTREME";
|
||||
|
||||
@ApiProperty()
|
||||
score: number;
|
||||
@@ -156,8 +151,8 @@ export class MatchPickDto {
|
||||
@ApiProperty()
|
||||
playable: boolean;
|
||||
|
||||
@ApiProperty({ enum: ['A', 'B', 'C', 'PASS'] })
|
||||
bet_grade: 'A' | 'B' | 'C' | 'PASS';
|
||||
@ApiProperty({ enum: ["A", "B", "C", "PASS"] })
|
||||
bet_grade: "A" | "B" | "C" | "PASS";
|
||||
|
||||
@ApiProperty()
|
||||
stake_units: number;
|
||||
@@ -170,7 +165,7 @@ export class MatchPickDto {
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
enum: ['CORE', 'VALUE', 'LEAN', 'LONGSHOT', 'PASS'],
|
||||
enum: ["CORE", "VALUE", "LEAN", "LONGSHOT", "PASS"],
|
||||
})
|
||||
signal_tier?: SignalTier;
|
||||
}
|
||||
@@ -185,15 +180,15 @@ export class MatchBetAdviceDto {
|
||||
@ApiProperty()
|
||||
reason: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: ['HIGH', 'MEDIUM', 'LOW'] })
|
||||
confidence_band?: 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
@ApiProperty({ required: false, enum: ["HIGH", "MEDIUM", "LOW"] })
|
||||
confidence_band?: "HIGH" | "MEDIUM" | "LOW";
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
min_confidence_for_play?: number;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
enum: ['CORE', 'VALUE', 'LEAN', 'LONGSHOT', 'PASS'],
|
||||
enum: ["CORE", "VALUE", "LEAN", "LONGSHOT", "PASS"],
|
||||
})
|
||||
signal_tier?: SignalTier;
|
||||
}
|
||||
@@ -211,8 +206,8 @@ export class MatchBetSummaryItemDto {
|
||||
@ApiProperty()
|
||||
calibrated_confidence: number;
|
||||
|
||||
@ApiProperty({ enum: ['A', 'B', 'C', 'PASS'] })
|
||||
bet_grade: 'A' | 'B' | 'C' | 'PASS';
|
||||
@ApiProperty({ enum: ["A", "B", "C", "PASS"] })
|
||||
bet_grade: "A" | "B" | "C" | "PASS";
|
||||
|
||||
@ApiProperty()
|
||||
playable: boolean;
|
||||
@@ -240,30 +235,30 @@ export class MatchBetSummaryItemDto {
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
enum: ['CORE', 'VALUE', 'LEAN', 'LONGSHOT', 'PASS'],
|
||||
enum: ["CORE", "VALUE", "LEAN", "LONGSHOT", "PASS"],
|
||||
})
|
||||
signal_tier?: SignalTier;
|
||||
}
|
||||
|
||||
export class HtFtPredictionDto {
|
||||
@ApiProperty()
|
||||
'1/1': number;
|
||||
"1/1": number;
|
||||
@ApiProperty()
|
||||
'1/X': number;
|
||||
"1/X": number;
|
||||
@ApiProperty()
|
||||
'1/2': number;
|
||||
"1/2": number;
|
||||
@ApiProperty()
|
||||
'X/1': number;
|
||||
"X/1": number;
|
||||
@ApiProperty()
|
||||
'X/X': number;
|
||||
"X/X": number;
|
||||
@ApiProperty()
|
||||
'X/2': number;
|
||||
"X/2": number;
|
||||
@ApiProperty()
|
||||
'2/1': number;
|
||||
"2/1": number;
|
||||
@ApiProperty()
|
||||
'2/X': number;
|
||||
"2/X": number;
|
||||
@ApiProperty()
|
||||
'2/2': number;
|
||||
"2/2": number;
|
||||
@ApiProperty()
|
||||
pick: string;
|
||||
@ApiProperty()
|
||||
@@ -310,8 +305,8 @@ export class AggressivePickDto {
|
||||
@ApiProperty()
|
||||
playable: boolean;
|
||||
|
||||
@ApiProperty({ enum: ['A', 'B', 'C', 'PASS'] })
|
||||
bet_grade: 'A' | 'B' | 'C' | 'PASS';
|
||||
@ApiProperty({ enum: ["A", "B", "C", "PASS"] })
|
||||
bet_grade: "A" | "B" | "C" | "PASS";
|
||||
|
||||
@ApiProperty()
|
||||
stake_units: number;
|
||||
@@ -468,4 +463,4 @@ export class AIHealthDto {
|
||||
predictionServiceReady: boolean;
|
||||
}
|
||||
|
||||
export * from './smart-coupon.dto';
|
||||
export * from "./smart-coupon.dto";
|
||||
|
||||
@@ -8,28 +8,28 @@ import {
|
||||
ArrayMaxSize,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
} from "class-validator";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
|
||||
export class GeneratePredictionDto {
|
||||
@ApiProperty({ description: 'Match ID to generate prediction for' })
|
||||
@ApiProperty({ description: "Match ID to generate prediction for" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
matchId: string;
|
||||
}
|
||||
|
||||
export enum CouponStrategy {
|
||||
SAFE = 'SAFE',
|
||||
BALANCED = 'BALANCED',
|
||||
AGGRESSIVE = 'AGGRESSIVE',
|
||||
VALUE = 'VALUE',
|
||||
MIRACLE = 'MIRACLE',
|
||||
SAFE = "SAFE",
|
||||
BALANCED = "BALANCED",
|
||||
AGGRESSIVE = "AGGRESSIVE",
|
||||
VALUE = "VALUE",
|
||||
MIRACLE = "MIRACLE",
|
||||
}
|
||||
|
||||
export class SmartCouponRequestDto {
|
||||
@ApiProperty({
|
||||
description: 'List of match IDs for coupon',
|
||||
example: ['match-1', 'match-2'],
|
||||
description: "List of match IDs for coupon",
|
||||
example: ["match-1", "match-2"],
|
||||
})
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@@ -44,7 +44,7 @@ export class SmartCouponRequestDto {
|
||||
@IsEnum(CouponStrategy)
|
||||
strategy?: CouponStrategy;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Maximum matches in coupon', example: 5 })
|
||||
@ApiPropertyOptional({ description: "Maximum matches in coupon", example: 5 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@@ -52,7 +52,7 @@ export class SmartCouponRequestDto {
|
||||
maxMatches?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Minimum confidence threshold (0-100)',
|
||||
description: "Minimum confidence threshold (0-100)",
|
||||
example: 60,
|
||||
})
|
||||
@IsOptional()
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
*/
|
||||
|
||||
export type CouponStrategy =
|
||||
| 'SAFE'
|
||||
| 'BALANCED'
|
||||
| 'AGGRESSIVE'
|
||||
| 'VALUE'
|
||||
| 'MIRACLE';
|
||||
| "SAFE"
|
||||
| "BALANCED"
|
||||
| "AGGRESSIVE"
|
||||
| "VALUE"
|
||||
| "MIRACLE";
|
||||
|
||||
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'EXTREME';
|
||||
export type DataQualityLabel = 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
export type RiskLevel = "LOW" | "MEDIUM" | "HIGH" | "EXTREME";
|
||||
export type DataQualityLabel = "HIGH" | "MEDIUM" | "LOW";
|
||||
|
||||
export interface SmartCouponRequestDto {
|
||||
match_ids: string[];
|
||||
|
||||
@@ -7,24 +7,24 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
||||
import { PredictionsService } from './predictions.service';
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
|
||||
import { PredictionsService } from "./predictions.service";
|
||||
import {
|
||||
MatchPredictionDto,
|
||||
PredictionHistoryResponseDto,
|
||||
UpcomingPredictionsDto,
|
||||
ValueBetDto,
|
||||
AIHealthDto,
|
||||
} from './dto';
|
||||
} from "./dto";
|
||||
import {
|
||||
GeneratePredictionDto,
|
||||
SmartCouponRequestDto,
|
||||
} from './dto/predictions-request.dto';
|
||||
import { Public } from 'src/common/decorators';
|
||||
} from "./dto/predictions-request.dto";
|
||||
import { Public } from "src/common/decorators";
|
||||
|
||||
@ApiTags('Predictions')
|
||||
@Controller('predictions')
|
||||
@ApiTags("Predictions")
|
||||
@Controller("predictions")
|
||||
export class PredictionsController {
|
||||
constructor(private readonly predictionsService: PredictionsService) {}
|
||||
|
||||
@@ -32,8 +32,8 @@ export class PredictionsController {
|
||||
* GET /predictions/health
|
||||
* Check AI Engine health status
|
||||
*/
|
||||
@Get('health')
|
||||
@ApiOperation({ summary: 'Check AI Engine health status' })
|
||||
@Get("health")
|
||||
@ApiOperation({ summary: "Check AI Engine health status" })
|
||||
@ApiResponse({ status: 200, type: AIHealthDto })
|
||||
async checkHealth(): Promise<AIHealthDto> {
|
||||
return this.predictionsService.checkHealth();
|
||||
@@ -43,8 +43,8 @@ export class PredictionsController {
|
||||
* GET /predictions/upcoming
|
||||
* Get predictions for upcoming matches
|
||||
*/
|
||||
@Get('upcoming')
|
||||
@ApiOperation({ summary: 'Get predictions for upcoming matches' })
|
||||
@Get("upcoming")
|
||||
@ApiOperation({ summary: "Get predictions for upcoming matches" })
|
||||
@ApiResponse({ status: 200, type: UpcomingPredictionsDto })
|
||||
async getUpcoming(): Promise<UpcomingPredictionsDto> {
|
||||
return this.predictionsService.getUpcomingPredictions();
|
||||
@@ -54,10 +54,10 @@ export class PredictionsController {
|
||||
* GET /predictions/test/:id
|
||||
* Refetch match data and get prediction
|
||||
*/
|
||||
@Get('test/:id')
|
||||
@ApiOperation({ summary: 'Refetch match data and get prediction' })
|
||||
@ApiParam({ name: 'id', description: 'Match ID' })
|
||||
async getTestPrediction(@Param('id') id: string) {
|
||||
@Get("test/:id")
|
||||
@ApiOperation({ summary: "Refetch match data and get prediction" })
|
||||
@ApiParam({ name: "id", description: "Match ID" })
|
||||
async getTestPrediction(@Param("id") id: string) {
|
||||
return this.predictionsService.testPrediction(id);
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ export class PredictionsController {
|
||||
* GET /predictions/value-bets
|
||||
* Get EV+ betting opportunities
|
||||
*/
|
||||
@Get('value-bets')
|
||||
@ApiOperation({ summary: 'Get value betting opportunities (EV+)' })
|
||||
@Get("value-bets")
|
||||
@ApiOperation({ summary: "Get value betting opportunities (EV+)" })
|
||||
@ApiResponse({ status: 200, type: [ValueBetDto] })
|
||||
async getValueBets(): Promise<ValueBetDto[]> {
|
||||
return this.predictionsService.getValueBets();
|
||||
@@ -76,8 +76,8 @@ export class PredictionsController {
|
||||
* GET /predictions/history
|
||||
* Get prediction history and accuracy stats
|
||||
*/
|
||||
@Get('history')
|
||||
@ApiOperation({ summary: 'Get prediction history and accuracy statistics' })
|
||||
@Get("history")
|
||||
@ApiOperation({ summary: "Get prediction history and accuracy statistics" })
|
||||
@ApiResponse({ status: 200, type: PredictionHistoryResponseDto })
|
||||
async getHistory(): Promise<PredictionHistoryResponseDto> {
|
||||
return this.predictionsService.getPredictionHistory();
|
||||
@@ -87,14 +87,14 @@ export class PredictionsController {
|
||||
* GET /predictions/:matchId
|
||||
* Get prediction for a specific match
|
||||
*/
|
||||
@Get(':matchId')
|
||||
@Get(":matchId")
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get prediction for a specific match' })
|
||||
@ApiParam({ name: 'matchId', description: 'Match ID' })
|
||||
@ApiOperation({ summary: "Get prediction for a specific match" })
|
||||
@ApiParam({ name: "matchId", description: "Match ID" })
|
||||
@ApiResponse({ status: 200, type: MatchPredictionDto })
|
||||
@ApiResponse({ status: 404, description: 'Match not found' })
|
||||
@ApiResponse({ status: 404, description: "Match not found" })
|
||||
async getPrediction(
|
||||
@Param('matchId') matchId: string,
|
||||
@Param("matchId") matchId: string,
|
||||
): Promise<MatchPredictionDto> {
|
||||
// Check cache first
|
||||
const cached = await this.predictionsService.getCachedPrediction(matchId);
|
||||
@@ -119,9 +119,9 @@ export class PredictionsController {
|
||||
* POST /predictions/generate
|
||||
* Generate prediction with provided match data
|
||||
*/
|
||||
@Post('generate')
|
||||
@Post("generate")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Generate prediction with provided match data' })
|
||||
@ApiOperation({ summary: "Generate prediction with provided match data" })
|
||||
@ApiResponse({ status: 200, type: MatchPredictionDto })
|
||||
async generatePrediction(
|
||||
@Body() dto: GeneratePredictionDto,
|
||||
@@ -131,7 +131,7 @@ export class PredictionsController {
|
||||
});
|
||||
|
||||
if (!prediction) {
|
||||
throw new NotFoundException('Failed to generate prediction');
|
||||
throw new NotFoundException("Failed to generate prediction");
|
||||
}
|
||||
|
||||
return prediction;
|
||||
@@ -141,19 +141,19 @@ export class PredictionsController {
|
||||
* POST /predictions/smart-coupon
|
||||
* Generate Smart Coupon using AI Engine V20
|
||||
*/
|
||||
@Post('smart-coupon')
|
||||
@Post("smart-coupon")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Generate Smart Coupon with V20 AI recommendations',
|
||||
summary: "Generate Smart Coupon with V20 AI recommendations",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Smart coupon generated successfully',
|
||||
description: "Smart coupon generated successfully",
|
||||
})
|
||||
async generateSmartCoupon(@Body() dto: SmartCouponRequestDto): Promise<any> {
|
||||
const coupon = await this.predictionsService.getSmartCoupon(
|
||||
dto.matchIds,
|
||||
dto.strategy || 'BALANCED',
|
||||
dto.strategy || "BALANCED",
|
||||
{
|
||||
maxMatches: dto.maxMatches,
|
||||
minConfidence: dto.minConfidence,
|
||||
@@ -161,7 +161,7 @@ export class PredictionsController {
|
||||
);
|
||||
|
||||
if (!coupon) {
|
||||
throw new NotFoundException('Failed to generate Smart Coupon');
|
||||
throw new NotFoundException("Failed to generate Smart Coupon");
|
||||
}
|
||||
|
||||
return coupon;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { PredictionsController } from './predictions.controller';
|
||||
import { PredictionsService } from './predictions.service';
|
||||
import { AiFeatureStoreService } from './services/ai-feature-store.service';
|
||||
import { DatabaseModule } from '../../database/database.module';
|
||||
import { MatchesModule } from '../matches/matches.module';
|
||||
import { PredictionsQueue } from './queues/predictions.queue';
|
||||
import { PredictionsProcessor } from './queues/predictions.processor';
|
||||
import { PREDICTIONS_QUEUE } from './queues/predictions.types';
|
||||
import { FeederModule } from '../feeder/feeder.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { PredictionsController } from "./predictions.controller";
|
||||
import { PredictionsService } from "./predictions.service";
|
||||
import { AiFeatureStoreService } from "./services/ai-feature-store.service";
|
||||
import { DatabaseModule } from "../../database/database.module";
|
||||
import { MatchesModule } from "../matches/matches.module";
|
||||
import { PredictionsQueue } from "./queues/predictions.queue";
|
||||
import { PredictionsProcessor } from "./queues/predictions.processor";
|
||||
import { PREDICTIONS_QUEUE } from "./queues/predictions.types";
|
||||
import { FeederModule } from "../feeder/feeder.module";
|
||||
|
||||
const redisEnabled = process.env.REDIS_ENABLED === 'true';
|
||||
const redisEnabled = process.env.REDIS_ENABLED === "true";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@@ -6,26 +6,26 @@ import {
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
Optional,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { QueueEvents } from 'bullmq';
|
||||
import { PredictionsQueue } from './queues/predictions.queue';
|
||||
import { PREDICTIONS_QUEUE } from './queues/predictions.types';
|
||||
} from "@nestjs/common";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { QueueEvents } from "bullmq";
|
||||
import { PredictionsQueue } from "./queues/predictions.queue";
|
||||
import { PREDICTIONS_QUEUE } from "./queues/predictions.types";
|
||||
import {
|
||||
MatchPredictionDto,
|
||||
PredictionHistoryResponseDto,
|
||||
UpcomingPredictionsDto,
|
||||
ValueBetDto,
|
||||
AIHealthDto,
|
||||
} from './dto';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { FeederService } from '../feeder/feeder.service';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
} from "./dto";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { FeederService } from "../feeder/feeder.service";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
type ConfidenceBand = 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
type ConfidenceBand = "HIGH" | "MEDIUM" | "LOW";
|
||||
|
||||
interface ConfidenceInterval {
|
||||
lower: number;
|
||||
@@ -47,73 +47,72 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly aiEngineUrl: string;
|
||||
private readonly topLeagueIds = new Set<string>();
|
||||
private readonly reasonTranslations: Record<string, string> = {
|
||||
confidence_below_threshold: 'Güven eşiğin altında',
|
||||
confidence_interval_too_wide: 'Güven aralığı çok geniş',
|
||||
confidence_below_threshold: "Güven eşiğin altında",
|
||||
confidence_interval_too_wide: "Güven aralığı çok geniş",
|
||||
confidence_interval_too_wide_for_main_pick:
|
||||
'Ana seçim için güven aralığı çok geniş',
|
||||
confidence_band_low: 'Güven bandı düşük',
|
||||
playable_edge_found: 'Oynanabilir avantaj bulundu',
|
||||
market_signal_dominant: 'Piyasa sinyali baskın',
|
||||
team_form_signal_dominant: 'Takım formuna dayalı sinyaller çok baskın',
|
||||
lineup_signal_strong: 'İlk on bir sinyali güçlü',
|
||||
lineup_signal_weak: 'İlk on bir sinyali zayıf',
|
||||
lineup_probable_xi_used: 'Muhtemel ilk on bir kullanıldı',
|
||||
lineup_probable_not_confirmed: 'Muhtemel ilk on bir henüz doğrulanmadı',
|
||||
lineup_unavailable: 'İlk on bir bilgisi mevcut değil',
|
||||
lineup_incomplete: 'İlk on bir bilgisi eksik',
|
||||
missing_referee: 'Hakem verisi eksik',
|
||||
draw_probability_elevated: 'Beraberlik olasılığı yükselmiş görünüyor',
|
||||
balanced_match_risk: 'Maç dengeli, sürpriz riski yükseliyor',
|
||||
draw_pressure: 'Beraberlik baskısı yüksek',
|
||||
upset_risk_detected: 'Sürpriz riski tespit edildi',
|
||||
limited_data_confidence: 'Veri kısıtlı olduğu için güven sınırlı',
|
||||
data_quality_issue: 'Veri kalitesi sorunu var',
|
||||
high_risk_low_data_quality: 'Risk yüksek, veri kalitesi düşük',
|
||||
insufficient_play_score: 'Oynanabilirlik puanı yetersiz',
|
||||
no_bet_conditions_met: 'Bahis koşulları oluşmadı',
|
||||
market_passed_all_gates: 'Market tüm güvenlik kontrollerini geçti',
|
||||
"Ana seçim için güven aralığı çok geniş",
|
||||
confidence_band_low: "Güven bandı düşük",
|
||||
playable_edge_found: "Oynanabilir avantaj bulundu",
|
||||
market_signal_dominant: "Piyasa sinyali baskın",
|
||||
team_form_signal_dominant: "Takım formuna dayalı sinyaller çok baskın",
|
||||
lineup_signal_strong: "İlk on bir sinyali güçlü",
|
||||
lineup_signal_weak: "İlk on bir sinyali zayıf",
|
||||
lineup_probable_xi_used: "Muhtemel ilk on bir kullanıldı",
|
||||
lineup_probable_not_confirmed: "Muhtemel ilk on bir henüz doğrulanmadı",
|
||||
lineup_unavailable: "İlk on bir bilgisi mevcut değil",
|
||||
lineup_incomplete: "İlk on bir bilgisi eksik",
|
||||
missing_referee: "Hakem verisi eksik",
|
||||
draw_probability_elevated: "Beraberlik olasılığı yükselmiş görünüyor",
|
||||
balanced_match_risk: "Maç dengeli, sürpriz riski yükseliyor",
|
||||
draw_pressure: "Beraberlik baskısı yüksek",
|
||||
upset_risk_detected: "Sürpriz riski tespit edildi",
|
||||
limited_data_confidence: "Veri kısıtlı olduğu için güven sınırlı",
|
||||
data_quality_issue: "Veri kalitesi sorunu var",
|
||||
high_risk_low_data_quality: "Risk yüksek, veri kalitesi düşük",
|
||||
insufficient_play_score: "Oynanabilirlik puanı yetersiz",
|
||||
no_bet_conditions_met: "Bahis koşulları oluşmadı",
|
||||
market_passed_all_gates: "Market tüm güvenlik kontrollerini geçti",
|
||||
no_ev_edge_minimum_stake:
|
||||
'Beklenen avantaj oluşmadı, minimum bahis önerildi',
|
||||
player_form_signal_strong: 'Oyuncu formu sinyali güçlü',
|
||||
player_form_signal_limited: 'Oyuncu formu sinyali sınırlı',
|
||||
live_state_impossible_market: 'Canlı maç durumu bu marketi geçersiz kılıyor',
|
||||
live_score_exceeds_under_line:
|
||||
'Mevcut skor bu alt seçeneğiyle çelişiyor',
|
||||
"Beklenen avantaj oluşmadı, minimum bahis önerildi",
|
||||
player_form_signal_strong: "Oyuncu formu sinyali güçlü",
|
||||
player_form_signal_limited: "Oyuncu formu sinyali sınırlı",
|
||||
live_state_impossible_market:
|
||||
"Canlı maç durumu bu marketi geçersiz kılıyor",
|
||||
live_score_exceeds_under_line: "Mevcut skor bu alt seçeneğiyle çelişiyor",
|
||||
score_model_conflicts_with_under_pick:
|
||||
'Skor modeli alt seçeneğiyle çelişiyor',
|
||||
"Skor modeli alt seçeneğiyle çelişiyor",
|
||||
score_model_conflicts_with_over_pick:
|
||||
'Skor modeli üst seçeneğiyle çelişiyor',
|
||||
"Skor modeli üst seçeneğiyle çelişiyor",
|
||||
market_stack_conflict_over25:
|
||||
'Üst 2.5 sinyaliyle çeliştiği için zayıflatıldı',
|
||||
"Üst 2.5 sinyaliyle çeliştiği için zayıflatıldı",
|
||||
market_stack_conflict_btts:
|
||||
'Karşılıklı gol sinyaliyle çeliştiği için zayıflatıldı',
|
||||
"Karşılıklı gol sinyaliyle çeliştiği için zayıflatıldı",
|
||||
first_half_result_conflicts_with_goalless_half:
|
||||
'İlk yarı sonucu beklentisi golsüz ilk yarıyla çelişiyor',
|
||||
"İlk yarı sonucu beklentisi golsüz ilk yarıyla çelişiyor",
|
||||
first_half_htft_conflicts_with_goalless_half:
|
||||
'İlk yarı/maç sonu beklentisi golsüz ilk yarıyla çelişiyor',
|
||||
"İlk yarı/maç sonu beklentisi golsüz ilk yarıyla çelişiyor",
|
||||
first_half_draw_conflicts_with_goal_pick:
|
||||
'İlk yarı beraberlik baskısı erken gol beklentisiyle çelişiyor',
|
||||
"İlk yarı beraberlik baskısı erken gol beklentisiyle çelişiyor",
|
||||
first_half_goalless_conflicts_with_result_pick:
|
||||
'Golsüz ilk yarı beklentisi ilk yarı sonuç seçimiyle çelişiyor',
|
||||
"Golsüz ilk yarı beklentisi ilk yarı sonuç seçimiyle çelişiyor",
|
||||
first_half_goalless_conflicts_with_htft_pick:
|
||||
'Golsüz ilk yarı beklentisi ilk yarı/maç sonu seçimiyle çelişiyor',
|
||||
"Golsüz ilk yarı beklentisi ilk yarı/maç sonu seçimiyle çelişiyor",
|
||||
first_half_goal_pressure_conflicts_with_htft_draw:
|
||||
'İlk yarı gol baskısı ilk yarı beraberlik kurgusuyla çelişiyor',
|
||||
live_total_goals_close_to_line:
|
||||
'Canlı toplam gol çizgisine çok yakın',
|
||||
"İlk yarı gol baskısı ilk yarı beraberlik kurgusuyla çelişiyor",
|
||||
live_total_goals_close_to_line: "Canlı toplam gol çizgisine çok yakın",
|
||||
score_model_conflicts_with_btts_no:
|
||||
'Skor modeli KG Yok seçeneğiyle çelişiyor',
|
||||
"Skor modeli KG Yok seçeneğiyle çelişiyor",
|
||||
score_model_conflicts_with_draw_pick:
|
||||
'Skor modeli beraberlik seçeneğiyle çelişiyor',
|
||||
"Skor modeli beraberlik seçeneğiyle çelişiyor",
|
||||
score_model_conflicts_with_home_pick:
|
||||
'Skor modeli ev sahibi seçeneğiyle çelişiyor',
|
||||
"Skor modeli ev sahibi seçeneğiyle çelişiyor",
|
||||
score_model_conflicts_with_away_pick:
|
||||
'Skor modeli deplasman seçeneğiyle çelişiyor',
|
||||
high_total_goal_volatility: 'Toplam gol volatilitesi yüksek',
|
||||
mutual_goal_pressure: 'İki takımın da gol baskısı yüksek',
|
||||
late_goal_swing_risk: 'Geç gol kaynaklı kırılma riski var',
|
||||
live_match_open_state: 'Canlı maç açık oyuna dönmüş durumda',
|
||||
live_match_active_state: 'Canlı maç aktif ve dalgalı ilerliyor',
|
||||
"Skor modeli deplasman seçeneğiyle çelişiyor",
|
||||
high_total_goal_volatility: "Toplam gol volatilitesi yüksek",
|
||||
mutual_goal_pressure: "İki takımın da gol baskısı yüksek",
|
||||
late_goal_swing_risk: "Geç gol kaynaklı kırılma riski var",
|
||||
live_match_open_state: "Canlı maç açık oyuna dönmüş durumda",
|
||||
live_match_active_state: "Canlı maç aktif ve dalgalı ilerliyor",
|
||||
};
|
||||
|
||||
constructor(
|
||||
@@ -123,8 +122,8 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
@Optional() private readonly predictionsQueue?: PredictionsQueue,
|
||||
) {
|
||||
this.aiEngineUrl = this.configService.get(
|
||||
'AI_ENGINE_URL',
|
||||
'http://localhost:8000',
|
||||
"AI_ENGINE_URL",
|
||||
"http://localhost:8000",
|
||||
);
|
||||
this.topLeagueIds = this.loadTopLeagueIds();
|
||||
}
|
||||
@@ -133,14 +132,14 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
if (this.predictionsQueue) {
|
||||
this.queueEvents = new QueueEvents(PREDICTIONS_QUEUE, {
|
||||
connection: {
|
||||
host: this.configService.get('redis.host', 'localhost'),
|
||||
port: this.configService.get('redis.port', 6379),
|
||||
password: this.configService.get('redis.password'),
|
||||
host: this.configService.get("redis.host", "localhost"),
|
||||
port: this.configService.get("redis.port", 6379),
|
||||
password: this.configService.get("redis.password"),
|
||||
},
|
||||
});
|
||||
this.logger.log('Queue mode enabled for predictions');
|
||||
this.logger.log("Queue mode enabled for predictions");
|
||||
} else {
|
||||
this.logger.log('Direct HTTP mode enabled for predictions (no Redis)');
|
||||
this.logger.log("Direct HTTP mode enabled for predictions (no Redis)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +151,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
checkHealth(): Promise<AIHealthDto> {
|
||||
return Promise.resolve({
|
||||
status: 'healthy',
|
||||
status: "healthy",
|
||||
modelLoaded: true,
|
||||
predictionServiceReady: true,
|
||||
});
|
||||
@@ -212,12 +211,12 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
if (status === 422) {
|
||||
throw new HttpException(
|
||||
`AI Engine: ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`,
|
||||
`AI Engine: ${typeof detail === "string" ? detail : JSON.stringify(detail)}`,
|
||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
);
|
||||
}
|
||||
throw new HttpException(
|
||||
`AI Engine error: ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`,
|
||||
`AI Engine error: ${typeof detail === "string" ? detail : JSON.stringify(detail)}`,
|
||||
status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
@@ -232,7 +231,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
async testPrediction(matchId: string): Promise<MatchPredictionDto | null> {
|
||||
this.logger.log(`[TEST PREDICTION] Syncing match data for ${matchId}...`);
|
||||
// refreshMatch triggers the feeder scraper to get all match info, odds, and lineups and write to DB
|
||||
const refreshResult = await this.feederService.refreshMatch(matchId, 'all');
|
||||
const refreshResult = await this.feederService.refreshMatch(matchId, "all");
|
||||
|
||||
if (!refreshResult.success) {
|
||||
this.logger.warn(
|
||||
@@ -251,7 +250,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
const upcoming = await this.prisma.prediction.findMany({
|
||||
where: {
|
||||
match: {
|
||||
status: 'NS',
|
||||
status: "NS",
|
||||
mstUtc: { gte: Math.floor(Date.now() / 1000) },
|
||||
},
|
||||
},
|
||||
@@ -260,13 +259,13 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
include: { homeTeam: true, awayTeam: true, league: true },
|
||||
},
|
||||
},
|
||||
orderBy: { match: { mstUtc: 'asc' } },
|
||||
orderBy: { match: { mstUtc: "asc" } },
|
||||
take: 50,
|
||||
});
|
||||
|
||||
return {
|
||||
count: upcoming.length,
|
||||
modelVersion: 'v25-v30-ensemble',
|
||||
modelVersion: "v25-v30-ensemble",
|
||||
matches: upcoming.map((p) => {
|
||||
const out = p.predictionJson as Record<string, unknown>;
|
||||
const matchInfo = (out?.match_info || {}) as Record<string, unknown>;
|
||||
@@ -276,9 +275,9 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
...matchInfo,
|
||||
match_name: `${p.match.homeTeam?.name} vs ${p.match.awayTeam?.name}`,
|
||||
match_date_ms: Number(p.match.mstUtc) * 1000,
|
||||
league: p.match.league?.name || '',
|
||||
league: p.match.league?.name || "",
|
||||
league_id: p.match.leagueId,
|
||||
is_top_league: this.topLeagueIds.has(p.match.leagueId ?? ''),
|
||||
is_top_league: this.topLeagueIds.has(p.match.leagueId ?? ""),
|
||||
},
|
||||
} as unknown as MatchPredictionDto;
|
||||
}),
|
||||
@@ -287,12 +286,12 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private loadTopLeagueIds(): Set<string> {
|
||||
try {
|
||||
const topLeaguesPath = path.join(process.cwd(), 'top_leagues.json');
|
||||
const topLeaguesPath = path.join(process.cwd(), "top_leagues.json");
|
||||
if (!fs.existsSync(topLeaguesPath)) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
const raw = JSON.parse(fs.readFileSync(topLeaguesPath, 'utf8'));
|
||||
const raw = JSON.parse(fs.readFileSync(topLeaguesPath, "utf8"));
|
||||
if (!Array.isArray(raw)) {
|
||||
return new Set<string>();
|
||||
}
|
||||
@@ -318,7 +317,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
if (match) {
|
||||
return {
|
||||
leagueId: match.leagueId ?? null,
|
||||
isTopLeague: this.topLeagueIds.has(match.leagueId ?? ''),
|
||||
isTopLeague: this.topLeagueIds.has(match.leagueId ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -329,7 +328,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
return {
|
||||
leagueId: liveMatch?.leagueId ?? null,
|
||||
isTopLeague: this.topLeagueIds.has(liveMatch?.leagueId ?? ''),
|
||||
isTopLeague: this.topLeagueIds.has(liveMatch?.leagueId ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -346,7 +345,8 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
league_id:
|
||||
this.asRecord(response.match_info).league_id ?? matchContext.leagueId,
|
||||
is_top_league:
|
||||
this.asRecord(response.match_info).is_top_league ?? matchContext.isTopLeague,
|
||||
this.asRecord(response.match_info).is_top_league ??
|
||||
matchContext.isTopLeague,
|
||||
};
|
||||
|
||||
const mainPick = this.enrichPick(
|
||||
@@ -369,9 +369,11 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
);
|
||||
|
||||
const supportingPicks = Array.isArray(response.supporting_picks)
|
||||
? response.supporting_picks.map((pick) =>
|
||||
this.enrichPick(pick, response, matchContext, marketBoard),
|
||||
).filter((pick): pick is NonNullable<typeof pick> => pick !== null)
|
||||
? response.supporting_picks
|
||||
.map((pick) =>
|
||||
this.enrichPick(pick, response, matchContext, marketBoard),
|
||||
)
|
||||
.filter((pick): pick is NonNullable<typeof pick> => pick !== null)
|
||||
: [];
|
||||
|
||||
const betSummary = Array.isArray(response.bet_summary)
|
||||
@@ -380,8 +382,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
)
|
||||
: [];
|
||||
|
||||
const mainBand =
|
||||
this.asRecord(mainPick?.confidence_interval).band ?? 'LOW';
|
||||
const mainBand = this.asRecord(mainPick?.confidence_interval).band ?? "LOW";
|
||||
const minConfidenceForPlay = this.getMinConfidenceForPlay(
|
||||
this.asRecord(mainPick).market,
|
||||
matchContext.isTopLeague,
|
||||
@@ -402,7 +403,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
if (mainPick && !isMainPlayable) {
|
||||
reasoningFactors.unshift(
|
||||
this.translateReason('confidence_interval_too_wide_for_main_pick'),
|
||||
this.translateReason("confidence_interval_too_wide_for_main_pick"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -416,9 +417,9 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
isMainPlayable
|
||||
? String(
|
||||
this.asRecord(response.bet_advice).reason ||
|
||||
'playable_edge_found',
|
||||
"playable_edge_found",
|
||||
)
|
||||
: 'confidence_below_threshold',
|
||||
: "confidence_below_threshold",
|
||||
),
|
||||
suggested_stake_units: isMainPlayable
|
||||
? Number(this.asRecord(response.bet_advice).suggested_stake_units ?? 0)
|
||||
@@ -428,15 +429,18 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
const enrichedMarketBoard = Object.fromEntries(
|
||||
Object.entries(marketBoard).map(([market, entry]) => {
|
||||
const record = this.asRecord(entry);
|
||||
const pickName = String(record.pick ?? '');
|
||||
if (!pickName || !record.probs || typeof record.probs !== 'object') {
|
||||
const pickName = String(record.pick ?? "");
|
||||
if (!pickName || !record.probs || typeof record.probs !== "object") {
|
||||
return [market, record];
|
||||
}
|
||||
|
||||
const syntheticPick = {
|
||||
market,
|
||||
pick: pickName,
|
||||
probability: this.lookupProbability(record.probs as Record<string, unknown>, pickName),
|
||||
probability: this.lookupProbability(
|
||||
record.probs as Record<string, unknown>,
|
||||
pickName,
|
||||
),
|
||||
confidence: Number(record.confidence ?? 0),
|
||||
calibrated_confidence: Number(record.confidence ?? 0),
|
||||
raw_confidence: Number(record.confidence ?? 0),
|
||||
@@ -447,7 +451,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
implied_prob: 0,
|
||||
play_score: 0,
|
||||
playable: false,
|
||||
bet_grade: 'PASS',
|
||||
bet_grade: "PASS",
|
||||
stake_units: 0,
|
||||
decision_reasons: [],
|
||||
};
|
||||
@@ -464,7 +468,8 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
{
|
||||
...record,
|
||||
confidence_interval: this.asRecord(enriched?.confidence_interval),
|
||||
confidence_band: this.asRecord(enriched?.confidence_interval).band ?? 'LOW',
|
||||
confidence_band:
|
||||
this.asRecord(enriched?.confidence_interval).band ?? "LOW",
|
||||
},
|
||||
];
|
||||
}),
|
||||
@@ -472,14 +477,14 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
return {
|
||||
...response,
|
||||
match_info: matchInfo as MatchPredictionDto['match_info'],
|
||||
match_info: matchInfo as MatchPredictionDto["match_info"],
|
||||
data_quality: {
|
||||
...dataQuality,
|
||||
lineup_source: String(dataQuality.lineup_source ?? 'none'),
|
||||
} as MatchPredictionDto['data_quality'],
|
||||
lineup_source: String(dataQuality.lineup_source ?? "none"),
|
||||
} as MatchPredictionDto["data_quality"],
|
||||
risk: {
|
||||
...risk,
|
||||
surprise_type: this.translateReason(String(risk.surprise_type ?? '')),
|
||||
surprise_type: this.translateReason(String(risk.surprise_type ?? "")),
|
||||
surprise_reasons: Array.isArray(risk.surprise_reasons)
|
||||
? risk.surprise_reasons.map((reason) =>
|
||||
this.translateReason(String(reason)),
|
||||
@@ -490,13 +495,13 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
this.translateReason(String(warning)),
|
||||
)
|
||||
: [],
|
||||
} as MatchPredictionDto['risk'],
|
||||
} as MatchPredictionDto["risk"],
|
||||
main_pick: mainPick,
|
||||
value_pick: valuePick,
|
||||
aggressive_pick: aggressivePick,
|
||||
supporting_picks: supportingPicks,
|
||||
bet_summary: betSummary,
|
||||
bet_advice: betAdvice as MatchPredictionDto['bet_advice'],
|
||||
bet_advice: betAdvice as MatchPredictionDto["bet_advice"],
|
||||
market_board: enrichedMarketBoard,
|
||||
reasoning_factors: reasoningFactors,
|
||||
};
|
||||
@@ -507,14 +512,14 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
prediction: Record<string, unknown>,
|
||||
matchContext: MatchContext,
|
||||
marketBoard: Record<string, unknown>,
|
||||
): MatchPredictionDto['main_pick'] {
|
||||
if (!pick || typeof pick !== 'object') {
|
||||
): MatchPredictionDto["main_pick"] {
|
||||
if (!pick || typeof pick !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = this.asRecord(pick);
|
||||
const market = String(record.market ?? '');
|
||||
const pickName = String(record.pick ?? '');
|
||||
const market = String(record.market ?? "");
|
||||
const pickName = String(record.pick ?? "");
|
||||
const probs = this.resolveMarketProbabilities(marketBoard, market);
|
||||
const probability =
|
||||
this.asNumber(record.probability) ||
|
||||
@@ -538,7 +543,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
),
|
||||
riskScore: this.normalizeScore(this.asRecord(prediction.risk).score),
|
||||
lineupSource: String(
|
||||
this.asRecord(prediction.data_quality).lineup_source ?? 'none',
|
||||
this.asRecord(prediction.data_quality).lineup_source ?? "none",
|
||||
),
|
||||
isTopLeague: matchContext.isTopLeague,
|
||||
});
|
||||
@@ -547,10 +552,10 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
? [...record.decision_reasons]
|
||||
: [];
|
||||
if (!interval.threshold_met) {
|
||||
nextReasons.push('confidence_interval_too_wide');
|
||||
nextReasons.push("confidence_interval_too_wide");
|
||||
}
|
||||
if (interval.band === 'LOW') {
|
||||
nextReasons.push('confidence_band_low');
|
||||
if (interval.band === "LOW") {
|
||||
nextReasons.push("confidence_band_low");
|
||||
}
|
||||
|
||||
const displayOdds = this.normalizeDisplayOdds(
|
||||
@@ -559,7 +564,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
);
|
||||
|
||||
return {
|
||||
...(record as MatchPredictionDto['main_pick']),
|
||||
...(record as MatchPredictionDto["main_pick"]),
|
||||
market,
|
||||
pick: pickName,
|
||||
probability,
|
||||
@@ -573,7 +578,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
odds: displayOdds,
|
||||
edge: this.asNumber(record.edge),
|
||||
play_score: this.asNumber(record.play_score),
|
||||
bet_grade: String(record.bet_grade || 'PASS') as 'A' | 'B' | 'C' | 'PASS',
|
||||
bet_grade: String(record.bet_grade || "PASS") as "A" | "B" | "C" | "PASS",
|
||||
implied_prob: impliedProb,
|
||||
ev_edge: evEdge,
|
||||
playable: Boolean(record.playable) && interval.threshold_met,
|
||||
@@ -594,10 +599,10 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
prediction: Record<string, unknown>,
|
||||
matchContext: MatchContext,
|
||||
marketBoard: Record<string, unknown>,
|
||||
): MatchPredictionDto['bet_summary'][number] {
|
||||
): MatchPredictionDto["bet_summary"][number] {
|
||||
const record = this.asRecord(item);
|
||||
const market = String(record.market ?? '');
|
||||
const pickName = String(record.pick ?? '');
|
||||
const market = String(record.market ?? "");
|
||||
const pickName = String(record.pick ?? "");
|
||||
const probs = this.resolveMarketProbabilities(marketBoard, market);
|
||||
const probability = this.lookupProbability(probs, pickName);
|
||||
const calibratedConfidence =
|
||||
@@ -621,13 +626,13 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
),
|
||||
riskScore: this.normalizeScore(this.asRecord(prediction.risk).score),
|
||||
lineupSource: String(
|
||||
this.asRecord(prediction.data_quality).lineup_source ?? 'none',
|
||||
this.asRecord(prediction.data_quality).lineup_source ?? "none",
|
||||
),
|
||||
isTopLeague: matchContext.isTopLeague,
|
||||
});
|
||||
|
||||
return {
|
||||
...(record as MatchPredictionDto['bet_summary'][number]),
|
||||
...(record as MatchPredictionDto["bet_summary"][number]),
|
||||
odds: this.normalizeDisplayOdds(odds, impliedProb),
|
||||
implied_prob: impliedProb,
|
||||
ev_edge: evEdge,
|
||||
@@ -658,12 +663,10 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private translateReason(reason: string): string {
|
||||
if (!reason) {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
const normalized = reason.startsWith('risk:')
|
||||
? reason.slice(5)
|
||||
: reason;
|
||||
const normalized = reason.startsWith("risk:") ? reason.slice(5) : reason;
|
||||
|
||||
if (this.reasonTranslations[normalized]) {
|
||||
return this.reasonTranslations[normalized];
|
||||
@@ -674,7 +677,9 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
return `Beklenen avantaj ${evMatch[1]} (Not ${evMatch[2]})`;
|
||||
}
|
||||
|
||||
const negativeEdgeMatch = normalized.match(/^negative_model_edge_([+\-]?[\d.]+)$/);
|
||||
const negativeEdgeMatch = normalized.match(
|
||||
/^negative_model_edge_([+\-]?[\d.]+)$/,
|
||||
);
|
||||
if (negativeEdgeMatch) {
|
||||
return `Model avantajı negatif (${negativeEdgeMatch[1]})`;
|
||||
}
|
||||
@@ -692,47 +697,44 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
private classifySignalTier(
|
||||
record: Record<string, unknown>,
|
||||
interval: {
|
||||
band?: 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
band?: "HIGH" | "MEDIUM" | "LOW";
|
||||
threshold_met?: boolean;
|
||||
},
|
||||
): 'CORE' | 'VALUE' | 'LEAN' | 'LONGSHOT' | 'PASS' {
|
||||
const playable = Boolean(record.playable) && Boolean(interval.threshold_met);
|
||||
): "CORE" | "VALUE" | "LEAN" | "LONGSHOT" | "PASS" {
|
||||
const playable =
|
||||
Boolean(record.playable) && Boolean(interval.threshold_met);
|
||||
const calibratedConfidence = this.asNumber(record.calibrated_confidence);
|
||||
const odds = this.asNumber(record.odds);
|
||||
const evEdge = this.asNumber(record.ev_edge) || this.asNumber(record.edge);
|
||||
const playScore = this.asNumber(record.play_score);
|
||||
const band = String(interval.band ?? 'LOW').toUpperCase();
|
||||
const band = String(interval.band ?? "LOW").toUpperCase();
|
||||
|
||||
if (
|
||||
playable &&
|
||||
band === 'HIGH' &&
|
||||
band === "HIGH" &&
|
||||
calibratedConfidence >= 72 &&
|
||||
evEdge >= 0.02 &&
|
||||
playScore >= 68
|
||||
) {
|
||||
return 'CORE';
|
||||
return "CORE";
|
||||
}
|
||||
|
||||
if (
|
||||
calibratedConfidence >= 52 &&
|
||||
odds >= 1.75 &&
|
||||
evEdge >= 0.04
|
||||
) {
|
||||
return playable ? 'VALUE' : 'LONGSHOT';
|
||||
if (calibratedConfidence >= 52 && odds >= 1.75 && evEdge >= 0.04) {
|
||||
return playable ? "VALUE" : "LONGSHOT";
|
||||
}
|
||||
|
||||
if (
|
||||
calibratedConfidence >= 46 &&
|
||||
(band === 'HIGH' || band === 'MEDIUM' || evEdge > 0)
|
||||
(band === "HIGH" || band === "MEDIUM" || evEdge > 0)
|
||||
) {
|
||||
return 'LEAN';
|
||||
return "LEAN";
|
||||
}
|
||||
|
||||
if (odds >= 2.2 && calibratedConfidence >= 38) {
|
||||
return 'LONGSHOT';
|
||||
return "LONGSHOT";
|
||||
}
|
||||
|
||||
return 'PASS';
|
||||
return "PASS";
|
||||
}
|
||||
|
||||
private estimateConfidenceInterval(input: {
|
||||
@@ -755,7 +757,9 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
const secondProb = sortedProbs[1] ?? 0;
|
||||
const topProb = sortedProbs[0] ?? probability;
|
||||
const margin = Math.max(0, topProb - secondProb);
|
||||
const normalizedConfidence = this.normalizePercent(input.calibratedConfidence);
|
||||
const normalizedConfidence = this.normalizePercent(
|
||||
input.calibratedConfidence,
|
||||
);
|
||||
|
||||
const baseWidthByMarket: Record<string, number> = {
|
||||
MS: 0.18,
|
||||
@@ -767,19 +771,19 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
};
|
||||
const baseWidth = baseWidthByMarket[input.market] ?? 0.19;
|
||||
const lineupPenalty =
|
||||
input.lineupSource === 'confirmed_live'
|
||||
input.lineupSource === "confirmed_live"
|
||||
? -0.015
|
||||
: input.lineupSource === 'probable_xi'
|
||||
: input.lineupSource === "probable_xi"
|
||||
? 0
|
||||
: 0.02;
|
||||
const width = this.clamp(
|
||||
baseWidth
|
||||
- margin * 0.22
|
||||
- normalizedConfidence * 0.05
|
||||
+ (1 - input.dataQualityScore) * 0.09
|
||||
+ input.riskScore * 0.08
|
||||
- (input.isTopLeague ? 0.012 : 0)
|
||||
+ lineupPenalty,
|
||||
baseWidth -
|
||||
margin * 0.22 -
|
||||
normalizedConfidence * 0.05 +
|
||||
(1 - input.dataQualityScore) * 0.09 +
|
||||
input.riskScore * 0.08 -
|
||||
(input.isTopLeague ? 0.012 : 0) +
|
||||
lineupPenalty,
|
||||
0.08,
|
||||
0.34,
|
||||
);
|
||||
@@ -795,17 +799,17 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
width <= this.getMaxAllowedWidth(input.market) &&
|
||||
input.dataQualityScore >= 0.58 &&
|
||||
input.evEdge >= this.getMinEdge(input.market) &&
|
||||
(upper - input.impliedProb) >= 0.03;
|
||||
upper - input.impliedProb >= 0.03;
|
||||
|
||||
let band: ConfidenceBand = 'LOW';
|
||||
let band: ConfidenceBand = "LOW";
|
||||
if (input.calibratedConfidence >= 69 && width <= 0.12 && margin >= 0.07) {
|
||||
band = 'HIGH';
|
||||
band = "HIGH";
|
||||
} else if (
|
||||
input.calibratedConfidence >= 58 &&
|
||||
width <= 0.18 &&
|
||||
margin >= 0.035
|
||||
) {
|
||||
band = 'MEDIUM';
|
||||
band = "MEDIUM";
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -864,7 +868,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
): Record<string, unknown> {
|
||||
const entry = this.asRecord(marketBoard[market]);
|
||||
const probs = entry.probs;
|
||||
return probs && typeof probs === 'object'
|
||||
return probs && typeof probs === "object"
|
||||
? (probs as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
@@ -895,7 +899,9 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private normalizeScore(value: unknown): number {
|
||||
const numeric = this.asNumber(value);
|
||||
return numeric > 1 ? this.clamp(numeric / 100, 0, 1) : this.clamp(numeric, 0, 1);
|
||||
return numeric > 1
|
||||
? this.clamp(numeric / 100, 0, 1)
|
||||
: this.clamp(numeric, 0, 1);
|
||||
}
|
||||
|
||||
private normalizePercent(value: number): number {
|
||||
@@ -903,15 +909,15 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
private asRecord(value: unknown): Record<string, any> {
|
||||
return value && typeof value === 'object'
|
||||
return value && typeof value === "object"
|
||||
? (value as Record<string, any>)
|
||||
: {};
|
||||
}
|
||||
|
||||
private asNumber(value: unknown): number {
|
||||
return typeof value === 'number'
|
||||
return typeof value === "number"
|
||||
? value
|
||||
: typeof value === 'string'
|
||||
: typeof value === "string"
|
||||
? Number(value) || 0
|
||||
: 0;
|
||||
}
|
||||
@@ -922,7 +928,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async getValueBets(): Promise<ValueBetDto[]> {
|
||||
const predictions = await this.prisma.prediction.findMany({
|
||||
where: { match: { status: 'NS' } },
|
||||
where: { match: { status: "NS" } },
|
||||
include: { match: { include: { homeTeam: true, awayTeam: true } } },
|
||||
});
|
||||
|
||||
@@ -937,14 +943,14 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
valueBets.push({
|
||||
matchId: p.matchId,
|
||||
matchName: `${p.match.homeTeam?.name} vs ${p.match.awayTeam?.name}`,
|
||||
betType: (vb.market || vb.betType || '') as string,
|
||||
prediction: (vb.pick || vb.prediction || '') as string,
|
||||
confidence: typeof vb.confidence === 'number' ? vb.confidence : 0,
|
||||
odd: typeof vb.odd === 'number' ? vb.odd : 0,
|
||||
betType: (vb.market || vb.betType || "") as string,
|
||||
prediction: (vb.pick || vb.prediction || "") as string,
|
||||
confidence: typeof vb.confidence === "number" ? vb.confidence : 0,
|
||||
odd: typeof vb.odd === "number" ? vb.odd : 0,
|
||||
expectedValue:
|
||||
typeof vb.edge === 'number'
|
||||
typeof vb.edge === "number"
|
||||
? vb.edge
|
||||
: typeof vb.expectedValue === 'number'
|
||||
: typeof vb.expectedValue === "number"
|
||||
? vb.expectedValue
|
||||
: 0,
|
||||
});
|
||||
@@ -959,7 +965,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async getSmartCoupon(
|
||||
matchIds: string[],
|
||||
strategy: string = 'BALANCED',
|
||||
strategy: string = "BALANCED",
|
||||
options: { maxMatches?: number; minConfidence?: number } = {},
|
||||
): Promise<any> {
|
||||
await this.ensureSmartCouponDataReady(matchIds);
|
||||
@@ -997,23 +1003,23 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private throwAiError(message: string): never {
|
||||
if (
|
||||
message.includes('timed out') ||
|
||||
message.includes('AI_ENGINE_TIMEOUT') ||
|
||||
message.includes('AI_ENGINE_504')
|
||||
message.includes("timed out") ||
|
||||
message.includes("AI_ENGINE_TIMEOUT") ||
|
||||
message.includes("AI_ENGINE_504")
|
||||
) {
|
||||
throw new HttpException(
|
||||
'Prediction request timed out',
|
||||
"Prediction request timed out",
|
||||
HttpStatus.GATEWAY_TIMEOUT,
|
||||
);
|
||||
}
|
||||
if (message.includes('AI_ENGINE_502')) {
|
||||
if (message.includes("AI_ENGINE_502")) {
|
||||
throw new HttpException(
|
||||
'AI Engine upstream returned 502',
|
||||
"AI Engine upstream returned 502",
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
}
|
||||
throw new HttpException(
|
||||
'Failed to get prediction from AI Engine',
|
||||
"Failed to get prediction from AI Engine",
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
@@ -1066,12 +1072,12 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
const cached = prediction.predictionJson as Record<string, unknown>;
|
||||
const modelVersion = cached['model_version'];
|
||||
if (typeof modelVersion !== 'string') {
|
||||
const modelVersion = cached["model_version"];
|
||||
if (typeof modelVersion !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!modelVersion.startsWith('v25')) {
|
||||
if (!modelVersion.startsWith("v25")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1082,7 +1088,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
const uniqueMatchIds = [...new Set(matchIds.filter((id) => !!id))];
|
||||
if (uniqueMatchIds.length === 0) {
|
||||
throw new HttpException(
|
||||
'No matchIds provided for smart coupon generation',
|
||||
"No matchIds provided for smart coupon generation",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -1122,7 +1128,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
const hasLiveOdds =
|
||||
!!liveMatch?.odds &&
|
||||
typeof liveMatch.odds === 'object' &&
|
||||
typeof liveMatch.odds === "object" &&
|
||||
!Array.isArray(liveMatch.odds) &&
|
||||
Object.keys(liveMatch.odds as Record<string, unknown>).length > 0;
|
||||
const matchExists = !!liveMatch?.id || !!persistedMatch?.id;
|
||||
@@ -1146,9 +1152,9 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
const isFinished =
|
||||
hasScores ||
|
||||
state === 'MS' ||
|
||||
state === 'postGame' ||
|
||||
['Finished', 'Played', 'FT', 'AET', 'PEN', 'Ended'].includes(
|
||||
state === "MS" ||
|
||||
state === "postGame" ||
|
||||
["Finished", "Played", "FT", "AET", "PEN", "Ended"].includes(
|
||||
status as string,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import axios from 'axios';
|
||||
import { PredictionJobType } from './predictions.types';
|
||||
import { PredictionsProcessor } from './predictions.processor';
|
||||
import axios from "axios";
|
||||
import { PredictionJobType } from "./predictions.types";
|
||||
import { PredictionsProcessor } from "./predictions.processor";
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock("axios");
|
||||
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
describe('PredictionsProcessor', () => {
|
||||
describe("PredictionsProcessor", () => {
|
||||
let processor: PredictionsProcessor;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env.AI_ENGINE_URL = 'http://unit-ai:8000';
|
||||
process.env.AI_ENGINE_URL = "http://unit-ai:8000";
|
||||
processor = new PredictionsProcessor();
|
||||
});
|
||||
|
||||
@@ -20,34 +20,34 @@ describe('PredictionsProcessor', () => {
|
||||
delete process.env.AI_ENGINE_URL;
|
||||
});
|
||||
|
||||
it('posts to analyze endpoint for predict-match jobs', async () => {
|
||||
it("posts to analyze endpoint for predict-match jobs", async () => {
|
||||
mockedAxios.post.mockResolvedValueOnce({ data: { ok: true } } as any);
|
||||
|
||||
const job = {
|
||||
id: 'j1',
|
||||
id: "j1",
|
||||
name: PredictionJobType.PREDICT_MATCH,
|
||||
data: { matchId: 'match-123' },
|
||||
data: { matchId: "match-123" },
|
||||
} as any;
|
||||
|
||||
const result = await processor.process(job);
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'http://unit-ai:8000/v20plus/analyze/match-123',
|
||||
"http://unit-ai:8000/v20plus/analyze/match-123",
|
||||
{},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('posts mapped payload to coupon endpoint for smart-coupon jobs', async () => {
|
||||
it("posts mapped payload to coupon endpoint for smart-coupon jobs", async () => {
|
||||
mockedAxios.post.mockResolvedValueOnce({ data: { bets: [] } } as any);
|
||||
|
||||
const job = {
|
||||
id: 'j2',
|
||||
id: "j2",
|
||||
name: PredictionJobType.SMART_COUPON,
|
||||
data: {
|
||||
matchIds: ['m1', 'm2'],
|
||||
strategy: 'BALANCED',
|
||||
matchIds: ["m1", "m2"],
|
||||
strategy: "BALANCED",
|
||||
options: { maxMatches: 4, minConfidence: 65 },
|
||||
},
|
||||
} as any;
|
||||
@@ -56,10 +56,10 @@ describe('PredictionsProcessor', () => {
|
||||
|
||||
expect(result).toEqual({ bets: [] });
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'http://unit-ai:8000/v20plus/coupon',
|
||||
"http://unit-ai:8000/v20plus/coupon",
|
||||
{
|
||||
match_ids: ['m1', 'm2'],
|
||||
strategy: 'BALANCED',
|
||||
match_ids: ["m1", "m2"],
|
||||
strategy: "BALANCED",
|
||||
max_matches: 4,
|
||||
min_confidence: 65,
|
||||
},
|
||||
@@ -67,15 +67,15 @@ describe('PredictionsProcessor', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('throws for unknown job type', async () => {
|
||||
it("throws for unknown job type", async () => {
|
||||
const job = {
|
||||
id: 'j3',
|
||||
name: 'unknown-job',
|
||||
id: "j3",
|
||||
name: "unknown-job",
|
||||
data: {},
|
||||
} as any;
|
||||
|
||||
await expect(processor.process(job)).rejects.toThrow(
|
||||
'Unknown job type: unknown-job',
|
||||
"Unknown job type: unknown-job",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { Processor, WorkerHost } from "@nestjs/bullmq";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Job } from "bullmq";
|
||||
import {
|
||||
PREDICTIONS_QUEUE,
|
||||
PredictionJobType,
|
||||
PredictMatchJobData,
|
||||
SmartCouponJobData,
|
||||
} from './predictions.types';
|
||||
import axios from 'axios';
|
||||
} from "./predictions.types";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* Predictions Processor
|
||||
@@ -22,7 +22,7 @@ export class PredictionsProcessor extends WorkerHost {
|
||||
constructor() {
|
||||
super();
|
||||
// Default to container service URL
|
||||
this.aiEngineUrl = process.env.AI_ENGINE_URL || 'http://ai-engine:8000';
|
||||
this.aiEngineUrl = process.env.AI_ENGINE_URL || "http://ai-engine:8000";
|
||||
}
|
||||
|
||||
async process(job: Job<any, any, string>): Promise<any> {
|
||||
@@ -56,7 +56,7 @@ export class PredictionsProcessor extends WorkerHost {
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw this.mapAxiosError(error, matchId, 'predict');
|
||||
throw this.mapAxiosError(error, matchId, "predict");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,14 +81,14 @@ export class PredictionsProcessor extends WorkerHost {
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw this.mapAxiosError(error, matchIds.join(','), 'smart-coupon');
|
||||
throw this.mapAxiosError(error, matchIds.join(","), "smart-coupon");
|
||||
}
|
||||
}
|
||||
|
||||
private mapAxiosError(
|
||||
error: unknown,
|
||||
identifier: string,
|
||||
flow: 'predict' | 'smart-coupon',
|
||||
flow: "predict" | "smart-coupon",
|
||||
): Error {
|
||||
if (!axios.isAxiosError(error)) {
|
||||
return error instanceof Error
|
||||
@@ -98,7 +98,7 @@ export class PredictionsProcessor extends WorkerHost {
|
||||
|
||||
const status = error.response?.status;
|
||||
const detail = error.response?.data?.detail || error.message;
|
||||
const code = error.code || '';
|
||||
const code = error.code || "";
|
||||
|
||||
if (status === 502) {
|
||||
this.logger.error(`AI Engine 502 (${flow}:${identifier}): ${detail}`);
|
||||
@@ -110,13 +110,13 @@ export class PredictionsProcessor extends WorkerHost {
|
||||
return new Error(`AI_ENGINE_504|${flow}|${detail}`);
|
||||
}
|
||||
|
||||
if (code === 'ECONNABORTED' || code === 'ETIMEDOUT') {
|
||||
if (code === "ECONNABORTED" || code === "ETIMEDOUT") {
|
||||
this.logger.error(`AI Engine timeout (${flow}:${identifier}): ${detail}`);
|
||||
return new Error(`AI_ENGINE_TIMEOUT|${flow}|${detail}`);
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
`AI Engine error (${flow}:${identifier}) [${status ?? 'N/A'}]: ${detail}`,
|
||||
`AI Engine error (${flow}:${identifier}) [${status ?? "N/A"}]: ${detail}`,
|
||||
);
|
||||
return new Error(`AI_ENGINE_ERROR|${flow}|${detail}`);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bullmq';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { InjectQueue } from "@nestjs/bullmq";
|
||||
import { Queue } from "bullmq";
|
||||
import {
|
||||
PREDICTIONS_QUEUE,
|
||||
PredictionJobType,
|
||||
PredictMatchJobData,
|
||||
SmartCouponJobData,
|
||||
} from './predictions.types';
|
||||
} from "./predictions.types";
|
||||
|
||||
@Injectable()
|
||||
export class PredictionsQueue {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* Senior Level Strict Typing
|
||||
*/
|
||||
|
||||
export const PREDICTIONS_QUEUE = 'predictions-queue';
|
||||
export const PREDICTIONS_QUEUE = "predictions-queue";
|
||||
|
||||
export enum PredictionJobType {
|
||||
PREDICT_MATCH = 'predict-match',
|
||||
SMART_COUPON = 'smart-coupon',
|
||||
PREDICT_MATCH = "predict-match",
|
||||
SMART_COUPON = "smart-coupon",
|
||||
}
|
||||
|
||||
export interface PredictMatchJobData {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../database/prisma.service';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../database/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class AiFeatureStoreService {
|
||||
@@ -16,10 +16,10 @@ export class AiFeatureStoreService {
|
||||
where: { id: matchId },
|
||||
include: {
|
||||
homeTeam: {
|
||||
include: { homeMatches: { take: 5, orderBy: { mstUtc: 'desc' } } },
|
||||
include: { homeMatches: { take: 5, orderBy: { mstUtc: "desc" } } },
|
||||
},
|
||||
awayTeam: {
|
||||
include: { awayMatches: { take: 5, orderBy: { mstUtc: 'desc' } } },
|
||||
include: { awayMatches: { take: 5, orderBy: { mstUtc: "desc" } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user