This commit is contained in:
2026-04-16 17:21:48 +03:00
parent c8fa4c442d
commit c8e7e4e927
116 changed files with 3720 additions and 4197 deletions
+76 -76
View File
@@ -9,8 +9,8 @@
* Usage: npx ts-node --transpile-only -r tsconfig-paths/register src/scripts/backtest-accuracy.ts
*/
import { PrismaClient } from '@prisma/client';
import axios from 'axios';
import { PrismaClient } from "@prisma/client";
import axios from "axios";
const prisma = new PrismaClient();
@@ -18,7 +18,7 @@ const prisma = new PrismaClient();
// Configuration
// ═══════════════════════════════════════════════════════
const AI_ENGINE_URL = process.env.AI_ENGINE_URL || 'http://127.0.0.1:3005';
const AI_ENGINE_URL = process.env.AI_ENGINE_URL || "http://127.0.0.1:3005";
const CONCURRENT_REQUESTS = 5;
const MAX_MATCHES = 1000;
@@ -60,14 +60,14 @@ function determineActualOutcome(
htScoreHome: number | null,
htScoreAway: number | null,
): { ms: string; ou25: string; btts: string; htft: string } {
const ms = scoreHome > scoreAway ? '1' : scoreHome < scoreAway ? '2' : 'X';
const ou25 = scoreHome + scoreAway > 2.5 ? 'Over' : 'Under';
const btts = scoreHome > 0 && scoreAway > 0 ? 'Yes' : 'No';
const ms = scoreHome > scoreAway ? "1" : scoreHome < scoreAway ? "2" : "X";
const ou25 = scoreHome + scoreAway > 2.5 ? "Over" : "Under";
const btts = scoreHome > 0 && scoreAway > 0 ? "Yes" : "No";
let htft = 'unknown';
let htft = "unknown";
if (htScoreHome !== null && htScoreAway !== null) {
const htResult =
htScoreHome > htScoreAway ? '1' : htScoreHome < htScoreAway ? '2' : 'X';
htScoreHome > htScoreAway ? "1" : htScoreHome < htScoreAway ? "2" : "X";
htft = `${htResult}/${ms}`;
}
@@ -78,7 +78,7 @@ function extractPrediction(response: unknown): {
ms: string;
ou25: string;
btts: string;
probs: BacktestResult['probabilities'];
probs: BacktestResult["probabilities"];
mainPick: string;
mainMarket: string;
} {
@@ -87,9 +87,9 @@ function extractPrediction(response: unknown): {
const mainPickObj = data?.main_pick as Record<string, unknown> | undefined;
const mainPick =
typeof mainPickObj?.pick === 'string' ? mainPickObj.pick : '';
typeof mainPickObj?.pick === "string" ? mainPickObj.pick : "";
const mainMarket =
typeof mainPickObj?.market === 'string' ? mainPickObj.market : '';
typeof mainPickObj?.market === "string" ? mainPickObj.market : "";
// Extract MS from probabilities or main pick
const msProbs = (predictions?.ms || data?.ms || {}) as Record<
@@ -97,27 +97,27 @@ function extractPrediction(response: unknown): {
unknown
>;
const homeProb =
typeof msProbs['1'] === 'number'
? msProbs['1']
: typeof msProbs.home_prob === 'number'
typeof msProbs["1"] === "number"
? msProbs["1"]
: typeof msProbs.home_prob === "number"
? msProbs.home_prob
: 0;
const drawProb =
typeof msProbs['X'] === 'number'
? msProbs['X']
: typeof msProbs.draw_prob === 'number'
typeof msProbs["X"] === "number"
? msProbs["X"]
: typeof msProbs.draw_prob === "number"
? msProbs.draw_prob
: 0;
const awayProb =
typeof msProbs['2'] === 'number'
? msProbs['2']
: typeof msProbs.away_prob === 'number'
typeof msProbs["2"] === "number"
? msProbs["2"]
: typeof msProbs.away_prob === "number"
? msProbs.away_prob
: 0;
let ms = '1';
if (drawProb > homeProb && drawProb > awayProb) ms = 'X';
else if (awayProb > homeProb) ms = '2';
let ms = "1";
if (drawProb > homeProb && drawProb > awayProb) ms = "X";
else if (awayProb > homeProb) ms = "2";
// Extract OU25
const ou25Probs = (predictions?.ou25 || data?.ou25 || {}) as Record<
@@ -125,18 +125,18 @@ function extractPrediction(response: unknown): {
unknown
>;
const overProb =
typeof ou25Probs.Over === 'number'
typeof ou25Probs.Over === "number"
? ou25Probs.Over
: typeof ou25Probs.over_prob === 'number'
: typeof ou25Probs.over_prob === "number"
? ou25Probs.over_prob
: 0;
const underProb =
typeof ou25Probs.Under === 'number'
typeof ou25Probs.Under === "number"
? ou25Probs.Under
: typeof ou25Probs.under_prob === 'number'
: typeof ou25Probs.under_prob === "number"
? ou25Probs.under_prob
: 0;
const ou25 = overProb > underProb ? 'Over' : 'Under';
const ou25 = overProb > underProb ? "Over" : "Under";
// Extract BTTS
const bttsProbs = (predictions?.btts || data?.btts || {}) as Record<
@@ -144,18 +144,18 @@ function extractPrediction(response: unknown): {
unknown
>;
const bttsYes =
typeof bttsProbs.Yes === 'number'
typeof bttsProbs.Yes === "number"
? bttsProbs.Yes
: typeof bttsProbs.yes_prob === 'number'
: typeof bttsProbs.yes_prob === "number"
? bttsProbs.yes_prob
: 0;
const bttsNo =
typeof bttsProbs.No === 'number'
typeof bttsProbs.No === "number"
? bttsProbs.No
: typeof bttsProbs.no_prob === 'number'
: typeof bttsProbs.no_prob === "number"
? bttsProbs.no_prob
: 0;
const btts = bttsYes > bttsNo ? 'Yes' : 'No';
const btts = bttsYes > bttsNo ? "Yes" : "No";
return {
ms,
@@ -197,11 +197,11 @@ async function processBatch(batch: TestMatch[]): Promise<BacktestResult[]> {
// Check main pick
let mainPickCorrect = false;
if (pred.mainMarket === 'MS') {
if (pred.mainMarket === "MS") {
mainPickCorrect = pred.mainPick === actual.ms;
} else if (pred.mainMarket === 'OU25') {
} else if (pred.mainMarket === "OU25") {
mainPickCorrect = pred.mainPick === actual.ou25;
} else if (pred.mainMarket === 'BTTS') {
} else if (pred.mainMarket === "BTTS") {
mainPickCorrect = pred.mainPick === actual.btts;
}
@@ -226,8 +226,8 @@ async function processBatch(batch: TestMatch[]): Promise<BacktestResult[]> {
// ═══════════════════════════════════════════════════════
async function runBacktest(): Promise<void> {
console.log('🎯 BACKTEST ACCURACY — V30 Betting Engine');
console.log('════════════════════════════════════════════════════════');
console.log("🎯 BACKTEST ACCURACY — V30 Betting Engine");
console.log("════════════════════════════════════════════════════════");
// 1. Health check
try {
@@ -236,12 +236,12 @@ async function runBacktest(): Promise<void> {
});
console.log(`✅ AI Engine: ${JSON.stringify(health.data)}`);
} catch {
console.error('❌ AI Engine not reachable at', AI_ENGINE_URL);
console.error("❌ AI Engine not reachable at", AI_ENGINE_URL);
process.exit(1);
}
// 2. Load finished matches with features
console.log('\n📥 Loading test matches...');
console.log("\n📥 Loading test matches...");
const matches = await prisma.$queryRaw<TestMatch[]>`
SELECT m.id, m.score_home AS "scoreHome", m.score_away AS "scoreAway",
m.ht_score_home AS "htScoreHome", m.ht_score_away AS "htScoreAway"
@@ -259,7 +259,7 @@ async function runBacktest(): Promise<void> {
console.log(` 📊 Test matches: ${matches.length}`);
// 3. Run predictions in batches
console.log('\n🤖 Running predictions...');
console.log("\n🤖 Running predictions...");
const allResults: BacktestResult[] = [];
let processed = 0;
@@ -277,7 +277,7 @@ async function runBacktest(): Promise<void> {
allResults.length) *
100
).toFixed(1)
: '0';
: "0";
console.log(
` 📊 ${processed}/${matches.length} — Success: ${allResults.length} — MS Acc: ${currentMsAcc}%`,
);
@@ -287,7 +287,7 @@ async function runBacktest(): Promise<void> {
// 4. Calculate metrics
const total = allResults.length;
if (total === 0) {
console.error('❌ No results to analyze');
console.error("❌ No results to analyze");
process.exit(1);
}
@@ -303,22 +303,22 @@ async function runBacktest(): Promise<void> {
const mainPickCorrect = allResults.filter((r) => r.mainPickCorrect).length;
// Actual distribution
const actHome = allResults.filter((r) => r.actual.ms === '1').length;
const actDraw = allResults.filter((r) => r.actual.ms === 'X').length;
const actAway = allResults.filter((r) => r.actual.ms === '2').length;
const actHome = allResults.filter((r) => r.actual.ms === "1").length;
const actDraw = allResults.filter((r) => r.actual.ms === "X").length;
const actAway = allResults.filter((r) => r.actual.ms === "2").length;
// Predicted distribution
const predHome = allResults.filter((r) => r.predicted.ms === '1').length;
const predDraw = allResults.filter((r) => r.predicted.ms === 'X').length;
const predAway = allResults.filter((r) => r.predicted.ms === '2').length;
const predHome = allResults.filter((r) => r.predicted.ms === "1").length;
const predDraw = allResults.filter((r) => r.predicted.ms === "X").length;
const predAway = allResults.filter((r) => r.predicted.ms === "2").length;
// Confidence calibration (based on max probability)
const buckets: Record<string, { correct: number; total: number }> = {
'33-40%': { correct: 0, total: 0 },
'40-50%': { correct: 0, total: 0 },
'50-60%': { correct: 0, total: 0 },
'60-70%': { correct: 0, total: 0 },
'70%+': { correct: 0, total: 0 },
"33-40%": { correct: 0, total: 0 },
"40-50%": { correct: 0, total: 0 },
"50-60%": { correct: 0, total: 0 },
"60-70%": { correct: 0, total: 0 },
"70%+": { correct: 0, total: 0 },
};
for (const r of allResults) {
@@ -329,25 +329,25 @@ async function runBacktest(): Promise<void> {
);
const key =
maxProb >= 0.7
? '70%+'
? "70%+"
: maxProb >= 0.6
? '60-70%'
? "60-70%"
: maxProb >= 0.5
? '50-60%'
? "50-60%"
: maxProb >= 0.4
? '40-50%'
: '33-40%';
? "40-50%"
: "33-40%";
buckets[key].total++;
if (r.predicted.ms === r.actual.ms) buckets[key].correct++;
}
// 5. Print Report
console.log('\n════════════════════════════════════════════════════════');
console.log('📊 BACKTEST ACCURACY REPORT');
console.log('════════════════════════════════════════════════════════');
console.log("\n════════════════════════════════════════════════════════");
console.log("📊 BACKTEST ACCURACY REPORT");
console.log("════════════════════════════════════════════════════════");
console.log(` Total Matches Analyzed: ${total}`);
console.log('');
console.log(' 🎯 Market Accuracy:');
console.log("");
console.log(" 🎯 Market Accuracy:");
console.log(
` ⚽ Match Result (MS): ${((msCorrect / total) * 100).toFixed(2)}% (${msCorrect}/${total})`,
);
@@ -361,7 +361,7 @@ async function runBacktest(): Promise<void> {
` 🏆 Main Pick Success: ${((mainPickCorrect / total) * 100).toFixed(2)}% (${mainPickCorrect}/${total})`,
);
console.log('\n 📊 MS Distribution:');
console.log("\n 📊 MS Distribution:");
console.log(
` Actual: 1: ${actHome} (${((actHome / total) * 100).toFixed(1)}%) | X: ${actDraw} (${((actDraw / total) * 100).toFixed(1)}%) | 2: ${actAway} (${((actAway / total) * 100).toFixed(1)}%)`,
);
@@ -369,21 +369,21 @@ async function runBacktest(): Promise<void> {
` Predicted: 1: ${predHome} (${((predHome / total) * 100).toFixed(1)}%) | X: ${predDraw} (${((predDraw / total) * 100).toFixed(1)}%) | 2: ${predAway} (${((predAway / total) * 100).toFixed(1)}%)`,
);
console.log('\n 📊 Confidence Calibration:');
console.log("\n 📊 Confidence Calibration:");
for (const [range, bucket] of Object.entries(buckets)) {
if (bucket.total === 0) continue;
const acc = (bucket.correct / bucket.total) * 100;
const bar = '█'.repeat(Math.round(acc / 3));
const bar = "█".repeat(Math.round(acc / 3));
console.log(
` ${range.padEnd(8)} : ${acc.toFixed(1)}% acc (n=${bucket.total}) ${bar}`,
);
}
// 6. Per-market deep dive
console.log('\n 📊 OU25 Breakdown:');
const actOver = allResults.filter((r) => r.actual.ou25 === 'Over').length;
console.log("\n 📊 OU25 Breakdown:");
const actOver = allResults.filter((r) => r.actual.ou25 === "Over").length;
const actUnder = total - actOver;
const predOver = allResults.filter((r) => r.predicted.ou25 === 'Over').length;
const predOver = allResults.filter((r) => r.predicted.ou25 === "Over").length;
const predUnder = total - predOver;
console.log(
` Actual: Over: ${actOver} (${((actOver / total) * 100).toFixed(1)}%) | Under: ${actUnder} (${((actUnder / total) * 100).toFixed(1)}%)`,
@@ -392,11 +392,11 @@ async function runBacktest(): Promise<void> {
` Predicted: Over: ${predOver} (${((predOver / total) * 100).toFixed(1)}%) | Under: ${predUnder} (${((predUnder / total) * 100).toFixed(1)}%)`,
);
console.log('\n 📊 BTTS Breakdown:');
const actBttsYes = allResults.filter((r) => r.actual.btts === 'Yes').length;
console.log("\n 📊 BTTS Breakdown:");
const actBttsYes = allResults.filter((r) => r.actual.btts === "Yes").length;
const actBttsNo = total - actBttsYes;
const predBttsYes = allResults.filter(
(r) => r.predicted.btts === 'Yes',
(r) => r.predicted.btts === "Yes",
).length;
const predBttsNo = total - predBttsYes;
console.log(
@@ -406,14 +406,14 @@ async function runBacktest(): Promise<void> {
` Predicted: Yes: ${predBttsYes} (${((predBttsYes / total) * 100).toFixed(1)}%) | No: ${predBttsNo} (${((predBttsNo / total) * 100).toFixed(1)}%)`,
);
console.log('════════════════════════════════════════════════════════');
console.log('✅ Backtest complete!');
console.log("════════════════════════════════════════════════════════");
console.log("✅ Backtest complete!");
await prisma.$disconnect();
}
runBacktest().catch((err: unknown) => {
console.error('❌ Backtest failed:', err);
console.error("❌ Backtest failed:", err);
void prisma.$disconnect();
process.exit(1);
});