Files
vyndr/src/services/parlayGrader.js
T
builtbykev 411cb6f196 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>
2026-03-21 12:45:15 -04:00

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 };