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.