Skip to content

[Phase 2] Market Consensus & Deviations #7

@WFord26

Description

@WFord26

name: "Phase 2: Market Consensus Calculations"
about: Calculate market consensus and identify outlier bookmakers
title: "[Phase 2] Market Consensus Calculations"
labels: enhancement, phase-2, analytics
assignees: ''

Overview

Calculate true market consensus by aggregating all bookmaker lines, identifying outliers, and providing users with a single "market truth" reference point for each game.

Business Value

  • Value Identification: Find bookmakers offering lines significantly better than consensus
  • Market Efficiency: Understand when markets are efficient vs inefficient
  • Outlier Betting: Target bookmakers consistently offering best value
  • Fair Value Reference: Use consensus as baseline for bet evaluation

Technical Requirements

Database Changes

Already defined in Phase 1 Disagreement Detection, enhanced here:

model MarketConsensus {
  id                  String   @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  gameId              String   @map("game_id") @db.Uuid
  marketType          String   @map("market_type") @db.VarChar(20)
  calculatedAt        DateTime @default(now()) @map("calculated_at") @db.Timestamptz(6)
  
  // Consensus metrics
  consensusLine       Decimal  @map("consensus_line") @db.Decimal(5,1)
  consensusPrice      Int      @map("consensus_price")  // American odds
  medianLine          Decimal  @map("median_line") @db.Decimal(5,1)
  meanLine            Decimal  @map("mean_line") @db.Decimal(5,1)
  mode Line           Decimal? @map("mode_line") @db.Decimal(5,1)
  
  // Dispersion
  standardDeviation   Decimal  @map("standard_deviation") @db.Decimal(5,2)
  range               Decimal  @db.Decimal(5,2)  // max - min
  interquartileRange  Decimal  @map("interquartile_range") @db.Decimal(5,2)
  
  // Outliers
  outlierBookmakers   Json     @map("outlier_bookmakers")
  bestValueSide       String   @map("best_value_side") @db.VarChar(20)
  bestValueBookmaker  String   @map("best_value_bookmaker") @db.VarChar(50)
  bestValueLine       Decimal  @map("best_value_line") @db.Decimal(5,1)
  
  // Quality metrics
  bookmakerCount      Int      @map("bookmaker_count")
  sharpBookWeight     Decimal  @map("sharp_book_weight") @db.Decimal(5,2)
  disagreementScore   Int      @map("disagreement_score")
  
  game Game @relation(fields: [gameId], references: [id])
  
  @@index([gameId])
  @@index([marketType])
  @@index([calculatedAt])
  @@map("market_consensus")
}

Backend Services

File: src/services/market-consensus.service.ts

class MarketConsensusService {
  async calculateConsensus(gameId: string, marketType: string): Promise<Consensus>
  async identifyOutliers(gameId: string): Promise<Outlier[]>
  async findBestValue(gameId: string, side: string): Promise<BestLine>
  async trackConsensusMovement(gameId: string): Promise<Movement[]>
  async generateConsensusReport(): Promise<Report>
}

Calculation Methodology

Weighted Consensus (gives more weight to sharp books):

function calculateWeightedConsensus(odds: BookmakerOdds[]): number {
  const weights = {
    'pinnacle': 2.0,      // Sharp book - double weight
    'circa': 1.8,         // Sharp book
    'betcris': 1.5,       // Sharp book
    'draftkings': 1.0,    // Standard recreational book
    'fanduel': 1.0,
    'betmgm': 1.0,
    // ... other books default to 1.0
  };
  
  let weightedSum = 0;
  let totalWeight = 0;
  
  for (const odd of odds) {
    const weight = weights[odd.bookmaker] || 1.0;
    weightedSum += odd.line * weight;
    totalWeight += weight;
  }
  
  return weightedSum / totalWeight;
}

Median Consensus (robust against outliers):

function calculateMedianConsensus(odds: BookmakerOdds[]): number {
  const sorted = odds.map(o => o.line).sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  
  if (sorted.length % 2 === 0) {
    return (sorted[mid - 1] + sorted[mid]) / 2;
  }
  return sorted[mid];
}

Outlier Detection (Z-score method):

function detectOutliers(odds: BookmakerOdds[], consensus: number): Outlier[] {
  const stdDev = calculateStdDev(odds.map(o => o.line));
  const outliers = [];
  
  for (const odd of odds) {
    const zScore = Math.abs((odd.line - consensus) / stdDev);
    
    if (zScore > 2.0) {  // More than 2 standard deviations
      outliers.push({
        bookmaker: odd.bookmaker,
        line: odd.line,
        deviation: odd.line - consensus,
        zScore: zScore,
        category: zScore > 3.0 ? 'extreme' : 'moderate'
      });
    }
  }
  
  return outliers;
}

Consensus Quality Score

function calculateQualityScore(consensus: Consensus): number {
  let score = 100;
  
  // Deduct for low bookmaker count
  if (consensus.bookmakerCount < 5) {
    score -= (5 - consensus.bookmakerCount) * 10;
  }
  
  // Deduct for high standard deviation (disagreement)
  if (consensus.standardDeviation > 2.0) {
    score -= (consensus.standardDeviation - 2.0) * 10;
  }
  
  // Bonus for sharp book inclusion
  if (consensus.hasSharpBooks) {
    score += 10;
  }
  
  return Math.max(0, Math.min(100, score));
}

API Endpoints

  • GET /api/consensus/game/:gameId - Consensus for all markets on a game
  • GET /api/consensus/game/:gameId/:market - Specific market consensus
  • GET /api/consensus/outliers - All current outlier opportunities
  • GET /api/consensus/best-value - Best value lines vs consensus
  • GET /api/consensus/movement/:gameId - How consensus has moved over time
  • GET /api/consensus/bookmaker/:bookmaker/accuracy - How often a bookmaker's lines match final consensus

Frontend Components

Consensus Overlay: ConsensusLineOverlay.tsx

  • Show consensus line alongside bookmaker lines
  • Visual indicator of where each bookmaker sits
  • Highlight outliers
  • Show consensus confidence score
Consensus: -6.5 (Quality: 85/100)
─────────────────────────────────
Pinnacle    -6.0  ⬅ Best Value
DraftKings  -6.5  ✓ At Consensus
FanDuel     -7.0  
Circa       -6.5  ✓ At Consensus
BetMGM      -5.5  🔴 Outlier

Best Value Finder: BestValueBoard.tsx

  • List all games sorted by best value vs consensus
  • Show which bookmaker offers best line
  • Calculate edge percentage
  • One-click bet placement

Consensus Movement Chart: ConsensusMovementChart.tsx

  • Line chart of consensus over time
  • Annotate with major news/events
  • Show current vs opening consensus
  • Identify consensus "traps"

Value Calculation

Edge Calculation:

function calculateEdge(bookmak erLine: number, consensusLine: number, marketType: string): number {
  if (marketType === 'spread' || marketType === 'total') {
    // For spreads/totals, edge is in points
    return Math.abs(bookmakerLine - consensusLine);
  } else {
    // For moneylines, convert to implied probability
    const bookmakerProb = toImpliedProbability(bookmakerLine);
    const consensusProb = toImpliedProbability(consensusLine);
    return (bookmakerProb - consensusProb) * 100;  // % edge
  }
}

Acceptance Criteria

  • Database migration completed
  • Weighted and median consensus calculations
  • Outlier detection algorithm
  • Scheduled calculation every 10 minutes
  • Consensus overlay on game cards
  • Best value board page
  • Consensus movement charts
  • API endpoints documented
  • Quality score calculation
  • Unit tests for all calculation methods

Dependencies

  • Odds sync service with multiple bookmakers (minimum 5)
  • Bookmaker classification (sharp vs recreational)
  • Historical data for consensus accuracy validation

Estimated Effort

  • Backend: 5 days
  • Frontend: 4 days
  • Testing & Validation: 2 days
  • Total: 11 days

Success Metrics

  • Consensus calculated for 95%+ of games with odds
  • Quality score >80 for 70%+ of consensus calculations
  • Users find best value opportunities 10+ times per day
  • Outlier detection accuracy >90% (manual validation)

Future Enhancements

  • Machine learning for dynamic bookmaker weighting
  • Consensus prediction model
  • "Consensus trap" detection (when consensus is wrong)
  • Bookmaker accuracy leaderboard
  • Market efficiency score by sport/league

Metadata

Metadata

Assignees

No one assigned

    Labels

    analyticsAdvanced analytics featuresenhancementNew feature or requestphase-2Phase 2: Market Intelligence

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions