cr
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -9,18 +9,18 @@
|
||||
* Usage: npx ts-node --transpile-only -r tsconfig-paths/register src/scripts/batch-predict.ts
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import axios from "axios";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
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 BATCH_SIZE = 5;
|
||||
const MAX_MATCHES_TO_PROCESS = 1000; // Limit for local testing/batch capacity
|
||||
|
||||
async function runBatchPrediction() {
|
||||
console.log('🗓 BATCH PREDICTION PIPELINE STARTING');
|
||||
console.log('════════════════════════════════════════════════════════');
|
||||
console.log("🗓 BATCH PREDICTION PIPELINE STARTING");
|
||||
console.log("════════════════════════════════════════════════════════");
|
||||
|
||||
// 1. Health check
|
||||
try {
|
||||
@@ -30,20 +30,20 @@ async function runBatchPrediction() {
|
||||
console.log(`✅ AI Engine Health: ${JSON.stringify(health.data)}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
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 upcoming matches (Not Started)
|
||||
const upcomingMatches = await prisma.match.findMany({
|
||||
where: {
|
||||
status: 'NS',
|
||||
status: "NS",
|
||||
mstUtc: {
|
||||
gte: Math.floor(Date.now() / 1000), // Future matches
|
||||
},
|
||||
sport: 'football',
|
||||
sport: "football",
|
||||
},
|
||||
orderBy: { mstUtc: 'asc' },
|
||||
orderBy: { mstUtc: "asc" },
|
||||
take: MAX_MATCHES_TO_PROCESS,
|
||||
select: {
|
||||
id: true,
|
||||
@@ -105,7 +105,7 @@ async function runBatchPrediction() {
|
||||
const err = e as Error;
|
||||
console.error(
|
||||
` ❌ Failed for match ${match.id}:`,
|
||||
err?.message || 'Unknown error',
|
||||
err?.message || "Unknown error",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -116,12 +116,12 @@ async function runBatchPrediction() {
|
||||
processedCount += batch.length;
|
||||
}
|
||||
|
||||
console.log('\n════════════════════════════════════════════════════════');
|
||||
console.log("\n════════════════════════════════════════════════════════");
|
||||
console.log(`🎉 BATCH PROCESS COMPLETE`);
|
||||
console.log(` Total Processed: ${processedCount}`);
|
||||
console.log(` Successfully Updated/Created: ${successCount}`);
|
||||
console.log(` Failed: ${processedCount - successCount}`);
|
||||
console.log('════════════════════════════════════════════════════════');
|
||||
console.log("════════════════════════════════════════════════════════");
|
||||
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔍 Checking for potential duplicate matches...');
|
||||
console.log("🔍 Checking for potential duplicate matches...");
|
||||
|
||||
// Group by unique match characteristics
|
||||
// Since we can't easily do GROUP BY with HAVING count > 1 in Prisma standard API without raw query,
|
||||
@@ -35,7 +35,7 @@ async function main() {
|
||||
|
||||
if (duplicates.length === 0) {
|
||||
console.log(
|
||||
'✅ No duplicate matches found based on (HomeTeam + AwayTeam + Date).',
|
||||
"✅ No duplicate matches found based on (HomeTeam + AwayTeam + Date).",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ async function main() {
|
||||
console.log(
|
||||
`📅 ${date} | ${homeTeam?.name} vs ${awayTeam?.name} (Count: ${group.count})`,
|
||||
);
|
||||
console.log(` IDs: ${group.ids.join(', ')}`);
|
||||
console.log(` IDs: ${group.ids.join(", ")}`);
|
||||
|
||||
// Check details of the duplicates to see if one is complete and one is not
|
||||
for (const id of group.ids) {
|
||||
@@ -73,20 +73,20 @@ async function main() {
|
||||
|
||||
if (match) {
|
||||
const counts = [
|
||||
match.oddCategories.length > 0 ? 'Odds' : '',
|
||||
match.footballTeamStats.length > 0 ? 'Stats' : '',
|
||||
match.playerEvents.length > 0 ? 'Events' : '',
|
||||
match.officials.length > 0 ? 'Officials' : '',
|
||||
match.oddCategories.length > 0 ? "Odds" : "",
|
||||
match.footballTeamStats.length > 0 ? "Stats" : "",
|
||||
match.playerEvents.length > 0 ? "Events" : "",
|
||||
match.officials.length > 0 ? "Officials" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
.join(", ");
|
||||
|
||||
console.log(
|
||||
` - [${id}] Status: ${match.status} | Score: ${match.scoreHome}-${match.scoreAway} | Data: ${counts || 'None'}`,
|
||||
` - [${id}] Status: ${match.status} | Score: ${match.scoreHome}-${match.scoreAway} | Data: ${counts || "None"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log('---------------------------------------------------');
|
||||
console.log("---------------------------------------------------");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,29 +5,29 @@
|
||||
* Kullanım: npx ts-node -r tsconfig-paths/register src/scripts/cleanup-live-matches.ts
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const FINISHED_STATUSES = ['Finished', 'Played', 'FT', 'AET', 'PEN', 'Ended'];
|
||||
const FINISHED_STATES = ['Finished', 'post', 'FT', 'postGame'];
|
||||
const FINISHED_STATUSES = ["Finished", "Played", "FT", "AET", "PEN", "Ended"];
|
||||
const FINISHED_STATES = ["Finished", "post", "FT", "postGame"];
|
||||
const LIVE_STATUSES = [
|
||||
'LIVE',
|
||||
'1H',
|
||||
'2H',
|
||||
'HT',
|
||||
'1Q',
|
||||
'2Q',
|
||||
'3Q',
|
||||
'4Q',
|
||||
'Playing',
|
||||
'Half Time',
|
||||
"LIVE",
|
||||
"1H",
|
||||
"2H",
|
||||
"HT",
|
||||
"1Q",
|
||||
"2Q",
|
||||
"3Q",
|
||||
"4Q",
|
||||
"Playing",
|
||||
"Half Time",
|
||||
];
|
||||
const LIVE_STATES = ['live', 'firsthalf', 'secondhalf'];
|
||||
const LIVE_STATES = ["live", "firsthalf", "secondhalf"];
|
||||
|
||||
async function cleanupLiveMatches() {
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
try {
|
||||
console.log('🧹 Live matches temizliği başlıyor...');
|
||||
console.log("🧹 Live matches temizliği başlıyor...");
|
||||
|
||||
const now = Date.now();
|
||||
const finishedGraceMs = 6 * 60 * 60 * 1000;
|
||||
@@ -51,7 +51,7 @@ async function cleanupLiveMatches() {
|
||||
},
|
||||
});
|
||||
|
||||
console.log('📊 Mevcut durum:');
|
||||
console.log("📊 Mevcut durum:");
|
||||
console.log(` Toplam live_matches: ${totalBefore}`);
|
||||
console.log(` Geçmiş zamanlı kayıt: ${outdatedCount}`);
|
||||
console.log(
|
||||
@@ -83,7 +83,7 @@ async function cleanupLiveMatches() {
|
||||
|
||||
const totalAfter = await prisma.liveMatch.count();
|
||||
|
||||
console.log('\n✅ Temizlik tamamlandı!');
|
||||
console.log("\n✅ Temizlik tamamlandı!");
|
||||
console.log(` Silinen maç: ${deleted.count}`);
|
||||
console.log(` Kalan maç: ${totalAfter}`);
|
||||
|
||||
@@ -93,12 +93,12 @@ async function cleanupLiveMatches() {
|
||||
GROUP BY state
|
||||
`;
|
||||
|
||||
console.log('\n📋 Kalan maçların durumları:');
|
||||
console.log("\n📋 Kalan maçların durumları:");
|
||||
(states as any).forEach((s: any) => {
|
||||
console.log(` ${s.state || 'null'}: ${s.count}`);
|
||||
console.log(` ${s.state || "null"}: ${s.count}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Hata:', error);
|
||||
console.error("❌ Hata:", error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* Usage: npx ts-node --transpile-only -r tsconfig-paths/register src/scripts/compute-elo-ratings.ts
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Types
|
||||
@@ -72,9 +72,9 @@ function getResultChar(
|
||||
scoreAway: number,
|
||||
isHomeTeam: boolean,
|
||||
): string {
|
||||
if (scoreHome > scoreAway) return isHomeTeam ? 'W' : 'L';
|
||||
if (scoreHome < scoreAway) return isHomeTeam ? 'L' : 'W';
|
||||
return 'D';
|
||||
if (scoreHome > scoreAway) return isHomeTeam ? "W" : "L";
|
||||
if (scoreHome < scoreAway) return isHomeTeam ? "L" : "W";
|
||||
return "D";
|
||||
}
|
||||
|
||||
function calculateFormElo(recentResults: string[]): number {
|
||||
@@ -86,7 +86,7 @@ function calculateFormElo(recentResults: string[]): number {
|
||||
for (let i = 0; i < recentResults.length; i++) {
|
||||
const weight = Math.pow(FORM_DECAY, i); // Most recent = highest weight
|
||||
const result = recentResults[i];
|
||||
const score = result === 'W' ? 3 : result === 'D' ? 1 : 0;
|
||||
const score = result === "W" ? 3 : result === "D" ? 1 : 0;
|
||||
formScore += score * weight;
|
||||
totalWeight += 3 * weight; // Max possible per match
|
||||
}
|
||||
@@ -105,14 +105,14 @@ async function computeEloRatings(): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
console.log('🏟️ ELO Rating Computation — Starting...');
|
||||
console.log('─'.repeat(60));
|
||||
console.log("🏟️ ELO Rating Computation — Starting...");
|
||||
console.log("─".repeat(60));
|
||||
|
||||
// 1. Fetch all finished football matches in chronological order
|
||||
const matches: MatchRecord[] = await prisma.match.findMany({
|
||||
where: {
|
||||
sport: 'football',
|
||||
status: 'FT',
|
||||
sport: "football",
|
||||
status: "FT",
|
||||
scoreHome: { not: null },
|
||||
scoreAway: { not: null },
|
||||
homeTeamId: { not: null },
|
||||
@@ -126,7 +126,7 @@ async function computeEloRatings(): Promise<void> {
|
||||
scoreAway: true,
|
||||
mstUtc: true,
|
||||
},
|
||||
orderBy: { mstUtc: 'asc' },
|
||||
orderBy: { mstUtc: "asc" },
|
||||
});
|
||||
|
||||
console.log(
|
||||
@@ -228,7 +228,7 @@ async function computeEloRatings(): Promise<void> {
|
||||
);
|
||||
|
||||
// 4. Bulk upsert to team_elo_ratings
|
||||
console.log('💾 Writing to team_elo_ratings...');
|
||||
console.log("💾 Writing to team_elo_ratings...");
|
||||
|
||||
const BATCH_SIZE = 500;
|
||||
const teams = Array.from(eloMap.entries());
|
||||
@@ -246,7 +246,7 @@ async function computeEloRatings(): Promise<void> {
|
||||
awayElo: Math.round(state.awayElo * 10) / 10,
|
||||
formElo: Math.round(state.formElo * 10) / 10,
|
||||
matchesPlayed: state.matchesPlayed,
|
||||
recentForm: state.recentResults.join(''),
|
||||
recentForm: state.recentResults.join(""),
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
@@ -255,7 +255,7 @@ async function computeEloRatings(): Promise<void> {
|
||||
awayElo: Math.round(state.awayElo * 10) / 10,
|
||||
formElo: Math.round(state.formElo * 10) / 10,
|
||||
matchesPlayed: state.matchesPlayed,
|
||||
recentForm: state.recentResults.join(''),
|
||||
recentForm: state.recentResults.join(""),
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -276,38 +276,38 @@ async function computeEloRatings(): Promise<void> {
|
||||
.map((s) => s.overallElo)
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
console.log('─'.repeat(60));
|
||||
console.log('📊 ELO Rating Summary:');
|
||||
console.log("─".repeat(60));
|
||||
console.log("📊 ELO Rating Summary:");
|
||||
console.log(` Teams rated: ${eloMap.size.toLocaleString()}`);
|
||||
console.log(` Matches used: ${processed.toLocaleString()}`);
|
||||
console.log(` Highest ELO: ${overallElos[0]?.toFixed(1) ?? 'N/A'}`);
|
||||
console.log(` Highest ELO: ${overallElos[0]?.toFixed(1) ?? "N/A"}`);
|
||||
console.log(
|
||||
` Lowest ELO: ${overallElos[overallElos.length - 1]?.toFixed(1) ?? 'N/A'}`,
|
||||
` Lowest ELO: ${overallElos[overallElos.length - 1]?.toFixed(1) ?? "N/A"}`,
|
||||
);
|
||||
console.log(
|
||||
` Median ELO: ${overallElos[Math.floor(overallElos.length / 2)]?.toFixed(1) ?? 'N/A'}`,
|
||||
` Median ELO: ${overallElos[Math.floor(overallElos.length / 2)]?.toFixed(1) ?? "N/A"}`,
|
||||
);
|
||||
console.log(` Duration: ${elapsedTotal}s`);
|
||||
console.log('─'.repeat(60));
|
||||
console.log("─".repeat(60));
|
||||
|
||||
// Top 20 teams
|
||||
const topTeams = await prisma.teamEloRating.findMany({
|
||||
orderBy: { overallElo: 'desc' },
|
||||
orderBy: { overallElo: "desc" },
|
||||
take: 20,
|
||||
include: { team: { select: { name: true } } },
|
||||
});
|
||||
|
||||
console.log('\n🏆 Top 20 Teams by ELO:');
|
||||
console.log("\n🏆 Top 20 Teams by ELO:");
|
||||
topTeams.forEach((t, i) => {
|
||||
const form = t.recentForm.split('').join('-');
|
||||
const form = t.recentForm.split("").join("-");
|
||||
console.log(
|
||||
` ${String(i + 1).padStart(2)}. ${t.team.name.padEnd(25)} Overall: ${t.overallElo.toFixed(1).padStart(7)} Home: ${t.homeElo.toFixed(1).padStart(7)} Away: ${t.awayElo.toFixed(1).padStart(7)} Form: ${form}`,
|
||||
);
|
||||
});
|
||||
|
||||
console.log('\n✅ Done!');
|
||||
console.log("\n✅ Done!");
|
||||
} catch (error) {
|
||||
console.error('❌ ELO computation failed:', error);
|
||||
console.error("❌ ELO computation failed:", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'reflect-metadata';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { AppModule } from '../app.module';
|
||||
import "reflect-metadata";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
import { AppModule } from "../app.module";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type SwaggerPaths = Record<string, Record<string, JsonRecord>>;
|
||||
@@ -14,7 +14,7 @@ interface PostmanResponse {
|
||||
originalRequest: JsonRecord;
|
||||
status: string;
|
||||
code: number;
|
||||
_postman_previewlanguage: 'json';
|
||||
_postman_previewlanguage: "json";
|
||||
header: Array<{ key: string; value: string }>;
|
||||
body: string;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ interface PostmanItem {
|
||||
|
||||
interface AiEndpointDefinition {
|
||||
name: string;
|
||||
method: 'GET' | 'POST';
|
||||
method: "GET" | "POST";
|
||||
path: string;
|
||||
description: string;
|
||||
query?: Array<{ key: string; value: string; description: string }>;
|
||||
@@ -40,7 +40,7 @@ function refName(ref: string | undefined): string | null {
|
||||
if (!ref) {
|
||||
return null;
|
||||
}
|
||||
const parts = ref.split('/');
|
||||
const parts = ref.split("/");
|
||||
return parts[parts.length - 1] ?? null;
|
||||
}
|
||||
|
||||
@@ -48,12 +48,13 @@ function resolveSchema(
|
||||
schema: unknown,
|
||||
schemas: SwaggerSchemas,
|
||||
): JsonRecord | null {
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schemaObject = schema as JsonRecord;
|
||||
const schemaRef = typeof schemaObject.$ref === 'string' ? schemaObject.$ref : null;
|
||||
const schemaRef =
|
||||
typeof schemaObject.$ref === "string" ? schemaObject.$ref : null;
|
||||
if (schemaRef) {
|
||||
const name = refName(schemaRef);
|
||||
return name ? (schemas[name] ?? null) : null;
|
||||
@@ -73,31 +74,31 @@ function examplePrimitive(schema: JsonRecord): unknown {
|
||||
return schema.enum[0];
|
||||
}
|
||||
|
||||
const type = typeof schema.type === 'string' ? schema.type : 'string';
|
||||
const format = typeof schema.format === 'string' ? schema.format : '';
|
||||
const type = typeof schema.type === "string" ? schema.type : "string";
|
||||
const format = typeof schema.format === "string" ? schema.format : "";
|
||||
|
||||
if (type === 'string') {
|
||||
if (format === 'email') {
|
||||
return 'user@example.com';
|
||||
if (type === "string") {
|
||||
if (format === "email") {
|
||||
return "user@example.com";
|
||||
}
|
||||
if (format === 'date-time') {
|
||||
return '2026-04-14T00:00:00.000Z';
|
||||
if (format === "date-time") {
|
||||
return "2026-04-14T00:00:00.000Z";
|
||||
}
|
||||
if (format === 'date') {
|
||||
return '2026-04-14';
|
||||
if (format === "date") {
|
||||
return "2026-04-14";
|
||||
}
|
||||
if (format === 'uuid') {
|
||||
return '11111111-1111-1111-1111-111111111111';
|
||||
if (format === "uuid") {
|
||||
return "11111111-1111-1111-1111-111111111111";
|
||||
}
|
||||
return 'string';
|
||||
return "string";
|
||||
}
|
||||
if (type === 'integer' || type === 'number') {
|
||||
if (type === "integer" || type === "number") {
|
||||
return 1;
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
if (type === "boolean") {
|
||||
return true;
|
||||
}
|
||||
return 'string';
|
||||
return "string";
|
||||
}
|
||||
|
||||
function buildExampleFromSchema(
|
||||
@@ -110,7 +111,7 @@ function buildExampleFromSchema(
|
||||
return null;
|
||||
}
|
||||
|
||||
const schemaRef = typeof resolved.$ref === 'string' ? resolved.$ref : null;
|
||||
const schemaRef = typeof resolved.$ref === "string" ? resolved.$ref : null;
|
||||
if (schemaRef) {
|
||||
const name = refName(schemaRef);
|
||||
if (!name || visited.has(name)) {
|
||||
@@ -124,7 +125,7 @@ function buildExampleFromSchema(
|
||||
if (Array.isArray(resolved.allOf) && resolved.allOf.length > 0) {
|
||||
return resolved.allOf.reduce<JsonRecord>((accumulator, part) => {
|
||||
const partial = buildExampleFromSchema(part, schemas, visited);
|
||||
if (partial && typeof partial === 'object' && !Array.isArray(partial)) {
|
||||
if (partial && typeof partial === "object" && !Array.isArray(partial)) {
|
||||
return { ...accumulator, ...(partial as JsonRecord) };
|
||||
}
|
||||
return accumulator;
|
||||
@@ -139,12 +140,12 @@ function buildExampleFromSchema(
|
||||
return buildExampleFromSchema(resolved.anyOf[0], schemas, visited);
|
||||
}
|
||||
|
||||
const type = typeof resolved.type === 'string' ? resolved.type : 'object';
|
||||
if (type === 'array') {
|
||||
const type = typeof resolved.type === "string" ? resolved.type : "object";
|
||||
if (type === "array") {
|
||||
return [buildExampleFromSchema(resolved.items, schemas, visited)];
|
||||
}
|
||||
|
||||
if (type === 'object' || resolved.properties) {
|
||||
if (type === "object" || resolved.properties) {
|
||||
const properties = (resolved.properties ?? {}) as JsonRecord;
|
||||
const output: JsonRecord = {};
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
@@ -159,23 +160,23 @@ function buildExampleFromSchema(
|
||||
}
|
||||
|
||||
function swaggerSchemaFromContent(content: unknown): unknown {
|
||||
if (!content || typeof content !== 'object') {
|
||||
if (!content || typeof content !== "object") {
|
||||
return null;
|
||||
}
|
||||
const contentObject = content as JsonRecord;
|
||||
const jsonContent = contentObject['application/json'];
|
||||
if (jsonContent && typeof jsonContent === 'object') {
|
||||
const jsonContent = contentObject["application/json"];
|
||||
if (jsonContent && typeof jsonContent === "object") {
|
||||
return (jsonContent as JsonRecord).schema ?? null;
|
||||
}
|
||||
const firstContent = Object.values(contentObject)[0];
|
||||
if (firstContent && typeof firstContent === 'object') {
|
||||
if (firstContent && typeof firstContent === "object") {
|
||||
return (firstContent as JsonRecord).schema ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toPostmanPath(pathname: string): string {
|
||||
return pathname.replace(/\{([^}]+)\}/g, '{{$1}}');
|
||||
return pathname.replace(/\{([^}]+)\}/g, "{{$1}}");
|
||||
}
|
||||
|
||||
function buildRequestBody(
|
||||
@@ -213,25 +214,26 @@ function buildResponses(
|
||||
name: `${method.toUpperCase()} ${rawPath} - ${statusCode}`,
|
||||
originalRequest: {
|
||||
method: method.toUpperCase(),
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
header: [{ key: "Content-Type", value: "application/json" }],
|
||||
body: body
|
||||
? {
|
||||
mode: 'raw',
|
||||
mode: "raw",
|
||||
raw: body,
|
||||
}
|
||||
: undefined,
|
||||
url: {
|
||||
raw: `{{${baseUrlVariable}}}${toPostmanPath(rawPath)}`,
|
||||
host: [`{{${baseUrlVariable}}}`],
|
||||
path: rawPath.split('/').filter(Boolean),
|
||||
path: rawPath.split("/").filter(Boolean),
|
||||
},
|
||||
},
|
||||
status: typeof responseRecord.description === 'string'
|
||||
? responseRecord.description
|
||||
: `HTTP ${statusCode}`,
|
||||
status:
|
||||
typeof responseRecord.description === "string"
|
||||
? responseRecord.description
|
||||
: `HTTP ${statusCode}`,
|
||||
code: Number.isFinite(numericStatus) ? numericStatus : 200,
|
||||
_postman_previewlanguage: 'json',
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
_postman_previewlanguage: "json",
|
||||
header: [{ key: "Content-Type", value: "application/json" }],
|
||||
body: JSON.stringify(example ?? {}, null, 2),
|
||||
};
|
||||
});
|
||||
@@ -243,14 +245,14 @@ function buildQueryParams(operation: JsonRecord): Array<JsonRecord> {
|
||||
: [];
|
||||
|
||||
return parameters
|
||||
.filter((parameter) => parameter.in === 'query')
|
||||
.filter((parameter) => parameter.in === "query")
|
||||
.map((parameter) => ({
|
||||
key: String(parameter.name ?? ''),
|
||||
key: String(parameter.name ?? ""),
|
||||
value:
|
||||
parameter.schema && typeof parameter.schema === 'object'
|
||||
? String(((parameter.schema as JsonRecord).default ?? ''))
|
||||
: '',
|
||||
description: String(parameter.description ?? ''),
|
||||
parameter.schema && typeof parameter.schema === "object"
|
||||
? String((parameter.schema as JsonRecord).default ?? "")
|
||||
: "",
|
||||
description: String(parameter.description ?? ""),
|
||||
disabled: parameter.required === true ? false : true,
|
||||
}));
|
||||
}
|
||||
@@ -258,8 +260,8 @@ function buildQueryParams(operation: JsonRecord): Array<JsonRecord> {
|
||||
function buildHeaders(operation: JsonRecord): Array<JsonRecord> {
|
||||
const headers: Array<JsonRecord> = [
|
||||
{
|
||||
key: 'Content-Type',
|
||||
value: 'application/json',
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -268,8 +270,8 @@ function buildHeaders(operation: JsonRecord): Array<JsonRecord> {
|
||||
: [];
|
||||
if (security.length > 0) {
|
||||
headers.push({
|
||||
key: 'Authorization',
|
||||
value: 'Bearer {{accessToken}}',
|
||||
key: "Authorization",
|
||||
value: "Bearer {{accessToken}}",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,20 +294,22 @@ function createRequestItem(
|
||||
method: method.toUpperCase(),
|
||||
header: headers,
|
||||
description:
|
||||
typeof operation.description === 'string'
|
||||
typeof operation.description === "string"
|
||||
? operation.description
|
||||
: (typeof operation.summary === 'string' ? operation.summary : ''),
|
||||
: typeof operation.summary === "string"
|
||||
? operation.summary
|
||||
: "",
|
||||
url: {
|
||||
raw: `{{${baseUrlVariable}}}${toPostmanPath(rawPath)}`,
|
||||
host: [`{{${baseUrlVariable}}}`],
|
||||
path: rawPath.split('/').filter(Boolean),
|
||||
path: rawPath.split("/").filter(Boolean),
|
||||
query,
|
||||
},
|
||||
};
|
||||
|
||||
if (body) {
|
||||
request.body = {
|
||||
mode: 'raw',
|
||||
mode: "raw",
|
||||
raw: body,
|
||||
};
|
||||
}
|
||||
@@ -335,18 +339,19 @@ function buildNestFolders(document: JsonRecord): PostmanItem[] {
|
||||
|
||||
for (const [rawPath, pathItem] of Object.entries(paths)) {
|
||||
for (const [method, operationObject] of Object.entries(pathItem)) {
|
||||
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
|
||||
if (!["get", "post", "put", "patch", "delete"].includes(method)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const operation = operationObject as JsonRecord;
|
||||
const operation = operationObject;
|
||||
const tags = Array.isArray(operation.tags) ? operation.tags : [];
|
||||
const folderName =
|
||||
typeof tags[0] === 'string' && tags[0].trim().length > 0
|
||||
typeof tags[0] === "string" && tags[0].trim().length > 0
|
||||
? tags[0]
|
||||
: 'Misc';
|
||||
: "Misc";
|
||||
const requestName =
|
||||
typeof operation.summary === 'string' && operation.summary.trim().length > 0
|
||||
typeof operation.summary === "string" &&
|
||||
operation.summary.trim().length > 0
|
||||
? operation.summary
|
||||
: `${method.toUpperCase()} ${rawPath}`;
|
||||
|
||||
@@ -354,7 +359,7 @@ function buildNestFolders(document: JsonRecord): PostmanItem[] {
|
||||
requestName,
|
||||
method,
|
||||
rawPath,
|
||||
'beBaseUrl',
|
||||
"beBaseUrl",
|
||||
operation,
|
||||
safeSchemas,
|
||||
);
|
||||
@@ -379,8 +384,8 @@ function createAiRequest(
|
||||
): PostmanItem {
|
||||
const url: JsonRecord = {
|
||||
raw: `{{aiBaseUrl}}${endpoint.path}`,
|
||||
host: ['{{aiBaseUrl}}'],
|
||||
path: endpoint.path.split('/').filter(Boolean),
|
||||
host: ["{{aiBaseUrl}}"],
|
||||
path: endpoint.path.split("/").filter(Boolean),
|
||||
};
|
||||
|
||||
if (endpoint.query && endpoint.query.length > 0) {
|
||||
@@ -393,14 +398,14 @@ function createAiRequest(
|
||||
|
||||
const request: JsonRecord = {
|
||||
method: endpoint.method,
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
header: [{ key: "Content-Type", value: "application/json" }],
|
||||
description: endpoint.description,
|
||||
url,
|
||||
};
|
||||
|
||||
if (endpoint.body) {
|
||||
request.body = {
|
||||
mode: 'raw',
|
||||
mode: "raw",
|
||||
raw: JSON.stringify(endpoint.body, null, 2),
|
||||
};
|
||||
}
|
||||
@@ -412,10 +417,10 @@ function createAiRequest(
|
||||
{
|
||||
name: `${endpoint.method} ${endpoint.path}`,
|
||||
originalRequest: request,
|
||||
status: 'OK',
|
||||
status: "OK",
|
||||
code: 200,
|
||||
_postman_previewlanguage: 'json',
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
_postman_previewlanguage: "json",
|
||||
header: [{ key: "Content-Type", value: "application/json" }],
|
||||
body: JSON.stringify(endpoint.response, null, 2),
|
||||
},
|
||||
],
|
||||
@@ -425,105 +430,105 @@ function createAiRequest(
|
||||
function buildAiFolder(): PostmanItem {
|
||||
const v20Endpoints: AiEndpointDefinition[] = [
|
||||
{
|
||||
name: 'Root',
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
description: 'AI engine root status endpoint',
|
||||
name: "Root",
|
||||
method: "GET",
|
||||
path: "/",
|
||||
description: "AI engine root status endpoint",
|
||||
response: {
|
||||
status: 'Suggest-Bet AI Engine v20+',
|
||||
engine: 'V20 Plus Single Match Orchestrator',
|
||||
status: "Suggest-Bet AI Engine v20+",
|
||||
engine: "V20 Plus Single Match Orchestrator",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Health',
|
||||
method: 'GET',
|
||||
path: '/health',
|
||||
description: 'AI engine health endpoint',
|
||||
response: { status: 'healthy', engine: 'v20plus', ready: true },
|
||||
name: "Health",
|
||||
method: "GET",
|
||||
path: "/health",
|
||||
description: "AI engine health endpoint",
|
||||
response: { status: "healthy", engine: "v20plus", ready: true },
|
||||
},
|
||||
{
|
||||
name: 'Analyze Match',
|
||||
method: 'POST',
|
||||
path: '/v20plus/analyze/{{match_id}}',
|
||||
description: 'Full V20+ single match analysis',
|
||||
name: "Analyze Match",
|
||||
method: "POST",
|
||||
path: "/v20plus/analyze/{{match_id}}",
|
||||
description: "Full V20+ single match analysis",
|
||||
response: {
|
||||
model_version: 'v30.0',
|
||||
match_info: { match_id: '{{match_id}}' },
|
||||
main_pick: { market: 'OU25', pick: '2.5 Üst' },
|
||||
model_version: "v30.0",
|
||||
match_info: { match_id: "{{match_id}}" },
|
||||
main_pick: { market: "OU25", pick: "2.5 Üst" },
|
||||
market_board: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Analyze HTMS',
|
||||
method: 'GET',
|
||||
path: '/v20plus/analyze-htms/{{match_id}}',
|
||||
description: 'Half-time result analysis endpoint',
|
||||
response: { match_id: '{{match_id}}', market: 'HT' },
|
||||
name: "Analyze HTMS",
|
||||
method: "GET",
|
||||
path: "/v20plus/analyze-htms/{{match_id}}",
|
||||
description: "Half-time result analysis endpoint",
|
||||
response: { match_id: "{{match_id}}", market: "HT" },
|
||||
},
|
||||
{
|
||||
name: 'Analyze HTFT',
|
||||
method: 'GET',
|
||||
path: '/v20plus/analyze-htft/{{match_id}}',
|
||||
description: 'Half-time/full-time analysis endpoint',
|
||||
name: "Analyze HTFT",
|
||||
method: "GET",
|
||||
path: "/v20plus/analyze-htft/{{match_id}}",
|
||||
description: "Half-time/full-time analysis endpoint",
|
||||
query: [
|
||||
{
|
||||
key: 'timeout_sec',
|
||||
value: '30',
|
||||
description: 'Timeout between 3 and 120 seconds',
|
||||
key: "timeout_sec",
|
||||
value: "30",
|
||||
description: "Timeout between 3 and 120 seconds",
|
||||
},
|
||||
],
|
||||
response: {
|
||||
engine: 'v20plus.1',
|
||||
match_info: { match_id: '{{match_id}}' },
|
||||
ht_ft_probs: { '1/1': 0.25, 'X/X': 0.18 },
|
||||
engine: "v20plus.1",
|
||||
match_info: { match_id: "{{match_id}}" },
|
||||
ht_ft_probs: { "1/1": 0.25, "X/X": 0.18 },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Generate Coupon',
|
||||
method: 'POST',
|
||||
path: '/v20plus/coupon',
|
||||
description: 'Generate V20+ coupon from selected matches',
|
||||
name: "Generate Coupon",
|
||||
method: "POST",
|
||||
path: "/v20plus/coupon",
|
||||
description: "Generate V20+ coupon from selected matches",
|
||||
body: {
|
||||
match_ids: ['match-1', 'match-2'],
|
||||
strategy: 'BALANCED',
|
||||
match_ids: ["match-1", "match-2"],
|
||||
strategy: "BALANCED",
|
||||
max_matches: 4,
|
||||
min_confidence: 55,
|
||||
},
|
||||
response: {
|
||||
success: true,
|
||||
data: {
|
||||
strategy: 'BALANCED',
|
||||
strategy: "BALANCED",
|
||||
bets: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Daily Banker',
|
||||
method: 'GET',
|
||||
path: '/v20plus/daily-banker',
|
||||
description: 'Get daily banker picks',
|
||||
name: "Daily Banker",
|
||||
method: "GET",
|
||||
path: "/v20plus/daily-banker",
|
||||
description: "Get daily banker picks",
|
||||
query: [
|
||||
{
|
||||
key: 'count',
|
||||
value: '3',
|
||||
description: 'Number of banker picks',
|
||||
key: "count",
|
||||
value: "3",
|
||||
description: "Number of banker picks",
|
||||
},
|
||||
],
|
||||
response: { count: 3, bankers: [] },
|
||||
},
|
||||
{
|
||||
name: 'Reversal Watchlist',
|
||||
method: 'GET',
|
||||
path: '/v20plus/reversal-watchlist',
|
||||
description: 'Reversal watchlist candidates',
|
||||
name: "Reversal Watchlist",
|
||||
method: "GET",
|
||||
path: "/v20plus/reversal-watchlist",
|
||||
description: "Reversal watchlist candidates",
|
||||
query: [
|
||||
{ key: 'count', value: '20', description: 'Result size' },
|
||||
{ key: 'horizon_hours', value: '72', description: 'Future horizon' },
|
||||
{ key: 'min_score', value: '45', description: 'Minimum score' },
|
||||
{ key: "count", value: "20", description: "Result size" },
|
||||
{ key: "horizon_hours", value: "72", description: "Future horizon" },
|
||||
{ key: "min_score", value: "45", description: "Minimum score" },
|
||||
{
|
||||
key: 'top_leagues_only',
|
||||
value: 'false',
|
||||
description: 'Filter to top leagues',
|
||||
key: "top_leagues_only",
|
||||
value: "false",
|
||||
description: "Filter to top leagues",
|
||||
},
|
||||
],
|
||||
response: { count: 0, items: [] },
|
||||
@@ -532,42 +537,42 @@ function buildAiFolder(): PostmanItem {
|
||||
|
||||
const v2Endpoints: AiEndpointDefinition[] = [
|
||||
{
|
||||
name: 'V2 Health',
|
||||
method: 'GET',
|
||||
path: '/v2/health',
|
||||
description: 'V2 betting engine health',
|
||||
name: "V2 Health",
|
||||
method: "GET",
|
||||
path: "/v2/health",
|
||||
description: "V2 betting engine health",
|
||||
response: {
|
||||
status: 'healthy',
|
||||
engine: 'v2.betting_engine',
|
||||
status: "healthy",
|
||||
engine: "v2.betting_engine",
|
||||
models_loaded: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'V2 Analyze Match',
|
||||
method: 'POST',
|
||||
path: '/v2/analyze/{{match_id}}',
|
||||
description: 'V2 leakage-free match analysis',
|
||||
name: "V2 Analyze Match",
|
||||
method: "POST",
|
||||
path: "/v2/analyze/{{match_id}}",
|
||||
description: "V2 leakage-free match analysis",
|
||||
response: {
|
||||
model_version: 'v2.betting_engine',
|
||||
match_info: { match_id: '{{match_id}}' },
|
||||
main_pick: { market: 'MS', pick: '1' },
|
||||
model_version: "v2.betting_engine",
|
||||
match_info: { match_id: "{{match_id}}" },
|
||||
main_pick: { market: "MS", pick: "1" },
|
||||
market_board: {
|
||||
MS: { pick: '1', confidence: 58.4 },
|
||||
MS: { pick: "1", confidence: 58.4 },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
name: 'AI Engine',
|
||||
name: "AI Engine",
|
||||
item: [
|
||||
{
|
||||
name: 'V20+',
|
||||
item: v20Endpoints.map((endpoint) => createAiRequest(endpoint, 'V20+')),
|
||||
name: "V20+",
|
||||
item: v20Endpoints.map((endpoint) => createAiRequest(endpoint, "V20+")),
|
||||
},
|
||||
{
|
||||
name: 'V2',
|
||||
item: v2Endpoints.map((endpoint) => createAiRequest(endpoint, 'V2')),
|
||||
name: "V2",
|
||||
item: v2Endpoints.map((endpoint) => createAiRequest(endpoint, "V2")),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -575,21 +580,21 @@ function buildAiFolder(): PostmanItem {
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const projectRoot = process.cwd();
|
||||
const outputDir = path.join(projectRoot, 'mds');
|
||||
const outputDir = path.join(projectRoot, "mds");
|
||||
const outputFile = path.join(
|
||||
outputDir,
|
||||
'suggest-bet-platform.postman_collection.json',
|
||||
"suggest-bet-platform.postman_collection.json",
|
||||
);
|
||||
|
||||
process.env.REDIS_ENABLED = 'true';
|
||||
process.env.REDIS_ENABLED = "true";
|
||||
|
||||
const app = await NestFactory.create(AppModule, { logger: false });
|
||||
app.setGlobalPrefix('api');
|
||||
app.setGlobalPrefix("api");
|
||||
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('Suggest Bet Backend API')
|
||||
.setDescription('Postman collection export source')
|
||||
.setVersion('1.0')
|
||||
.setTitle("Suggest Bet Backend API")
|
||||
.setDescription("Postman collection export source")
|
||||
.setVersion("1.0")
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
@@ -600,25 +605,25 @@ async function run(): Promise<void> {
|
||||
|
||||
const collection: JsonRecord = {
|
||||
info: {
|
||||
name: 'Suggest-Bet Platform API',
|
||||
name: "Suggest-Bet Platform API",
|
||||
description:
|
||||
'Auto-generated Postman collection for Nest backend and AI engine endpoints.',
|
||||
"Auto-generated Postman collection for Nest backend and AI engine endpoints.",
|
||||
schema:
|
||||
'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
"https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
},
|
||||
variable: [
|
||||
{ key: 'beBaseUrl', value: 'http://localhost:3005' },
|
||||
{ key: 'aiBaseUrl', value: 'http://localhost:8000' },
|
||||
{ key: 'accessToken', value: '' },
|
||||
{ key: 'match_id', value: 'sample-match-id' },
|
||||
{ key: "beBaseUrl", value: "http://localhost:3005" },
|
||||
{ key: "aiBaseUrl", value: "http://localhost:8000" },
|
||||
{ key: "accessToken", value: "" },
|
||||
{ key: "match_id", value: "sample-match-id" },
|
||||
],
|
||||
auth: {
|
||||
type: 'bearer',
|
||||
bearer: [{ key: 'token', value: '{{accessToken}}', type: 'string' }],
|
||||
type: "bearer",
|
||||
bearer: [{ key: "token", value: "{{accessToken}}", type: "string" }],
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'Nest API',
|
||||
name: "Nest API",
|
||||
item: buildNestFolders(document),
|
||||
},
|
||||
buildAiFolder(),
|
||||
@@ -626,7 +631,7 @@ async function run(): Promise<void> {
|
||||
};
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
writeFileSync(outputFile, JSON.stringify(collection, null, 2), 'utf8');
|
||||
writeFileSync(outputFile, JSON.stringify(collection, null, 2), "utf8");
|
||||
|
||||
await app.close();
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import 'reflect-metadata';
|
||||
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import ts from 'typescript';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from '../app.module';
|
||||
import "reflect-metadata";
|
||||
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import ts from "typescript";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||
import { AppModule } from "../app.module";
|
||||
|
||||
type HttpMethod =
|
||||
| 'get'
|
||||
| 'post'
|
||||
| 'put'
|
||||
| 'patch'
|
||||
| 'delete'
|
||||
| 'options'
|
||||
| 'head'
|
||||
| 'all';
|
||||
| "get"
|
||||
| "post"
|
||||
| "put"
|
||||
| "patch"
|
||||
| "delete"
|
||||
| "options"
|
||||
| "head"
|
||||
| "all";
|
||||
|
||||
interface TsDecoratorMeta {
|
||||
name: string;
|
||||
@@ -42,14 +42,14 @@ interface TsMethodMeta {
|
||||
}
|
||||
|
||||
const HTTP_DECORATOR_TO_METHOD: Record<string, HttpMethod> = {
|
||||
Get: 'get',
|
||||
Post: 'post',
|
||||
Put: 'put',
|
||||
Patch: 'patch',
|
||||
Delete: 'delete',
|
||||
Options: 'options',
|
||||
Head: 'head',
|
||||
All: 'all',
|
||||
Get: "get",
|
||||
Post: "post",
|
||||
Put: "put",
|
||||
Patch: "patch",
|
||||
Delete: "delete",
|
||||
Options: "options",
|
||||
Head: "head",
|
||||
All: "all",
|
||||
};
|
||||
|
||||
function getDecorators(node: ts.Node): readonly ts.Decorator[] {
|
||||
@@ -105,7 +105,7 @@ function collectControllerFiles(dirPath: string): string[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith('.controller.ts')) {
|
||||
if (entry.isFile() && entry.name.endsWith(".controller.ts")) {
|
||||
files.push(absolutePath);
|
||||
}
|
||||
}
|
||||
@@ -115,9 +115,9 @@ function collectControllerFiles(dirPath: string): string[] {
|
||||
|
||||
function normalizeRoutePart(value: string | undefined): string {
|
||||
if (!value || value === "''" || value === '""') {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
return value.trim().replace(/^\/+|\/+$/g, '');
|
||||
return value.trim().replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function buildSwaggerPath(
|
||||
@@ -131,18 +131,18 @@ function buildSwaggerPath(
|
||||
normalizeRoutePart(routePath),
|
||||
].filter(Boolean);
|
||||
|
||||
return `/${parts.join('/')}`;
|
||||
return `/${parts.join("/")}`;
|
||||
}
|
||||
|
||||
function collectTsEndpointMetadata(
|
||||
projectRoot: string,
|
||||
): Map<string, TsMethodMeta> {
|
||||
const modulesDir = path.join(projectRoot, 'src', 'modules');
|
||||
const modulesDir = path.join(projectRoot, "src", "modules");
|
||||
const controllerFiles = collectControllerFiles(modulesDir);
|
||||
const metadataByOperationId = new Map<string, TsMethodMeta>();
|
||||
|
||||
for (const filePath of controllerFiles) {
|
||||
const sourceText = readFileSync(filePath, 'utf8');
|
||||
const sourceText = readFileSync(filePath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
sourceText,
|
||||
@@ -164,7 +164,7 @@ function collectTsEndpointMetadata(
|
||||
);
|
||||
|
||||
const controllerDecorator = classDecorators.find(
|
||||
(decorator) => decorator.name === 'Controller',
|
||||
(decorator) => decorator.name === "Controller",
|
||||
);
|
||||
if (!controllerDecorator) {
|
||||
return;
|
||||
@@ -221,7 +221,7 @@ function collectTsEndpointMetadata(
|
||||
routePath,
|
||||
returnType,
|
||||
hasPublicDecorator: methodDecorators.some(
|
||||
(decorator) => decorator.name === 'Public',
|
||||
(decorator) => decorator.name === "Public",
|
||||
),
|
||||
methodDecorators: methodDecorators.map((decorator) => decorator.name),
|
||||
params,
|
||||
@@ -235,10 +235,10 @@ function collectTsEndpointMetadata(
|
||||
}
|
||||
|
||||
function refName(ref?: string): string | null {
|
||||
if (!ref || typeof ref !== 'string') {
|
||||
if (!ref || typeof ref !== "string") {
|
||||
return null;
|
||||
}
|
||||
const parts = ref.split('/');
|
||||
const parts = ref.split("/");
|
||||
return parts[parts.length - 1] ?? null;
|
||||
}
|
||||
|
||||
@@ -246,13 +246,13 @@ function collectSchemaRefs(
|
||||
value: unknown,
|
||||
refs = new Set<string>(),
|
||||
): Set<string> {
|
||||
if (!value || typeof value !== 'object') {
|
||||
if (!value || typeof value !== "object") {
|
||||
return refs;
|
||||
}
|
||||
|
||||
const recordValue = value as Record<string, unknown>;
|
||||
const maybeRef = recordValue.$ref;
|
||||
if (typeof maybeRef === 'string') {
|
||||
if (typeof maybeRef === "string") {
|
||||
const name = refName(maybeRef);
|
||||
if (name) {
|
||||
refs.add(name);
|
||||
@@ -267,22 +267,22 @@ function collectSchemaRefs(
|
||||
}
|
||||
|
||||
function schemaTypeSummary(schema: unknown): string {
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
return 'unknown';
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const schemaObj = schema as Record<string, unknown>;
|
||||
if (typeof schemaObj.$ref === 'string') {
|
||||
return refName(schemaObj.$ref) ?? 'unknown';
|
||||
if (typeof schemaObj.$ref === "string") {
|
||||
return refName(schemaObj.$ref) ?? "unknown";
|
||||
}
|
||||
|
||||
const type = typeof schemaObj.type === 'string' ? schemaObj.type : 'object';
|
||||
if (type === 'array') {
|
||||
const type = typeof schemaObj.type === "string" ? schemaObj.type : "object";
|
||||
if (type === "array") {
|
||||
return `array<${schemaTypeSummary(schemaObj.items)}>`;
|
||||
}
|
||||
|
||||
if (Array.isArray(schemaObj.enum) && schemaObj.enum.length > 0) {
|
||||
return `${type}(${schemaObj.enum.join(' | ')})`;
|
||||
return `${type}(${schemaObj.enum.join(" | ")})`;
|
||||
}
|
||||
|
||||
return type;
|
||||
@@ -295,35 +295,35 @@ function normalizeParameters(parameters: unknown[] = []) {
|
||||
.map((parameter) => {
|
||||
const schema = (parameter.schema ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
name: typeof parameter.name === 'string' ? parameter.name : '',
|
||||
in: typeof parameter.in === 'string' ? parameter.in : '',
|
||||
name: typeof parameter.name === "string" ? parameter.name : "",
|
||||
in: typeof parameter.in === "string" ? parameter.in : "",
|
||||
required: Boolean(parameter.required),
|
||||
description:
|
||||
typeof parameter.description === 'string'
|
||||
typeof parameter.description === "string"
|
||||
? parameter.description
|
||||
: null,
|
||||
type: schemaTypeSummary(schema),
|
||||
enum: Array.isArray(schema.enum) ? schema.enum : [],
|
||||
default: schema.default ?? null,
|
||||
format: typeof schema.format === 'string' ? schema.format : null,
|
||||
format: typeof schema.format === "string" ? schema.format : null,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
path: parsed.filter((item) => item.in === 'path'),
|
||||
query: parsed.filter((item) => item.in === 'query'),
|
||||
header: parsed.filter((item) => item.in === 'header'),
|
||||
cookie: parsed.filter((item) => item.in === 'cookie'),
|
||||
path: parsed.filter((item) => item.in === "path"),
|
||||
query: parsed.filter((item) => item.in === "query"),
|
||||
header: parsed.filter((item) => item.in === "header"),
|
||||
cookie: parsed.filter((item) => item.in === "cookie"),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRequestBody(requestBody: unknown) {
|
||||
if (!requestBody || typeof requestBody !== 'object') {
|
||||
if (!requestBody || typeof requestBody !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestBodyObj = requestBody as Record<string, unknown>;
|
||||
if (typeof requestBodyObj.$ref === 'string') {
|
||||
if (typeof requestBodyObj.$ref === "string") {
|
||||
return {
|
||||
required: false,
|
||||
contentTypes: [],
|
||||
@@ -378,9 +378,9 @@ function normalizeResponses(responses: Record<string, unknown>) {
|
||||
return {
|
||||
status: Number(statusCode),
|
||||
description:
|
||||
typeof responseObj.description === 'string'
|
||||
typeof responseObj.description === "string"
|
||||
? responseObj.description
|
||||
: '',
|
||||
: "",
|
||||
contentTypes,
|
||||
schemaTypes,
|
||||
schemaRefs: [...refs].sort(),
|
||||
@@ -392,25 +392,25 @@ function normalizeResponses(responses: Record<string, unknown>) {
|
||||
|
||||
async function run() {
|
||||
const projectRoot = process.cwd();
|
||||
const outputDir = path.join(projectRoot, 'mds');
|
||||
const outputDir = path.join(projectRoot, "mds");
|
||||
const outputFile = path.join(
|
||||
outputDir,
|
||||
'backend_endpoints_swagger_summary.json',
|
||||
"backend_endpoints_swagger_summary.json",
|
||||
);
|
||||
|
||||
// Predictions module is conditionally loaded with REDIS_ENABLED in AppModule.
|
||||
// Force-enable here to include all backend endpoints in one Swagger export.
|
||||
process.env.REDIS_ENABLED = 'true';
|
||||
process.env.REDIS_ENABLED = "true";
|
||||
|
||||
const tsMetadata = collectTsEndpointMetadata(projectRoot);
|
||||
|
||||
const app = await NestFactory.create(AppModule, { logger: false });
|
||||
app.setGlobalPrefix('api');
|
||||
app.setGlobalPrefix("api");
|
||||
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('Suggest Bet Backend API')
|
||||
.setDescription('Auto-generated endpoint summary from Swagger document')
|
||||
.setVersion('1.0')
|
||||
.setTitle("Suggest Bet Backend API")
|
||||
.setDescription("Auto-generated endpoint summary from Swagger document")
|
||||
.setVersion("1.0")
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
@@ -419,7 +419,7 @@ async function run() {
|
||||
|
||||
const endpoints: Array<Record<string, unknown>> = [];
|
||||
const seenOperationIds = new Set<string>();
|
||||
const globalPrefix = 'api';
|
||||
const globalPrefix = "api";
|
||||
|
||||
const sortedPaths = Object.keys(paths).sort((a, b) => a.localeCompare(b));
|
||||
for (const endpointPath of sortedPaths) {
|
||||
@@ -427,7 +427,7 @@ async function run() {
|
||||
|
||||
const methods = Object.keys(pathItem)
|
||||
.filter((method) =>
|
||||
['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].includes(
|
||||
["get", "post", "put", "patch", "delete", "options", "head"].includes(
|
||||
method,
|
||||
),
|
||||
)
|
||||
@@ -436,7 +436,7 @@ async function run() {
|
||||
for (const method of methods) {
|
||||
const operation = pathItem[method] as Record<string, unknown>;
|
||||
const operationId =
|
||||
typeof operation.operationId === 'string' ? operation.operationId : '';
|
||||
typeof operation.operationId === "string" ? operation.operationId : "";
|
||||
|
||||
if (operationId) {
|
||||
seenOperationIds.add(operationId);
|
||||
@@ -464,13 +464,13 @@ async function run() {
|
||||
const tsBodyParams =
|
||||
tsMeta?.params
|
||||
.filter((param) =>
|
||||
param.decorators.some((decorator) => decorator.name === 'Body'),
|
||||
param.decorators.some((decorator) => decorator.name === "Body"),
|
||||
)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
bodyKey:
|
||||
param.decorators.find((decorator) => decorator.name === 'Body')
|
||||
param.decorators.find((decorator) => decorator.name === "Body")
|
||||
?.firstArg ?? null,
|
||||
})) ?? [];
|
||||
|
||||
@@ -482,9 +482,9 @@ async function run() {
|
||||
tag: tags[0] ?? null,
|
||||
tags,
|
||||
summary:
|
||||
typeof operation.summary === 'string' ? operation.summary : null,
|
||||
typeof operation.summary === "string" ? operation.summary : null,
|
||||
description:
|
||||
typeof operation.description === 'string'
|
||||
typeof operation.description === "string"
|
||||
? operation.description
|
||||
: null,
|
||||
auth: {
|
||||
@@ -527,10 +527,10 @@ async function run() {
|
||||
tsMeta.controllerRoute,
|
||||
tsMeta.routePath,
|
||||
),
|
||||
tag: tsMeta.controller.replace(/Controller$/, ''),
|
||||
tags: [tsMeta.controller.replace(/Controller$/, '')],
|
||||
tag: tsMeta.controller.replace(/Controller$/, ""),
|
||||
tags: [tsMeta.controller.replace(/Controller$/, "")],
|
||||
summary: null,
|
||||
description: 'Not present in generated Swagger document',
|
||||
description: "Not present in generated Swagger document",
|
||||
auth: {
|
||||
swaggerSecurityRequired: null,
|
||||
swaggerSecuritySchemes: [],
|
||||
@@ -546,13 +546,13 @@ async function run() {
|
||||
body: null,
|
||||
tsBodyParams: tsMeta.params
|
||||
.filter((param) =>
|
||||
param.decorators.some((decorator) => decorator.name === 'Body'),
|
||||
param.decorators.some((decorator) => decorator.name === "Body"),
|
||||
)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
bodyKey:
|
||||
param.decorators.find((decorator) => decorator.name === 'Body')
|
||||
param.decorators.find((decorator) => decorator.name === "Body")
|
||||
?.firstArg ?? null,
|
||||
})),
|
||||
},
|
||||
@@ -569,19 +569,19 @@ async function run() {
|
||||
}
|
||||
|
||||
endpoints.sort((a, b) => {
|
||||
const pathA = typeof a.path === 'string' ? a.path : '';
|
||||
const pathB = typeof b.path === 'string' ? b.path : '';
|
||||
const pathA = typeof a.path === "string" ? a.path : "";
|
||||
const pathB = typeof b.path === "string" ? b.path : "";
|
||||
if (pathA !== pathB) {
|
||||
return pathA.localeCompare(pathB);
|
||||
}
|
||||
return (typeof a.method === 'string' ? a.method : '').localeCompare(
|
||||
typeof b.method === 'string' ? b.method : '',
|
||||
return (typeof a.method === "string" ? a.method : "").localeCompare(
|
||||
typeof b.method === "string" ? b.method : "",
|
||||
);
|
||||
});
|
||||
|
||||
const tagStats = new Map<string, number>();
|
||||
for (const endpoint of endpoints) {
|
||||
const tag = typeof endpoint.tag === 'string' ? endpoint.tag : 'Unknown';
|
||||
const tag = typeof endpoint.tag === "string" ? endpoint.tag : "Unknown";
|
||||
tagStats.set(tag, (tagStats.get(tag) ?? 0) + 1);
|
||||
}
|
||||
|
||||
@@ -591,7 +591,7 @@ async function run() {
|
||||
.body as Record<string, unknown> | null;
|
||||
if (requestBody && Array.isArray(requestBody.schemaRefs)) {
|
||||
for (const schemaName of requestBody.schemaRefs) {
|
||||
if (typeof schemaName === 'string') {
|
||||
if (typeof schemaName === "string") {
|
||||
referencedSchemas.add(schemaName);
|
||||
}
|
||||
}
|
||||
@@ -604,7 +604,7 @@ async function run() {
|
||||
continue;
|
||||
}
|
||||
for (const schemaName of status.schemaRefs) {
|
||||
if (typeof schemaName === 'string') {
|
||||
if (typeof schemaName === "string") {
|
||||
referencedSchemas.add(schemaName);
|
||||
}
|
||||
}
|
||||
@@ -626,16 +626,16 @@ async function run() {
|
||||
|
||||
const summary = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
generatedBy: 'src/scripts/export-swagger-endpoints-summary.ts',
|
||||
project: 'Suggest-Bet-BE',
|
||||
generatedBy: "src/scripts/export-swagger-endpoints-summary.ts",
|
||||
project: "Suggest-Bet-BE",
|
||||
swagger: {
|
||||
docsPath: '/api/docs',
|
||||
globalPrefix: '/api',
|
||||
docsPath: "/api/docs",
|
||||
globalPrefix: "/api",
|
||||
endpointCountInSwagger: endpoints.filter((item) => item.inSwagger).length,
|
||||
endpointCountTotal: endpoints.length,
|
||||
warnings: [
|
||||
'Swagger output reflects loaded modules for current environment.',
|
||||
'This export forces REDIS_ENABLED=true to include conditional Prediction endpoints.',
|
||||
"Swagger output reflects loaded modules for current environment.",
|
||||
"This export forces REDIS_ENABLED=true to include conditional Prediction endpoints.",
|
||||
],
|
||||
},
|
||||
stats: {
|
||||
@@ -668,7 +668,7 @@ async function run() {
|
||||
};
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
writeFileSync(outputFile, JSON.stringify(summary, null, 2), 'utf8');
|
||||
writeFileSync(outputFile, JSON.stringify(summary, null, 2), "utf8");
|
||||
|
||||
await app.close();
|
||||
|
||||
@@ -680,7 +680,7 @@ async function run() {
|
||||
}
|
||||
|
||||
void run().catch((error: unknown) => {
|
||||
console.error('❌ Failed to export Swagger endpoint summary');
|
||||
console.error("❌ Failed to export Swagger endpoint summary");
|
||||
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* Usage: npx ts-node --transpile-only -r tsconfig-paths/register src/scripts/populate-feature-store.ts
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -180,16 +180,16 @@ function buildFormIndex(
|
||||
|
||||
const homeResult =
|
||||
match.scoreHome > match.scoreAway
|
||||
? 'W'
|
||||
? "W"
|
||||
: match.scoreHome < match.scoreAway
|
||||
? 'L'
|
||||
: 'D';
|
||||
? "L"
|
||||
: "D";
|
||||
const awayResult =
|
||||
match.scoreAway > match.scoreHome
|
||||
? 'W'
|
||||
? "W"
|
||||
: match.scoreAway < match.scoreHome
|
||||
? 'L'
|
||||
: 'D';
|
||||
? "L"
|
||||
: "D";
|
||||
|
||||
homeState.results.unshift(homeResult);
|
||||
awayState.results.unshift(awayResult);
|
||||
@@ -222,14 +222,14 @@ function extractFormFeatures(formState: TeamFormState): {
|
||||
|
||||
let winStreak = 0;
|
||||
for (const r of formState.results) {
|
||||
if (r === 'W') winStreak++;
|
||||
if (r === "W") winStreak++;
|
||||
else break;
|
||||
}
|
||||
|
||||
// Form score: (W=3, D=1, L=0) over last 5, normalized to 0-100
|
||||
const last5Results = formState.results.slice(0, 5);
|
||||
const points = last5Results.reduce(
|
||||
(sum, r) => sum + (r === 'W' ? 3 : r === 'D' ? 1 : 0),
|
||||
(sum, r) => sum + (r === "W" ? 3 : r === "D" ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
const maxPoints = last5Results.length * 3 || 1;
|
||||
@@ -302,20 +302,20 @@ async function loadOddsIndex(): Promise<Map<string, OddsData>> {
|
||||
let bttsY = 0;
|
||||
|
||||
for (const s of selections) {
|
||||
if (s.cat === 'Maç Sonucu') {
|
||||
if (s.sel === '1') msH = s.odds;
|
||||
else if (s.sel === 'X' || s.sel === '0') msD = s.odds;
|
||||
else if (s.sel === '2') msA = s.odds;
|
||||
} else if (s.cat === 'Alt/Üst 2,5') {
|
||||
if (s.cat === "Maç Sonucu") {
|
||||
if (s.sel === "1") msH = s.odds;
|
||||
else if (s.sel === "X" || s.sel === "0") msD = s.odds;
|
||||
else if (s.sel === "2") msA = s.odds;
|
||||
} else if (s.cat === "Alt/Üst 2,5") {
|
||||
if (
|
||||
s.sel.toLowerCase().includes('üst') ||
|
||||
s.sel.toLowerCase().includes('over')
|
||||
s.sel.toLowerCase().includes("üst") ||
|
||||
s.sel.toLowerCase().includes("over")
|
||||
)
|
||||
ou25O = s.odds;
|
||||
} else if (s.cat === 'Karşılıklı Gol') {
|
||||
} else if (s.cat === "Karşılıklı Gol") {
|
||||
if (
|
||||
s.sel.toLowerCase().includes('var') ||
|
||||
s.sel.toLowerCase().includes('yes')
|
||||
s.sel.toLowerCase().includes("var") ||
|
||||
s.sel.toLowerCase().includes("yes")
|
||||
)
|
||||
bttsY = s.odds;
|
||||
}
|
||||
@@ -411,7 +411,7 @@ function buildLeagueIndex(matches: MatchRow[]): Map<string, LeagueStats> {
|
||||
const leagueMap = new Map<string, LeagueStats>();
|
||||
|
||||
for (const match of matches) {
|
||||
const key = match.leagueId ?? 'unknown';
|
||||
const key = match.leagueId ?? "unknown";
|
||||
let stats = leagueMap.get(key);
|
||||
if (!stats) {
|
||||
stats = { totalMatches: 0, totalGoals: 0, homeWins: 0, over25Count: 0 };
|
||||
@@ -520,15 +520,15 @@ async function populateFeatureStore(): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
console.log('🧠 Feature Store Population — Starting...');
|
||||
console.log('─'.repeat(60));
|
||||
console.log("🧠 Feature Store Population — Starting...");
|
||||
console.log("─".repeat(60));
|
||||
|
||||
// Load all finished football matches
|
||||
console.log('📥 Loading matches...');
|
||||
console.log("📥 Loading matches...");
|
||||
const rawMatches = await prisma.match.findMany({
|
||||
where: {
|
||||
sport: 'football',
|
||||
status: 'FT',
|
||||
sport: "football",
|
||||
status: "FT",
|
||||
scoreHome: { not: null },
|
||||
scoreAway: { not: null },
|
||||
homeTeamId: { not: null },
|
||||
@@ -543,7 +543,7 @@ async function populateFeatureStore(): Promise<void> {
|
||||
scoreAway: true,
|
||||
mstUtc: true,
|
||||
},
|
||||
orderBy: { mstUtc: 'asc' },
|
||||
orderBy: { mstUtc: "asc" },
|
||||
});
|
||||
|
||||
const matches: MatchRow[] = rawMatches.map((m) => ({
|
||||
@@ -559,31 +559,31 @@ async function populateFeatureStore(): Promise<void> {
|
||||
console.log(` 📊 Matches loaded: ${matches.length.toLocaleString()}`);
|
||||
|
||||
// Pre-compute all indexes
|
||||
console.log('\n📊 Building feature indexes...');
|
||||
console.log("\n📊 Building feature indexes...");
|
||||
|
||||
console.log(' 🏅 Pillar 1: Loading ELO ratings...');
|
||||
console.log(" 🏅 Pillar 1: Loading ELO ratings...");
|
||||
const eloMap = await loadEloMap();
|
||||
|
||||
console.log(' 📈 Pillar 2: Building form index...');
|
||||
console.log(" 📈 Pillar 2: Building form index...");
|
||||
const formIndex = buildFormIndex(matches);
|
||||
|
||||
console.log(' 💰 Pillar 3: Loading odds data...');
|
||||
console.log(" 💰 Pillar 3: Loading odds data...");
|
||||
const oddsIndex = await loadOddsIndex();
|
||||
|
||||
console.log(' ⚔️ Pillar 5: Building H2H index...');
|
||||
console.log(" ⚔️ Pillar 5: Building H2H index...");
|
||||
const h2hIndex = buildH2HIndex(matches);
|
||||
|
||||
console.log(' 📋 Pillar 6: Loading referee data...');
|
||||
console.log(" 📋 Pillar 6: Loading referee data...");
|
||||
const refereeIndex = await loadRefereeIndex(matches);
|
||||
|
||||
console.log(' 🏟️ Pillar 7: Building league DNA...');
|
||||
console.log(" 🏟️ Pillar 7: Building league DNA...");
|
||||
const leagueIndex = buildLeagueIndex(matches);
|
||||
|
||||
console.log('\n✅ All indexes built!');
|
||||
console.log('─'.repeat(60));
|
||||
console.log("\n✅ All indexes built!");
|
||||
console.log("─".repeat(60));
|
||||
|
||||
// Build feature vectors and batch upsert
|
||||
console.log('💾 Writing features to database...');
|
||||
console.log("💾 Writing features to database...");
|
||||
|
||||
const BATCH_SIZE = 1000;
|
||||
let processed = 0;
|
||||
@@ -651,7 +651,7 @@ async function populateFeatureStore(): Promise<void> {
|
||||
const refTotal = refStats?.totalMatches ?? 0;
|
||||
|
||||
// Pillar 7: League DNA
|
||||
const leagueKey = match.leagueId ?? 'unknown';
|
||||
const leagueKey = match.leagueId ?? "unknown";
|
||||
const leagueStats = leagueIndex.get(leagueKey) ?? {
|
||||
totalMatches: 1,
|
||||
totalGoals: 0,
|
||||
@@ -730,7 +730,7 @@ async function populateFeatureStore(): Promise<void> {
|
||||
),
|
||||
// Meta
|
||||
missingPlayersImpact: 0,
|
||||
calculatorVer: 'v2.0',
|
||||
calculatorVer: "v2.0",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -749,7 +749,7 @@ async function populateFeatureStore(): Promise<void> {
|
||||
}
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log('─'.repeat(60));
|
||||
console.log("─".repeat(60));
|
||||
console.log(`✅ Feature Store population complete!`);
|
||||
console.log(` Features written: ${processed.toLocaleString()}`);
|
||||
console.log(` Skipped: ${skipped}`);
|
||||
@@ -758,9 +758,9 @@ async function populateFeatureStore(): Promise<void> {
|
||||
// Verify
|
||||
const count = await prisma.footballAiFeature.count();
|
||||
console.log(` DB row count: ${count.toLocaleString()}`);
|
||||
console.log('─'.repeat(60));
|
||||
console.log("─".repeat(60));
|
||||
} catch (error) {
|
||||
console.error('❌ Feature store population failed:', error);
|
||||
console.error("❌ Feature store population failed:", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
process.env.PORT = process.env.PORT || '3005';
|
||||
process.env.AI_ENGINE_URL = process.env.AI_ENGINE_URL || 'http://127.0.0.1:8000';
|
||||
process.env.PORT = process.env.PORT || "3005";
|
||||
process.env.AI_ENGINE_URL =
|
||||
process.env.AI_ENGINE_URL || "http://127.0.0.1:8000";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require('./run-full-stack');
|
||||
require("./run-full-stack");
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from '../app.module';
|
||||
import { FeederService } from '../modules/feeder/feeder.service';
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "../app.module";
|
||||
import { FeederService } from "../modules/feeder/feeder.service";
|
||||
|
||||
async function bootstrap() {
|
||||
console.log('🏀 Bootstrapping Basketball Feeder...');
|
||||
console.log("🏀 Bootstrapping Basketball Feeder...");
|
||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||
logger: ['log', 'error', 'warn', 'debug', 'verbose'],
|
||||
logger: ["log", "error", "warn", "debug", "verbose"],
|
||||
});
|
||||
|
||||
const feederService = app.get(FeederService);
|
||||
|
||||
// Run ONLY for basketball
|
||||
// Adjust start date if needed, otherwise uses default
|
||||
await feederService.runHistoricalScan(['basketball']);
|
||||
await feederService.runHistoricalScan(["basketball"]);
|
||||
|
||||
console.log('✅ Basketball Feeder finished.');
|
||||
console.log("✅ Basketball Feeder finished.");
|
||||
await app.close();
|
||||
}
|
||||
|
||||
bootstrap().catch((err) => {
|
||||
console.error('❌ Basketball Feeder failed:', err);
|
||||
console.error("❌ Basketball Feeder failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
* Fetches matches only for leagues in top_leagues.json for the last ~2.5 seasons.
|
||||
*/
|
||||
|
||||
process.env.FEEDER_MODE = 'historical';
|
||||
process.env.FEEDER_MODE = "historical";
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from '../app.module';
|
||||
import { FeederService } from '../modules/feeder/feeder.service';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "../app.module";
|
||||
import { FeederService } from "../modules/feeder/feeder.service";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('FeederFilteredScript');
|
||||
logger.log('🚀 Starting Targeted Historical Feeder...');
|
||||
const logger = new Logger("FeederFilteredScript");
|
||||
logger.log("🚀 Starting Targeted Historical Feeder...");
|
||||
|
||||
// Read top_leagues.json
|
||||
const leaguesPath = path.join(process.cwd(), 'top_leagues.json');
|
||||
const leaguesPath = path.join(process.cwd(), "top_leagues.json");
|
||||
let targetLeagues: string[] = [];
|
||||
try {
|
||||
const data = fs.readFileSync(leaguesPath, 'utf8');
|
||||
const data = fs.readFileSync(leaguesPath, "utf8");
|
||||
targetLeagues = JSON.parse(data);
|
||||
// Deduplicate
|
||||
targetLeagues = [...new Set(targetLeagues)];
|
||||
@@ -34,21 +34,21 @@ async function bootstrap() {
|
||||
}
|
||||
|
||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||
logger: ['log', 'error', 'warn'],
|
||||
logger: ["log", "error", "warn"],
|
||||
});
|
||||
|
||||
try {
|
||||
const feederService = app.get(FeederService);
|
||||
// Start from 2023-07-01 to cover 2023-2024, 2024-2025, and current 2025-2026 seasons
|
||||
const START_DATE = '2023-07-01';
|
||||
const START_DATE = "2023-07-01";
|
||||
logger.log(`📅 Date Range: ${START_DATE} -> Today`);
|
||||
|
||||
await feederService.runHistoricalScan(
|
||||
['football'],
|
||||
["football"],
|
||||
START_DATE,
|
||||
targetLeagues,
|
||||
);
|
||||
logger.log('✅ Targeted Feeder completed successfully!');
|
||||
logger.log("✅ Targeted Feeder completed successfully!");
|
||||
} catch (error: any) {
|
||||
logger.error(`❌ Feeder failed: ${error.message}`);
|
||||
logger.error(error.stack);
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
* Usage: npm run feeder:historical
|
||||
*/
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { FeederService } from '../modules/feeder/feeder.service';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { FeederService } from "../modules/feeder/feeder.service";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
async function bootstrap() {
|
||||
process.env.FEEDER_MODE = 'historical';
|
||||
process.env.FEEDER_MODE = "historical";
|
||||
|
||||
const logger = new Logger('FeederScript');
|
||||
const logger = new Logger("FeederScript");
|
||||
|
||||
logger.log('🚀 Starting previous-day completed match sync...');
|
||||
logger.log("🚀 Starting previous-day completed match sync...");
|
||||
|
||||
// Load AppModule after FEEDER_MODE is set so cron imports can be disabled.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { AppModule } = require('../app.module');
|
||||
const { AppModule } = require("../app.module");
|
||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||
logger: ['log', 'error', 'warn'],
|
||||
logger: ["log", "error", "warn"],
|
||||
});
|
||||
|
||||
try {
|
||||
const feederService = app.get(FeederService);
|
||||
await feederService.runPreviousDayCompletedMatchesScan();
|
||||
logger.log('✅ Previous-day completed match sync completed successfully!');
|
||||
logger.log("✅ Previous-day completed match sync completed successfully!");
|
||||
} catch (error: any) {
|
||||
logger.error(`❌ Feeder failed: ${error.message}`);
|
||||
logger.error(error.stack);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { access } from 'node:fs/promises';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { access } from "node:fs/promises";
|
||||
import net from "node:net";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
interface ManagedProcess {
|
||||
readonly name: string;
|
||||
@@ -11,10 +11,10 @@ interface ManagedProcess {
|
||||
}
|
||||
|
||||
const ROOT_DIR = process.cwd();
|
||||
const AI_ENGINE_DIR = path.join(ROOT_DIR, 'ai-engine');
|
||||
loadEnvFile(path.join(ROOT_DIR, '.env'));
|
||||
const DEFAULT_AI_URL = process.env.AI_ENGINE_URL ?? 'http://127.0.0.1:8000';
|
||||
const DEFAULT_API_PORT = Number(process.env.PORT ?? '3005');
|
||||
const AI_ENGINE_DIR = path.join(ROOT_DIR, "ai-engine");
|
||||
loadEnvFile(path.join(ROOT_DIR, ".env"));
|
||||
const DEFAULT_AI_URL = process.env.AI_ENGINE_URL ?? "http://127.0.0.1:8000";
|
||||
const DEFAULT_API_PORT = Number(process.env.PORT ?? "3005");
|
||||
const AI_ENGINE_PORT = resolveAiPort(DEFAULT_AI_URL);
|
||||
const AI_START_TIMEOUT_MS = 120_000;
|
||||
const NEST_START_TIMEOUT_MS = 90_000;
|
||||
@@ -43,26 +43,26 @@ async function main(): Promise<void> {
|
||||
|
||||
const pythonCommand = await resolvePythonCommand();
|
||||
aiProcess = {
|
||||
name: 'ai-engine',
|
||||
name: "ai-engine",
|
||||
child: spawn(
|
||||
pythonCommand.command,
|
||||
[
|
||||
...pythonCommand.args,
|
||||
'-m',
|
||||
'uvicorn',
|
||||
'main:app',
|
||||
'--host',
|
||||
'0.0.0.0',
|
||||
'--port',
|
||||
"-m",
|
||||
"uvicorn",
|
||||
"main:app",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"--port",
|
||||
String(AI_ENGINE_PORT),
|
||||
...resolveAiExtraArgs(),
|
||||
],
|
||||
{
|
||||
cwd: AI_ENGINE_DIR,
|
||||
stdio: 'inherit',
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
PYTHONUNBUFFERED: '1',
|
||||
PYTHONUNBUFFERED: "1",
|
||||
PORT: String(AI_ENGINE_PORT),
|
||||
},
|
||||
},
|
||||
@@ -73,7 +73,7 @@ async function main(): Promise<void> {
|
||||
|
||||
log(`Waiting for AI engine health at ${aiHealthUrl}`);
|
||||
await waitForHealth(aiHealthUrl, AI_START_TIMEOUT_MS);
|
||||
log('AI engine is ready');
|
||||
log("AI engine is ready");
|
||||
}
|
||||
|
||||
const nestHealthUrl = `http://127.0.0.1:${DEFAULT_API_PORT}/api/health/live`;
|
||||
@@ -82,7 +82,7 @@ async function main(): Promise<void> {
|
||||
if (nestAlreadyHealthy) {
|
||||
log(`NestJS already running at ${nestHealthUrl}`);
|
||||
} else {
|
||||
const nestPortBusy = await isPortInUse('127.0.0.1', DEFAULT_API_PORT);
|
||||
const nestPortBusy = await isPortInUse("127.0.0.1", DEFAULT_API_PORT);
|
||||
if (nestPortBusy) {
|
||||
throw new Error(
|
||||
`NestJS port ${DEFAULT_API_PORT} is already in use but ${nestHealthUrl} is not healthy`,
|
||||
@@ -90,7 +90,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
nestProcess = {
|
||||
name: 'nest',
|
||||
name: "nest",
|
||||
child: spawnNestProcess(),
|
||||
};
|
||||
|
||||
@@ -98,54 +98,54 @@ async function main(): Promise<void> {
|
||||
|
||||
log(`Waiting for NestJS health at ${nestHealthUrl}`);
|
||||
await waitForHealth(nestHealthUrl, NEST_START_TIMEOUT_MS);
|
||||
log('NestJS is ready');
|
||||
log("NestJS is ready");
|
||||
}
|
||||
|
||||
log('Full stack is running');
|
||||
log("Full stack is running");
|
||||
}
|
||||
|
||||
function ensureWindowsOrUnixShellAwareness(): void {
|
||||
process.on('SIGINT', () => {
|
||||
void shutdown('SIGINT');
|
||||
process.on("SIGINT", () => {
|
||||
void shutdown("SIGINT");
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
void shutdown('SIGTERM');
|
||||
process.on("SIGTERM", () => {
|
||||
void shutdown("SIGTERM");
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error: Error) => {
|
||||
console.error('[full:run] Uncaught exception:', error);
|
||||
void shutdown('uncaughtException', 1);
|
||||
process.on("uncaughtException", (error: Error) => {
|
||||
console.error("[full:run] Uncaught exception:", error);
|
||||
void shutdown("uncaughtException", 1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason: unknown) => {
|
||||
console.error('[full:run] Unhandled rejection:', reason);
|
||||
void shutdown('unhandledRejection', 1);
|
||||
process.on("unhandledRejection", (reason: unknown) => {
|
||||
console.error("[full:run] Unhandled rejection:", reason);
|
||||
void shutdown("unhandledRejection", 1);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveNestStartScript(): string {
|
||||
return process.env.FULL_RUN_NEST_SCRIPT ?? 'start:dev';
|
||||
return process.env.FULL_RUN_NEST_SCRIPT ?? "start:dev";
|
||||
}
|
||||
|
||||
function resolveAiExtraArgs(): string[] {
|
||||
return process.env.FULL_RUN_AI_RELOAD === 'true' ? ['--reload'] : [];
|
||||
return process.env.FULL_RUN_AI_RELOAD === "true" ? ["--reload"] : [];
|
||||
}
|
||||
|
||||
function spawnNestProcess(): ChildProcess {
|
||||
const nestScript = resolveNestStartScript();
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return spawn('cmd.exe', ['/d', '/s', '/c', `npm run ${nestScript}`], {
|
||||
if (process.platform === "win32") {
|
||||
return spawn("cmd.exe", ["/d", "/s", "/c", `npm run ${nestScript}`], {
|
||||
cwd: ROOT_DIR,
|
||||
stdio: 'inherit',
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
return spawn('npm', ['run', nestScript], {
|
||||
return spawn("npm", ["run", nestScript], {
|
||||
cwd: ROOT_DIR,
|
||||
stdio: 'inherit',
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
@@ -160,15 +160,18 @@ async function resolvePythonCommand(): Promise<{
|
||||
}
|
||||
|
||||
const localVenvPython =
|
||||
process.platform === 'win32'
|
||||
? path.join(AI_ENGINE_DIR, 'venv', 'Scripts', 'python.exe')
|
||||
: path.join(AI_ENGINE_DIR, 'venv', 'bin', 'python');
|
||||
process.platform === "win32"
|
||||
? path.join(AI_ENGINE_DIR, "venv", "Scripts", "python.exe")
|
||||
: path.join(AI_ENGINE_DIR, "venv", "bin", "python");
|
||||
|
||||
if (await pathExists(localVenvPython)) {
|
||||
return { command: localVenvPython, args: [] };
|
||||
}
|
||||
|
||||
return { command: process.platform === 'win32' ? 'python' : 'python3', args: [] };
|
||||
return {
|
||||
command: process.platform === "win32" ? "python" : "python3",
|
||||
args: [],
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAiPort(aiUrl: string): number {
|
||||
@@ -178,7 +181,7 @@ function resolveAiPort(aiUrl: string): number {
|
||||
return Number(parsedUrl.port);
|
||||
}
|
||||
|
||||
return parsedUrl.protocol === 'https:' ? 443 : 80;
|
||||
return parsedUrl.protocol === "https:" ? 443 : 80;
|
||||
}
|
||||
|
||||
async function pathExists(targetPath: string): Promise<boolean> {
|
||||
@@ -196,21 +199,24 @@ function resolveHost(url: string): string {
|
||||
}
|
||||
|
||||
function attachExitHandlers(managedProcess: ManagedProcess): void {
|
||||
managedProcess.child.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
managedProcess.child.on(
|
||||
"exit",
|
||||
(code: number | null, signal: NodeJS.Signals | null) => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detail =
|
||||
signal !== null
|
||||
? `signal=${signal}`
|
||||
: `code=${code ?? 'unknown'}`;
|
||||
const detail =
|
||||
signal !== null ? `signal=${signal}` : `code=${code ?? "unknown"}`;
|
||||
|
||||
console.error(`[full:run] ${managedProcess.name} exited unexpectedly (${detail})`);
|
||||
void shutdown(`${managedProcess.name}-exit`, code ?? 1);
|
||||
});
|
||||
console.error(
|
||||
`[full:run] ${managedProcess.name} exited unexpectedly (${detail})`,
|
||||
);
|
||||
void shutdown(`${managedProcess.name}-exit`, code ?? 1);
|
||||
},
|
||||
);
|
||||
|
||||
managedProcess.child.on('error', (error: Error) => {
|
||||
managedProcess.child.on("error", (error: Error) => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
@@ -252,12 +258,12 @@ async function isPortInUse(host: string, port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.createConnection({ host, port });
|
||||
|
||||
socket.once('connect', () => {
|
||||
socket.once("connect", () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.once('error', () => {
|
||||
socket.once("error", () => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
@@ -283,7 +289,9 @@ async function shutdown(reason: string, exitCode = 0): Promise<void> {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
async function stopProcess(managedProcess: ManagedProcess | null): Promise<void> {
|
||||
async function stopProcess(
|
||||
managedProcess: ManagedProcess | null,
|
||||
): Promise<void> {
|
||||
if (!managedProcess) {
|
||||
return;
|
||||
}
|
||||
@@ -293,16 +301,21 @@ async function stopProcess(managedProcess: ManagedProcess | null): Promise<void>
|
||||
return;
|
||||
}
|
||||
|
||||
child.kill('SIGTERM');
|
||||
child.kill("SIGTERM");
|
||||
const stopped = await waitForProcessExit(child, 10_000);
|
||||
if (!stopped) {
|
||||
console.warn(`[full:run] ${name} did not stop gracefully, forcing termination`);
|
||||
child.kill('SIGKILL');
|
||||
console.warn(
|
||||
`[full:run] ${name} did not stop gracefully, forcing termination`,
|
||||
);
|
||||
child.kill("SIGKILL");
|
||||
await waitForProcessExit(child, 5_000);
|
||||
}
|
||||
}
|
||||
|
||||
function waitForProcessExit(child: ChildProcess, timeoutMs: number): Promise<boolean> {
|
||||
function waitForProcessExit(
|
||||
child: ChildProcess,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
@@ -316,10 +329,10 @@ function waitForProcessExit(child: ChildProcess, timeoutMs: number): Promise<boo
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
child.off('exit', onExit);
|
||||
child.off("exit", onExit);
|
||||
};
|
||||
|
||||
child.once('exit', onExit);
|
||||
child.once("exit", onExit);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,23 +342,23 @@ function log(message: string): void {
|
||||
|
||||
function loadEnvFile(envPath: string): void {
|
||||
try {
|
||||
const content = readFileSync(envPath, 'utf8');
|
||||
const content = readFileSync(envPath, "utf8");
|
||||
const lines = content.split(/\r?\n/u);
|
||||
|
||||
lines.forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const separatorIndex = trimmed.indexOf('=');
|
||||
const separatorIndex = trimmed.indexOf("=");
|
||||
if (separatorIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = trimmed.slice(0, separatorIndex).trim();
|
||||
const rawValue = trimmed.slice(separatorIndex + 1).trim();
|
||||
const normalizedValue = rawValue.replace(/^['"]|['"]$/gu, '');
|
||||
const normalizedValue = rawValue.replace(/^['"]|['"]$/gu, "");
|
||||
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = normalizedValue;
|
||||
@@ -357,6 +370,6 @@ function loadEnvFile(envPath: string): void {
|
||||
}
|
||||
|
||||
void main().catch((error: Error) => {
|
||||
console.error('[full:run] Startup failed:', error);
|
||||
void shutdown('startup-failed', 1);
|
||||
console.error("[full:run] Startup failed:", error);
|
||||
void shutdown("startup-failed", 1);
|
||||
});
|
||||
|
||||
@@ -1,37 +1,25 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from '../app.module';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { DataFetcherTask } from '../tasks/data-fetcher.task';
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "../app.module";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { DataFetcherTask } from "../tasks/data-fetcher.task";
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('LiveFeederShell');
|
||||
console.log('🚀 Starting Manual Live Feeder Update (Console)...');
|
||||
logger.log('🚀 Starting Manual Live Feeder Update...');
|
||||
const logger = new Logger("LiveFeederShell");
|
||||
console.log("🚀 Starting Manual Live Feeder Update (Console)...");
|
||||
logger.log("🚀 Starting Manual Live Feeder Update...");
|
||||
|
||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||
logger: ['log', 'error', 'warn'],
|
||||
logger: ["log", "error", "warn"],
|
||||
});
|
||||
|
||||
try {
|
||||
const dataFetcherTask = app.get(DataFetcherTask);
|
||||
|
||||
// 1. Fetch Soccer Matches (4-day full sync: today + 3 days ahead)
|
||||
logger.log('⚽ Fetching soccer live matches (4-day window)...');
|
||||
await dataFetcherTask.fetchLiveMatchesFull();
|
||||
// Run full sync (matches + live scores + odds + lineups)
|
||||
logger.log("⚽🏀 Running full live match sync...");
|
||||
await dataFetcherTask.syncLiveMatches();
|
||||
|
||||
// 2. Fetch Basketball Matches
|
||||
logger.log('🏀 Fetching basketball live matches...');
|
||||
await dataFetcherTask.fetchBasketballMatches();
|
||||
|
||||
// 3. Fetch Odds for all live matches
|
||||
logger.log('📊 Fetching odds for all live matches...');
|
||||
await dataFetcherTask.fetchOddsForPreMatches();
|
||||
|
||||
// 4. Fetch Lineups & Sidelined (NEW)
|
||||
logger.log('👕 Fetching lineups & sidelined for active matches...');
|
||||
await dataFetcherTask.updateLineupsAndSidelined();
|
||||
|
||||
logger.log('✅ Live Feeder update completed successfully!');
|
||||
logger.log("✅ Live Feeder update completed successfully!");
|
||||
} catch (error: any) {
|
||||
logger.error(`❌ Live Feeder failed: ${error.message}`);
|
||||
console.error(error.stack);
|
||||
|
||||
Reference in New Issue
Block a user