gg
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user