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