diff --git a/src/db.js b/src/db.js index 8163638..f91061a 100644 --- a/src/db.js +++ b/src/db.js @@ -35,6 +35,7 @@ function initializeDatabase() { const hasPredictedRating = columns.some(col => col.name === 'predicted_rating'); const hasStdDev = columns.some(col => col.name === 'std_dev'); const hasExcludedRoundsCount = columns.some(col => col.name === 'excluded_rounds_count'); + const hasCutoffRating = columns.some(col => col.name === 'cutoff_rating'); if (!hasLastRoundUpdate) { logger.info('Adding last_round_update column to players table...'); @@ -67,6 +68,14 @@ function initializeDatabase() { else logger.info('Successfully added excluded_rounds_count column'); }); } + + if (!hasCutoffRating) { + logger.info('Adding cutoff_rating column to players table...'); + db.run(`ALTER TABLE players ADD COLUMN cutoff_rating INTEGER DEFAULT NULL`, (err) => { + if (err) logger.error('Error adding cutoff_rating column:', err.message); + else logger.info('Successfully added cutoff_rating column'); + }); + } }); }); diff --git a/src/models/player.js b/src/models/player.js index 1d63b28..733c138 100644 --- a/src/models/player.js +++ b/src/models/player.js @@ -172,11 +172,11 @@ function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) { }); } -function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null) { +function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null, cutoffRating = null) { return new Promise((resolve, reject) => { db.run( - 'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ? WHERE pdga_number = ?', - [predictedRating, stdDev, excludedRoundsCount, pdgaNumber], + 'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ?, cutoff_rating = ? WHERE pdga_number = ?', + [predictedRating, stdDev, excludedRoundsCount, cutoffRating, pdgaNumber], function(err) { if (err) reject(err); else resolve(); diff --git a/src/routes/players.js b/src/routes/players.js index bd0434b..85ca4d5 100644 --- a/src/routes/players.js +++ b/src/routes/players.js @@ -400,7 +400,7 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => { const result = calculatePredictedRating(roundsForPrediction); - await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount); + await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount, result.cutoffRating); const officialCount = allRounds.filter(r => r.source === 'official').length; const newCount = allRounds.filter(r => r.source === 'new').length; @@ -410,6 +410,7 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => { predictedRating: result.rating, stdDev: result.stdDev, excludedRoundsCount: result.excludedRoundsCount, + cutoffRating: result.cutoffRating, totalRounds: roundsForPrediction.length, officialRounds: officialCount, newRounds: newCount, diff --git a/src/services/player-service.js b/src/services/player-service.js index 4de0b5b..25bcb7d 100644 --- a/src/services/player-service.js +++ b/src/services/player-service.js @@ -41,11 +41,13 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } let predictedRating = cachedPlayer.predicted_rating; let stdDev = cachedPlayer.std_dev; let excludedRoundsCount = cachedPlayer.excluded_rounds_count; + let cutoffRating = cachedPlayer.cutoff_rating; if (!predictedRating || predictedRating === 0) { predictedRating = await getPredictedRatingFromDB(pdgaNumber); const updatedPlayer = await getPlayerFromDB(pdgaNumber); stdDev = updatedPlayer?.std_dev; excludedRoundsCount = updatedPlayer?.excluded_rounds_count; + cutoffRating = updatedPlayer?.cutoff_rating; } const rating = cachedPlayer.current_rating; @@ -68,6 +70,7 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } predictedRating: resolvedPredicted, stdDev: resolvedStdDev, excludedRoundsCount: (excludedRoundsCount != null && excludedRoundsCount >= 0) ? excludedRoundsCount : null, + cutoffRating: (cutoffRating != null && cutoffRating > 0) ? cutoffRating : null, lastMonthRating, // gap between next predicted update and current rating (null when either is missing) deltaPredicted: (resolvedPredicted != null && rating != null) ? resolvedPredicted - rating : null, @@ -148,7 +151,7 @@ async function getPredictedRatingFromDB(pdgaNumber) { const result = calculatePredictedRating(roundRatings); - await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount); + await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount, result.cutoffRating); return result.rating; } @@ -233,6 +236,7 @@ async function getAllRatingsFromDB(progressCallback = null) { predictedRating: null, stdDev: null, excludedRoundsCount: null, + cutoffRating: null, lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null, deltaPredicted: null, monthlyHistory: [], diff --git a/src/services/rating-calculator.js b/src/services/rating-calculator.js index a30a9c1..576427f 100644 --- a/src/services/rating-calculator.js +++ b/src/services/rating-calculator.js @@ -85,7 +85,7 @@ function calculatePredictedRating(roundRatings) { if (!roundRatings || roundRatings.length === 0) { debugLog.push('❌ No rounds provided for prediction'); - return { rating: 0, debugLog, excludedRoundsCount: null }; + return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null }; } debugLog.push(`📊 Starting with ${roundRatings.length} total rounds`); @@ -100,7 +100,7 @@ function calculatePredictedRating(roundRatings) { if (allSortedRounds.length === 0) { debugLog.push('❌ No valid rounds after filtering for update date'); - return { rating: 0, debugLog, excludedRoundsCount: null }; + return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null }; } debugLog.push(`📊 After update date filter: ${allSortedRounds.length} rounds`); @@ -127,7 +127,7 @@ function calculatePredictedRating(roundRatings) { if (eligibleRounds.length === 0) { debugLog.push('❌ No eligible rounds found'); - return { rating: 0, debugLog, excludedRoundsCount: null }; + return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null }; } debugLog.push(`📈 ELIGIBLE ROUNDS: ${eligibleRounds.length}`); @@ -138,6 +138,7 @@ function calculatePredictedRating(roundRatings) { let workingRounds = [...eligibleRounds]; let workingRatings = workingRounds.map(r => r.rating); let excludedRoundsCount = 0; + let cutoffRating = null; if (workingRatings.length >= 7) { debugLog.push('🔍 OUTLIER EXCLUSION (≥7 rounds available):'); @@ -162,6 +163,7 @@ function calculatePredictedRating(roundRatings) { const hundredPointOutliers = workingRatings.filter(rating => rating < hundredPointCutoff && rating >= stdDevCutoff); excludedRoundsCount = stdDevOutliers.length + hundredPointOutliers.length; + cutoffRating = Math.round(Math.max(stdDevCutoff, hundredPointCutoff)); if (stdDevOutliers.length > 0) { debugLog.push(` ❌ 2.5σ outliers removed: ${stdDevOutliers.length} rounds`); @@ -192,6 +194,7 @@ function calculatePredictedRating(roundRatings) { } else { debugLog.push(` ⚠️ Too few rounds after outlier removal (${filteredRatings.length}), keeping all rounds`); excludedRoundsCount = 0; + cutoffRating = null; } } else { debugLog.push(`⏭️ OUTLIER EXCLUSION SKIPPED (only ${workingRatings.length} rounds, need ≥7)`); @@ -232,7 +235,7 @@ function calculatePredictedRating(roundRatings) { debugLog.push(` Final Rating: ${finalRating}`); debugLog.push('=== END PDGA CALCULATION ==='); - return { rating: finalRating, stdDev: Math.round(stdDev), debugLog, excludedRoundsCount }; + return { rating: finalRating, stdDev: Math.round(stdDev), debugLog, excludedRoundsCount, cutoffRating }; } module.exports = { parseDate, getNextPDGAUpdateDate, calculatePredictedRating, calculateStandardDeviation }; diff --git a/views/partials/player-history.ejs b/views/partials/player-history.ejs index 62e532b..66db999 100644 --- a/views/partials/player-history.ejs +++ b/views/partials/player-history.ejs @@ -42,6 +42,12 @@ const chartPdgaNumber = hasPlayer ? player.pdgaNumber : pdgaNumber;
<%= player.excludedRoundsCount %>
<% } %> + <% if (player.cutoffRating != null && player.rating) { %> +
+
Cutoff rating
+
<%= player.cutoffRating %>
+
+ <% } %> <% } %> diff --git a/views/partials/ratings-cards.ejs b/views/partials/ratings-cards.ejs index d46e2aa..009de31 100644 --- a/views/partials/ratings-cards.ejs +++ b/views/partials/ratings-cards.ejs @@ -130,6 +130,12 @@ function renderSparkline(values, opts) {
<%= player.excludedRoundsCount %>
<% } %> + <% if (player.cutoffRating != null && player.rating) { %> +
+
Cutoff rating
+
<%= player.cutoffRating %>
+
+ <% } %>