Skip to content

[Phase 3] Bet Correlation Analysis #10

@WFord26

Description

@WFord26

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 bets
  • POST /api/correlation/parlay - Analyze parlay for correlation
  • GET /api/correlation/history - Historical correlation data
  • POST /api/correlation/hedge - Find hedging opportunities
  • GET /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:

  1. Check correlation with existing bets
  2. Show warning badge if correlation detected
  3. Update true odds in real-time
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    analyticsAdvanced analytics featuresenhancementNew feature or requestphase-3Phase 3: Advanced Features

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions