Multi-Sport Betting Analytics Dashboard

Lesson 1 of 2

Normalizing Sports Betting Odds Across Platforms

Estimated time: 10 minutes

Normalizing Sports Betting Odds Across Platforms

Odds Formats

Different platforms use different formats. You need to normalize to compare.

Decimal Odds (European)

Odds shown as a single number (e.g., 2.50). Payout = stake × decimal_odds.

function decimalToImpliedProb(odds) {
  return 1 / odds;
}

decimalToImpliedProb(2.50);  // 0.40 or 40%

Fractional Odds (UK)

Odds shown as a fraction (e.g., 3/2). Payout = stake × (numerator/denominator + 1).

function fractionalToDecimal(numerator, denominator) {
  return (numerator / denominator) + 1;
}

function fractionalToImpliedProb(numerator, denominator) {
  return denominator / (numerator + denominator);
}

fractionalToImpliedProb(3, 2);  // 0.40 or 40%

Moneyline Odds (US)

Odds shown as +/- number.

  • Negative: (-110 means you need to bet $110 to win $100)
  • Positive: (+150 means you bet $100 to win $150)
function moneylineToImpliedProb(moneyline) {
  if (moneyline < 0) {
    return Math.abs(moneyline) / (Math.abs(moneyline) + 100);
  } else {
    return 100 / (moneyline + 100);
  }
}

moneylineToImpliedProb(-110);  // 0.524 or 52.4%
moneylineToImpliedProb(+150);   // 0.400 or 40%

Platform-Specific Formats

Kalshi (Cents)

Kalshi prices are in cents (0-100). Convert to decimal odds:

function kalshiToDecimal(priceInCents) {
  const decimal = priceInCents / 100;
  return 1 / decimal;  // Implied prob
}

kalshiToDecimal(40);  // 2.50 decimal odds

Polymarket (Decimal)

Polymarket uses decimal probabilities (0.0-1.0) directly:

const polymarketOdds = 0.40;  // Already a probability
const decimalOdds = 1 / polymarketOdds;  // 2.50

Unified Aggregator

class OddsAggregator {
  normalize(odds, format) {
    switch (format) {
      case 'kalshi':
        return 1 / (odds.yes_price / 100);
      case 'polymarket':
        return 1 / odds.price;
      case 'decimal':
        return odds;
      case 'moneyline':
        return this.moneylineToDecimal(odds);
      default:
        throw new Error(`Unknown format: ${format}`);
    }
  }

  moneylineToDecimal(moneyline) {
    if (moneyline < 0) {
      return Math.abs(moneyline) / 100;
    } else {
      return (moneyline + 100) / 100;
    }
  }

  aggregateMarkets(markets) {
    return {
      yes_prices: markets.map(m => ({
        platform: m.platform,
        decimal_odds: this.normalize(m.odds, m.format),
        implied_prob: 1 / this.normalize(m.odds, m.format)
      })),
      average_price: markets.reduce((sum, m) => sum + this.normalize(m.odds, m.format), 0) / markets.length
    };
  }
}

const agg = new OddsAggregator();

const markets = [
  { platform: 'kalshi', odds: { yes_price: 40 }, format: 'kalshi' },
  { platform: 'polymarket', odds: { price: 0.40 }, format: 'polymarket' },
  { platform: 'traditional', odds: -110, format: 'moneyline' }
];

const normalized = agg.aggregateMarkets(markets);
console.log(normalized);

Detecting Line Movement

Line movement (odds changing over time) signals market activity:

class LineMovementDetector {
  constructor() {
    this.history = new Map();
  }

  record(marketId, odds, timestamp) {
    if (!this.history.has(marketId)) {
      this.history.set(marketId, []);
    }
    this.history.get(marketId).push({ odds, timestamp });
  }

  getMovement(marketId) {
    const records = this.history.get(marketId);
    if (!records || records.length < 2) return null;

    const newest = records[records.length - 1];
    const oldest = records[0];

    const movement = ((newest.odds - oldest.odds) / oldest.odds) * 100;
    return {
      movement_pct: movement,
      direction: movement > 0 ? 'up' : 'down',
      volatility: this.calculateVolatility(records)
    };
  }

  calculateVolatility(records) {
    const odds = records.map(r => r.odds);
    const mean = odds.reduce((a, b) => a + b) / odds.length;
    const variance = odds.reduce((sum, o) => sum + Math.pow(o - mean, 2), 0) / odds.length;
    return Math.sqrt(variance);
  }
}

Next: Real-Time Aggregation

Combine normalization with live streaming to build a unified dashboard showing odds across all major sports books.