411cb6f196
POST /api/scan/parlay — authenticated parlay analysis: - Supabase JWT auth middleware (auth.getUser verification) - 5 correlation types detected between legs (same_game, same_team, same_player_conflicting, positive_correlation, blowout_cascade) - Overall parlay grading (A/B/C/D) with correlation penalty adjustments - Free tier: 5 scans/month, atomic scan count increment - Scan 5: full analysis + personalized upgrade pitch - Scan 6+: 403 block with upgrade pitch - Pitch personalization from scan history (top stats, grades, tier rec) - DB writes: picks + scan_sessions per scan 30 new tests, 158 total (131 Node.js + 27 Python), all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
64 lines
1.8 KiB
JavaScript
64 lines
1.8 KiB
JavaScript
function gradeParlayFromLegs(legResults, correlationFlags) {
|
|
if (legResults.length === 0) {
|
|
return { grade: 'D', confidence: 30, composite: 0 };
|
|
}
|
|
|
|
// Extract leg composites and grades
|
|
const legComposites = legResults.map((l) => l._composite || 0);
|
|
const legGrades = legResults.map((l) => l.grade);
|
|
const legConfidences = legResults.map((l) => l.confidence);
|
|
const dCount = legGrades.filter((g) => g === 'D').length;
|
|
|
|
// Average of leg composites
|
|
const legAvg = legComposites.reduce((a, b) => a + b, 0) / legComposites.length;
|
|
|
|
// Correlation penalties
|
|
let correlationPenalty = 0;
|
|
let hasMajorNegative = false;
|
|
for (const flag of correlationFlags) {
|
|
if (flag.impact === 'minor_negative') correlationPenalty -= 0.3;
|
|
if (flag.impact === 'major_negative') {
|
|
correlationPenalty -= 1.0;
|
|
hasMajorNegative = true;
|
|
}
|
|
}
|
|
|
|
const parlayComposite = legAvg + correlationPenalty;
|
|
|
|
// Grade assignment
|
|
let grade;
|
|
if (parlayComposite >= 2.5 && dCount === 0 && !hasMajorNegative) {
|
|
grade = 'A';
|
|
} else if (parlayComposite >= 1.5 && dCount <= 1) {
|
|
grade = 'B';
|
|
} else if (parlayComposite >= 0.5) {
|
|
grade = 'C';
|
|
} else {
|
|
grade = 'D';
|
|
}
|
|
|
|
// 2+ D legs forces grade D
|
|
if (dCount >= 2) grade = 'D';
|
|
|
|
// Major negative caps at B
|
|
if (hasMajorNegative && (grade === 'A')) {
|
|
grade = 'B';
|
|
}
|
|
|
|
// Confidence: average of legs, adjusted for correlations
|
|
let confidence = legConfidences.reduce((a, b) => a + b, 0) / legConfidences.length;
|
|
for (const flag of correlationFlags) {
|
|
if (flag.impact === 'minor_negative') confidence -= 5;
|
|
if (flag.impact === 'major_negative') confidence -= 15;
|
|
}
|
|
confidence = Math.max(30, Math.min(95, Math.round(confidence)));
|
|
|
|
return {
|
|
grade,
|
|
confidence,
|
|
composite: Math.round(parlayComposite * 100) / 100,
|
|
};
|
|
}
|
|
|
|
module.exports = { gradeParlayFromLegs };
|