@@ -399,6 +399,12 @@ export class DataFetcherTask {
|
||||
const closingOddsSnapshot = await this.getClosingOddsSnapshot(
|
||||
row.matchId,
|
||||
);
|
||||
// ── Per-market settlement (V31e forward-test) ────────────────
|
||||
// Score EVERY captured market (not just main_pick) against reality,
|
||||
// so the admin Model Performance page can compute per-market
|
||||
// calibration (model% → actual%) and ROI. won=null → push (skip).
|
||||
const marketsSettled = this.settleAllMarkets(row);
|
||||
|
||||
const settlementSummary = {
|
||||
settled_at: new Date().toISOString(),
|
||||
model_version: row.engineVersion,
|
||||
@@ -413,6 +419,7 @@ export class DataFetcherTask {
|
||||
away: row.htScoreAway,
|
||||
},
|
||||
closing_odds_snapshot: closingOddsSnapshot,
|
||||
markets_settled: marketsSettled,
|
||||
};
|
||||
|
||||
await this.prisma.$executeRawUnsafe(
|
||||
@@ -538,6 +545,64 @@ export class DataFetcherTask {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V31e forward-test: settle EVERY captured market (payload_summary.markets_full)
|
||||
* against the final score. Produces one compact record per market with its
|
||||
* shown probability, the real outcome, and flat profit — the raw material for
|
||||
* per-market calibration (model% vs actual%) on the admin dashboard.
|
||||
*/
|
||||
private settleAllMarkets(
|
||||
row: PendingPredictionRunForSettlement,
|
||||
): Array<Record<string, unknown>> {
|
||||
const summary = this.asRecord(row.payloadSummary);
|
||||
const markets = Array.isArray(summary.markets_full)
|
||||
? (summary.markets_full as unknown[])
|
||||
: [];
|
||||
const out: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (const raw of markets) {
|
||||
const m = this.asRecord(raw);
|
||||
const market = typeof m.market === "string" ? m.market : "";
|
||||
const pick = typeof m.pick === "string" ? m.pick : "";
|
||||
if (!market || !pick) continue;
|
||||
|
||||
const won = this.isPredictionPickWon({
|
||||
market,
|
||||
pick,
|
||||
scoreHome: row.scoreHome,
|
||||
scoreAway: row.scoreAway,
|
||||
htScoreHome: row.htScoreHome,
|
||||
htScoreAway: row.htScoreAway,
|
||||
});
|
||||
if (won === null) continue; // push / unresolvable → exclude from stats
|
||||
|
||||
const odds = Number(m.odds || 0);
|
||||
const hasOdds = Number.isFinite(odds) && odds > 1.01;
|
||||
out.push({
|
||||
market,
|
||||
pick,
|
||||
won,
|
||||
// shown probability for calibration (0–100). Prefer calibrated_confidence.
|
||||
shown_confidence:
|
||||
m.calibrated_confidence != null
|
||||
? Number(m.calibrated_confidence)
|
||||
: m.model_probability != null
|
||||
? Number(m.model_probability) * 100
|
||||
: null,
|
||||
model_probability:
|
||||
m.model_probability != null ? Number(m.model_probability) : null,
|
||||
odds: hasOdds ? odds : null,
|
||||
playable: m.playable === true,
|
||||
bet_grade: typeof m.bet_grade === "string" ? m.bet_grade : null,
|
||||
action: typeof m.action === "string" ? m.action : null,
|
||||
value_tier: typeof m.value_tier === "string" ? m.value_tier : null,
|
||||
// flat 1u profit if a real price existed (for per-market ROI)
|
||||
flat_profit: hasOdds ? Number((won ? odds - 1 : -1).toFixed(4)) : null,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private isPredictionPickWon(input: {
|
||||
market: string;
|
||||
pick: string;
|
||||
|
||||
Reference in New Issue
Block a user