feat: Feature 2.1 — Parlay Scan with correlation detection + monetization
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>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user