-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
analyticsAdvanced analytics featuresAdvanced analytics featuresenhancementNew feature or requestNew feature or requestphase-3Phase 3: Advanced FeaturesPhase 3: Advanced Features
Description
name: "Phase 3: Bet Correlation Analysis"
about: Identify correlated bets and warn against improper parlays
title: "[Phase 3] Bet Correlation Analysis"
labels: enhancement, phase-3, analytics, advanced
assignees: ''
Overview
Detect correlation between bets to help users avoid common mistakes (e.g., parlaying correlated outcomes) and identify hedging opportunities.
Business Value
- Parlay Protection: Warn users when combining correlated bets
- Education: Teach users about correlation
- Hedging: Identify natural hedging opportunities
- Portfolio Optimization: Better bankroll management
- Competitive Advantage: Few sportsbooks explain correlation
Technical Requirements
Database Changes
model BetCorrelation {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
// Correlated bets
bet1Id String @map("bet1_id") @db.Uuid
bet2Id String @map("bet2_id") @db.Uuid
// Correlation metrics
correlationType String @map("correlation_type") @db.VarChar(30) // same-game, derivative, inverse, temporal
correlationScore Decimal @map("correlation_score") @db.Decimal(5,4) // -1.0 to 1.0
confidence Decimal @db.Decimal(5,4) // 0.0 to 1.0
// Analysis
reasoning String? @db.Text
commonFactors Json? @map("common_factors") // [factor1, factor2, ...]
// Validation
historicalData Int? @map("historical_data") // Number of historical samples
pValue Decimal? @map("p_value") @db.Decimal(6,5) // Statistical significance
bet1 Bet @relation("bet1_correlations", fields: [bet1Id], references: [id])
bet2 Bet @relation("bet2_correlations", fields: [bet2Id], references: [id])
@@unique([bet1Id, bet2Id])
@@index([correlationType])
@@index([correlationScore])
@@map("bet_correlations")
}
model ParlayAnalysis {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
userId String @map("user_id") @db.Uuid
// Parlay details
betIds String[] @map("bet_ids") @db.Uuid[]
nominalOdds Int @map("nominal_odds") // Advertised parlay odds
trueOdds Int @map("true_odds") // Correlation-adjusted odds
oddsDiscrepancy Decimal @map("odds_discrepancy") @db.Decimal(5,2) // % difference
// Risk assessment
riskLevel String @map("risk_level") @db.VarChar(20) // low, medium, high
correlationFlags Json @map("correlation_flags") // [{bet1, bet2, type, score}]
// Recommendation
recommendation String @db.VarChar(30) // approve, warning, reject
reasoning String @db.Text
suggestedFix String? @map("suggested_fix") @db.Text
// Outcome (if placed)
placed Boolean @default(false)
actualResult String? @map("actual_result") @db.VarChar(20) // won, lost, push
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@index([createdAt])
@@index([riskLevel])
@@map("parlay_analyses")
}Backend Services
File: src/services/correlation.service.ts
class CorrelationService {
async analyzeCorrelation(bet1: Bet, bet2: Bet): Promise<Correlation>
async analyzePalray(bets: Bet[]): Promise<ParlayAnalysis>
async calculateTrueOdds(bets: Bet[]): Promise<number>
async findHedgingOpportunities(bet: Bet): Promise<Bet[]>
async getHistoricalCorrelation(betTypes: string[]): Promise<number>
}Correlation Detection Algorithm
Same-Game Correlation (most common):
function detectSameGameCorrelation(bet1: Bet, bet2: Bet): Correlation | null {
if (bet1.gameId !== bet2.gameId) return null;
// Example: Team spread + Team total
if (bet1.marketType === 'spreads' && bet2.marketType === 'totals') {
// Heavily correlated
return {
type: 'same-game',
score: 0.85,
reasoning: 'Team covering spread implies higher scoring',
factors: ['team_performance', 'scoring_pace']
};
}
// Example: Player prop + Team result
if (bet1.marketType.startsWith('player_') && bet2.marketType === 'h2h') {
if (bet1.selection.includes(bet2.selection)) {
return {
type: 'derivative',
score: 0.70,
reasoning: 'Player performance correlated with team win',
factors: ['player_team', 'win_probability']
};
}
}
// Example: Multiple player props same team
if (bet1.marketType.startsWith('player_') && bet2.marketType.startsWith('player_')) {
if (bet1.team === bet2.team) {
return {
type: 'same-game',
score: 0.50,
reasoning: 'Team performance affects all players',
factors: ['team_success', 'game_script']
};
}
}
return null;
}Derivative Correlation (one outcome implies another):
function detectDerivativeCorrelation(bet1: Bet, bet2: Bet): Correlation | null {
// Example: Team total OVER + Player points OVER
if (bet1.marketType === 'totals' && bet2.marketType === 'player_points') {
const teamTotal = bet1.point;
const playerTotal = bet2.point;
// If player needs 30+ points and team total is high
if (playerTotal >= 30 && teamTotal >= 220) {
return {
type: 'derivative',
score: 0.75,
reasoning: 'High team total suggests player scoring opportunity',
factors: ['scoring_environment', 'game_pace']
};
}
}
// Example: Spread + Moneyline
if (bet1.marketType === 'spreads' && bet2.marketType === 'h2h') {
if (bet1.selection === bet2.selection) {
const spread = Math.abs(bet1.point);
// Small spread = high correlation with moneyline
if (spread <= 3.0) {
return {
type: 'derivative',
score: 0.90,
reasoning: 'Small spread means moneyline and spread highly correlated',
factors: ['point_differential', 'win_probability']
};
}
}
}
return null;
}Inverse Correlation (mutually exclusive):
function detectInverseCorrelation(bet1: Bet, bet2: Bet): Correlation | null {
// Example: Team A moneyline + Team B moneyline (same game)
if (bet1.gameId === bet2.gameId &&
bet1.marketType === 'h2h' &&
bet2.marketType === 'h2h' &&
bet1.selection !== bet2.selection) {
return {
type: 'inverse',
score: -1.0, // Perfect negative correlation
reasoning: 'Mutually exclusive outcomes',
factors: ['game_result']
};
}
return null;
}Temporal Correlation (sequential games):
function detectTemporalCorrelation(bet1: Bet, bet2: Bet): Correlation | null {
// Example: Back-to-back games same team
if (bet1.team === bet2.team) {
const timeDiff = Math.abs(bet1.gameTime - bet2.gameTime);
// Within 48 hours = some correlation
if (timeDiff <= 48 * 60 * 60 * 1000) {
return {
type: 'temporal',
score: 0.40,
reasoning: 'Back-to-back games affect rest and performance',
factors: ['fatigue', 'schedule', 'travel']
};
}
}
return null;
}True Odds Calculation
Adjust parlay odds for correlation:
function calculateTrueOdds(bets: Bet[]): number {
// Start with nominal parlay odds
let trueOdds = bets.reduce((acc, bet) => acc * bet.odds, 1.0);
// Find all pairwise correlations
const correlations = [];
for (let i = 0; i < bets.length; i++) {
for (let j = i + 1; j < bets.length; j++) {
const corr = analyzeCorrelation(bets[i], bets[j]);
if (corr) correlations.push(corr);
}
}
// Apply correlation penalty
for (const corr of correlations) {
const penalty = calculateCorrelationPenalty(corr.score);
trueOdds *= penalty;
}
return trueOdds;
}
function calculateCorrelationPenalty(score: number): number {
// Positive correlation reduces true odds
if (score > 0) {
return 1.0 - (score * 0.30); // Up to 30% reduction
}
// Negative correlation increases true odds (rare)
if (score < 0) {
return 1.0 + (Math.abs(score) * 0.20); // Up to 20% increase
}
return 1.0; // No correlation
}
// Example:
// 2-leg parlay: Lakers -5.5 @ -110 + Lakers total OVER 110.5 @ -110
// Nominal odds: 2.64 (+264)
// Correlation score: 0.75
// Penalty: 1.0 - (0.75 * 0.30) = 0.775
// True odds: 2.64 * 0.775 = 2.05 (+105)
// Discrepancy: 28%Parlay Analysis
async function analyzeParlayRequest(bets: Bet[]): Promise<ParlayAnalysis> {
const correlations = [];
// Check all pairs
for (let i = 0; i < bets.length; i++) {
for (let j = i + 1; j < bets.length; j++) {
const corr = await analyzeCorrelation(bets[i], bets[j]);
if (corr && Math.abs(corr.score) > 0.30) {
correlations.push({
bet1: bets[i],
bet2: bets[j],
correlation: corr
});
}
}
}
// Calculate true odds
const nominalOdds = calculateNominalParlayOdds(bets);
const trueOdds = calculateTrueOdds(bets);
const discrepancy = ((nominalOdds / trueOdds) - 1) * 100;
// Determine risk level
let riskLevel = 'low';
if (correlations.some(c => c.correlation.score > 0.70)) {
riskLevel = 'high';
} else if (correlations.some(c => c.correlation.score > 0.50)) {
riskLevel = 'medium';
}
// Generate recommendation
let recommendation = 'approve';
let reasoning = 'No significant correlations detected';
if (riskLevel === 'high') {
recommendation = 'reject';
reasoning = `Strong correlation detected (${discrepancy.toFixed(1)}% odds discrepancy). Consider betting separately.`;
} else if (riskLevel === 'medium') {
recommendation = 'warning';
reasoning = `Moderate correlation detected (${discrepancy.toFixed(1)}% odds discrepancy). Proceed with caution.`;
}
return {
bets,
nominalOdds,
trueOdds,
discrepancy,
riskLevel,
correlations,
recommendation,
reasoning
};
}Hedging Opportunities
async function findHedgingOpportunities(bet: Bet): Promise<Bet[]> {
const opportunities = [];
// Find inverse correlated bets (same game, opposite side)
if (bet.marketType === 'h2h') {
const opposingSide = await findOppositeMoneyline(bet.gameId, bet.selection);
if (opposingSide) opportunities.push(opposingSide);
}
// Find derivative hedges (player props)
if (bet.marketType.startsWith('player_')) {
const relatedProps = await findRelatedPlayerProps(bet);
opportunities.push(...relatedProps);
}
// Find temporal hedges (live betting)
if (bet.status === 'pending' && isGameInProgress(bet.gameId)) {
const liveOdds = await getLiveOdds(bet.gameId);
const hedges = calculateOptimalHedge(bet, liveOdds);
opportunities.push(...hedges);
}
return opportunities;
}API Endpoints
POST /api/correlation/analyze- Analyze correlation between betsPOST /api/correlation/parlay- Analyze parlay for correlationGET /api/correlation/history- Historical correlation dataPOST /api/correlation/hedge- Find hedging opportunitiesGET /api/correlation/education- Educational content on correlation
Frontend Components
Parlay Validator: ParlayValidator.tsx
┌────────────────────────────────────────────────┐
│ 🔍 Parlay Correlation Check │
├────────────────────────────────────────────────┤
│ ⚠️ WARNING: Correlated Bets Detected │
│ │
│ Lakers -5.5 @ -110 │
│ Lakers OVER 110.5 @ -110 │
│ │
│ Correlation: 75% (Strong) │
│ Nominal Odds: +264 │
│ True Odds: +105 │
│ Discrepancy: 28% │
│ │
│ 📊 These bets are highly correlated because: │
│ • Same game outcomes │
│ • Lakers covering spread implies higher score│
│ • True probability lower than odds suggest │
│ │
│ Recommendation: Bet separately for better │
│ value, or reduce stake significantly. │
│ │
│ [PROCEED ANYWAY] [BET SEPARATELY] │
└────────────────────────────────────────────────┘
Correlation Heatmap: CorrelationHeatmap.tsx
- Visual matrix showing correlation between bet types
- Color-coded (green = low, yellow = medium, red = high)
- Click to see detailed explanation
- Historical correlation data
Hedge Calculator: HedgeCalculator.tsx
- Input: Original bet, current odds
- Output: Optimal hedge bet, guaranteed profit
- Support for partial hedging
- Scenario analysis (what-if)
Education Module: CorrelationEducation.tsx
- Interactive examples of correlation
- Common mistakes to avoid
- Correlation glossary
- Quiz to test knowledge
Real-time Parlay Validation
When user adds bet to bet slip:
- Check correlation with existing bets
- Show warning badge if correlation detected
- Update true odds in real-time
- Provide alternative suggestions
Warning Levels:
- 🟢 Low Risk: Correlation < 30%
- 🟡 Medium Risk: Correlation 30-60%
- 🔴 High Risk: Correlation > 60%
- ⛔ Prohibited: Inverse correlation (mutually exclusive)
Acceptance Criteria
- Database migration completed
- Correlation detection algorithms implemented
- Parlay analysis working for 2+ leg parlays
- True odds calculation accurate (within 5%)
- Hedging opportunity finder functional
- Parlay validator showing real-time warnings
- Correlation heatmap displays correctly
- Education module published
- API endpoints documented
- Unit tests for all correlation types
- Historical data validation
Dependencies
- Odds data with multiple bookmakers
- Historical game data for validation
- Player/team statistics
- Live odds (for hedging)
Estimated Effort
- Backend: 10 days
- Frontend: 7 days
- Testing & Validation: 5 days
- Total: 22 days
Success Metrics
- Warn users on 80%+ of correlated parlays
- True odds accuracy within 10% of actual results
- 50% of users accept recommendations
- Reduce correlation mistakes by 60%
- Hedge finder used 100+ times per week
Mathematical Foundation
Correlation Coefficient (Pearson's r):
r = Cov(X,Y) / (σ_X * σ_Y)
where:
X = outcome of bet 1
Y = outcome of bet 2
Cov = covariance
σ = standard deviation
Joint Probability (correlated events):
P(A ∩ B) = P(A) * P(B|A)
where:
P(B|A) = probability of B given A occurred
If independent: P(B|A) = P(B)
If correlated: P(B|A) ≠ P(B)
Parlay Expected Value (with correlation):
EV = (P_win * Payout) - (P_lose * Stake)
Without correlation:
P_win = P(A) * P(B)
With correlation:
P_win = P(A) * P(B|A) [usually lower]
Future Enhancements
- Machine learning for correlation prediction
- Sport-specific correlation models
- Dynamic correlation (changes during game)
- Correlation-adjusted parlay builder
- Social features (share parlay analysis)
- Correlation insurance (refund on bad-luck outcomes)
- Multi-book parlay optimization
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
analyticsAdvanced analytics featuresAdvanced analytics featuresenhancementNew feature or requestNew feature or requestphase-3Phase 3: Advanced FeaturesPhase 3: Advanced Features