This commit is contained in:
2026-04-21 16:53:56 +03:00
parent 1346924387
commit 2ccd6831eb
26 changed files with 430403 additions and 3 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import { Controller, Get, Res } from "@nestjs/common";
import { ApiTags, ApiOperation } from "@nestjs/swagger";
import { Response } from "express";
import type { Response } from "express";
import { Public } from "../../common/decorators";
import { PrismaService } from "../../database/prisma.service";
import { PredictionsService } from "../predictions/predictions.service";
+27
View File
@@ -115,6 +115,9 @@ export class MatchPickDto {
@ApiProperty()
market: string;
@ApiProperty({ required: false, default: "standard" })
strategy_channel?: string;
@ApiProperty()
pick: string;
@@ -350,6 +353,15 @@ export class MatchPredictionDto {
@ApiProperty()
model_version: string;
@ApiProperty({ required: false, nullable: true })
calibration_version?: string | null;
@ApiProperty({ required: false, nullable: true })
shadow_engine_version?: string | null;
@ApiProperty({ required: false, nullable: true })
decision_trace_id?: string | null;
@ApiProperty({ type: MatchInfoDto })
match_info: MatchInfoDto;
@@ -368,6 +380,9 @@ export class MatchPredictionDto {
@ApiProperty({ type: MatchPickDto, nullable: true })
value_pick: MatchPickDto | null;
@ApiProperty({ type: MatchPickDto, nullable: true, required: false })
surprise_pick?: MatchPickDto | null;
@ApiProperty({ type: MatchBetAdviceDto })
bet_advice: MatchBetAdviceDto;
@@ -394,6 +409,15 @@ export class MatchPredictionDto {
@ApiProperty({ type: [String] })
reasoning_factors: string[];
@ApiProperty({ type: Object, required: false })
market_reliability?: Record<string, number>;
@ApiProperty({ type: Object, required: false })
shadow_engine?: Record<string, unknown>;
@ApiProperty({ type: Object, required: false })
surprise_hunter?: Record<string, unknown>;
}
export class ValueBetDto {
@@ -476,6 +500,9 @@ export class AIHealthDto {
@ApiProperty({ required: false, nullable: true })
detail?: string | null;
@ApiProperty({ required: false, nullable: true })
mode?: string | null;
}
export * from "./smart-coupon.dto";
@@ -183,6 +183,10 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
circuitState: circuit.state,
consecutiveFailures: circuit.consecutiveFailures,
endpoint: this.aiEngineUrl,
mode:
typeof (response.data as Record<string, unknown>)?.mode === "string"
? String((response.data as Record<string, unknown>).mode)
: this.configService.get("AI_ENGINE_MODE", "v25"),
};
} catch (error: unknown) {
const requestError =
@@ -203,6 +207,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
typeof requestError.detail === "string"
? requestError.detail
: requestError.message,
mode: this.configService.get("AI_ENGINE_MODE", "v25"),
};
}
}
@@ -219,6 +224,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
if (!data || data.error) {
return null;
}
await this.recordPredictionRun(matchId, data as MatchPredictionDto);
return this.enrichPredictionResponse(
data as MatchPredictionDto,
matchContext,
@@ -236,6 +242,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
`/v20plus/analyze/${matchId}`,
{},
);
await this.recordPredictionRun(matchId, response.data);
return this.enrichPredictionResponse(
response.data as MatchPredictionDto,
matchContext,
@@ -1228,4 +1235,124 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
private async recordPredictionRun(
matchId: string,
payload: MatchPredictionDto,
): Promise<void> {
try {
const oddsSnapshot = await this.getPredictionOddsSnapshot(matchId);
const payloadSummary = this.buildPredictionPayloadSummary(payload);
await this.prisma.$executeRawUnsafe(
`
INSERT INTO prediction_runs (
match_id,
engine_version,
decision_trace_id,
odds_snapshot,
payload_summary
)
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb)
`,
matchId,
String(payload.model_version || "unknown"),
typeof payload.decision_trace_id === "string"
? payload.decision_trace_id
: null,
JSON.stringify(oddsSnapshot),
JSON.stringify(payloadSummary),
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.warn(`Prediction run audit skipped for ${matchId}: ${message}`);
}
}
private async getPredictionOddsSnapshot(
matchId: string,
): Promise<Record<string, unknown>> {
const liveMatch = await this.prisma.liveMatch.findUnique({
where: { id: matchId },
select: {
odds: true,
oddsUpdatedAt: true,
state: true,
status: true,
scoreHome: true,
scoreAway: true,
},
});
if (liveMatch) {
return {
source: "live_match",
odds: liveMatch.odds ?? {},
odds_updated_at: liveMatch.oddsUpdatedAt?.toISOString() ?? null,
state: liveMatch.state ?? null,
status: liveMatch.status ?? null,
score_home: liveMatch.scoreHome ?? null,
score_away: liveMatch.scoreAway ?? null,
};
}
const oddCategoryCount = await this.prisma.oddCategory.count({
where: { matchId },
});
return {
source: "historical_match",
odd_category_count: oddCategoryCount,
};
}
private buildPredictionPayloadSummary(
payload: MatchPredictionDto,
): Record<string, unknown> {
const topSummary = Array.isArray(payload.bet_summary)
? payload.bet_summary.slice(0, 5).map((item) => ({
market: item.market,
pick: item.pick,
playable: item.playable,
bet_grade: item.bet_grade,
calibrated_confidence: item.calibrated_confidence,
ev_edge: item.ev_edge ?? 0,
stake_units: item.stake_units,
}))
: [];
return {
model_version: payload.model_version,
calibration_version: payload.calibration_version ?? null,
shadow_engine_version: payload.shadow_engine_version ?? null,
decision_trace_id: payload.decision_trace_id ?? null,
main_pick: payload.main_pick
? {
market: payload.main_pick.market,
pick: payload.main_pick.pick,
playable: payload.main_pick.playable,
bet_grade: payload.main_pick.bet_grade,
calibrated_confidence: payload.main_pick.calibrated_confidence,
ev_edge: payload.main_pick.ev_edge ?? 0,
stake_units: payload.main_pick.stake_units,
}
: null,
value_pick: payload.value_pick
? {
market: payload.value_pick.market,
pick: payload.value_pick.pick,
playable: payload.value_pick.playable,
bet_grade: payload.value_pick.bet_grade,
calibrated_confidence: payload.value_pick.calibrated_confidence,
ev_edge: payload.value_pick.ev_edge ?? 0,
}
: null,
bet_advice: {
playable: payload.bet_advice?.playable ?? false,
suggested_stake_units:
payload.bet_advice?.suggested_stake_units ?? 0,
reason: payload.bet_advice?.reason ?? null,
},
top_summary: topSummary,
market_reliability: payload.market_reliability ?? {},
shadow_engine: payload.shadow_engine ?? null,
};
}
}