DEV Community

Cover image for Building a Sports Betting Calculator: The Math Behind Multi-Leg Arbitrage
Shoumya Chowdhury
Shoumya Chowdhury

Posted on

Building a Sports Betting Calculator: The Math Behind Multi-Leg Arbitrage

Building a Sports Betting Calculator: The Math Behind Multi-Leg Arbitrage

A deep dive into the complex mathematics powering accumulator betting calculators and arbitrage detection algorithms


Ever wondered how sports betting platforms calculate complex multi-leg accumulator odds in real-time? Or how arbitrage detection systems identify profitable opportunities across dozens of bookmakers simultaneously?

As developers, we often encounter fascinating mathematical challenges hidden within seemingly simple applications. Today, we're diving into one of the most computationally interesting problems in the betting industry: accumulator betting calculations and the algorithms that power modern matched betting tools.

The Mathematical Foundation

Understanding Accumulator Odds

An accumulator (or "acca") combines multiple individual bets into one, where all selections must win for the bet to succeed. The mathematics seem straightforward:

// Basic accumulator odds calculation
function calculateAccumulatorOdds(selections) {
    return selections.reduce((total, odds) => total * odds, 1);
}

const selections = [1.50, 1.80, 2.00, 1.90];
const combinedOdds = calculateAccumulatorOdds(selections);
console.log(combinedOdds); // 10.26
Enter fullscreen mode Exit fullscreen mode

But real-world implementations need to handle much more complexity:

class AccumulatorCalculator {
    constructor() {
        this.PRECISION_DECIMALS = 6;
        this.MAX_SELECTIONS = 20;
    }

    calculateOdds(selections, includeBoosts = false) {
        if (!Array.isArray(selections) || selections.length === 0) {
            throw new Error('Invalid selections array');
        }

        if (selections.length > this.MAX_SELECTIONS) {
            throw new Error(`Maximum ${this.MAX_SELECTIONS} selections allowed`);
        }

        const validatedOdds = selections.map(odds => {
            const numOdds = parseFloat(odds);
            if (isNaN(numOdds) || numOdds < 1.001) {
                throw new Error('Invalid odds: must be numeric and greater than 1.001');
            }
            return numOdds;
        });

        let combinedOdds = validatedOdds.reduce((total, odds) => {
            return this.preciseMath(total * odds);
        }, 1);

        if (includeBoosts) {
            combinedOdds = this.applyOddsBoosts(combinedOdds, selections.length);
        }

        return this.roundToPrecision(combinedOdds);
    }

    preciseMath(value) {
        return Math.round(value * Math.pow(10, this.PRECISION_DECIMALS)) / Math.pow(10, this.PRECISION_DECIMALS);
    }

    roundToPrecision(value, decimals = 2) {
        return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
    }

    applyOddsBoosts(odds, selectionCount) {
        const boostMapping = {
            5: 1.05,
            7: 1.10,
            10: 1.15
        };

        const applicableBoost = Object.keys(boostMapping)
            .reverse()
            .find(threshold => selectionCount >= parseInt(threshold));

        return applicableBoost ? odds * boostMapping[applicableBoost] : odds;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Arbitrage Detection Challenge

Understanding Matched Betting Mathematics

Matched betting exploits the mathematical differences between bookmaker odds and betting exchange rates. Here's where it gets algorithmically interesting:

class MatchedBettingCalculator {
    constructor() {
        this.DEFAULT_COMMISSION = 0.02;
    }

    calculateArbitrage(backOdds, layOdds, commission = this.DEFAULT_COMMISSION) {
        const backImpliedProbability = 1 / backOdds;
        const layImpliedProbability = 1 / layOdds;
        const adjustedLayProbability = layImpliedProbability * (1 + commission);
        const totalImpliedProbability = backImpliedProbability + adjustedLayProbability;

        if (totalImpliedProbability < 1) {
            return this.calculateOptimalStakes(backOdds, layOdds, commission);
        }

        return null;
    }

    calculateOptimalStakes(backOdds, layOdds, commission, totalStake = 100) {
        const backStake = totalStake / (1 + (backOdds / (layOdds - 1)));
        const layStake = (backStake * backOdds) / layOdds;
        const layLiability = layStake * (layOdds - 1);

        const backWinProfit = (backStake * backOdds) - totalStake - (layStake * commission);
        const layWinProfit = layStake - backStake - (layStake * commission);

        return {
            backStake: this.roundToPrecision(backStake),
            layStake: this.roundToPrecision(layStake),
            layLiability: this.roundToPrecision(layLiability),
            backWinProfit: this.roundToPrecision(backWinProfit),
            layWinProfit: this.roundToPrecision(layWinProfit),
            guaranteedProfit: this.roundToPrecision(Math.min(backWinProfit, layWinProfit))
        };
    }

    roundToPrecision(value, decimals = 2) {
        return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
    }
}
Enter fullscreen mode Exit fullscreen mode

Advanced ACCA Insurance Calculations

The Complex Multi-Scenario Problem

class ACCAInsuranceCalculator {
    constructor() {
        this.commission = 0.02;
        this.freeBetConversionRate = 0.75;
    }

    calculateInsuranceStrategy(selections, insuranceTerms, totalStake) {
        const combinedOdds = this.calculateCombinedOdds(selections);
        const scenarios = this.generateScenarios(selections.length);

        let optimalStrategy = null;
        let maxExpectedValue = -Infinity;

        for (let strategy of this.generateLayStrategies(selections)) {
            const ev = this.calculateExpectedValue(
                selections,
                strategy,
                insuranceTerms,
                totalStake
            );

            if (ev > maxExpectedValue) {
                maxExpectedValue = ev;
                optimalStrategy = strategy;
            }
        }

        return {
            strategy: optimalStrategy,
            expectedValue: maxExpectedValue,
            scenarios: this.calculateAllScenarios(selections, optimalStrategy, insuranceTerms, totalStake)
        };
    }

    generateScenarios(numSelections) {
        const scenarios = [];
        const totalCombinations = Math.pow(2, numSelections);

        for (let i = 0; i < totalCombinations; i++) {
            const scenario = [];
            for (let j = 0; j < numSelections; j++) {
                scenario.push((i >> j) & 1);
            }
            scenarios.push(scenario);
        }

        return scenarios;
    }

    calculateScenarioProbability(selections, scenario) {
        let probability = 1;
        for (let i = 0; i < selections.length; i++) {
            const impliedProb = 1 / selections[i].odds;
            probability *= scenario[i] ? impliedProb : (1 - impliedProb);
        }
        return probability;
    }

    calculateExpectedValue(selections, layStrategy, insuranceTerms, totalStake) {
        const scenarios = this.generateScenarios(selections.length);
        let expectedValue = 0;

        for (let scenario of scenarios) {
            const probability = this.calculateScenarioProbability(selections, scenario);
            const profit = this.calculateScenarioProfit(scenario, selections, layStrategy, insuranceTerms, totalStake);
            expectedValue += probability * profit;
        }

        return expectedValue;
    }

    calculateScenarioProfit(scenario, selections, layStrategy, insuranceTerms, totalStake) {
        const lossCount = scenario.filter(result => result === 0).length;
        let profit = 0;

        if (scenario.every(result => result === 1)) {
            const combinedOdds = selections.reduce((odds, sel) => odds * sel.odds, 1);
            profit += (totalStake * combinedOdds) - totalStake;
        } else if (lossCount === 1 && insuranceTerms.refundType === 'freeBet') {
            const refundValue = Math.min(totalStake, insuranceTerms.maxRefund);
            profit += refundValue * this.freeBetConversionRate;
        } else if (lossCount === 1 && insuranceTerms.refundType === 'cash') {
            profit += Math.min(totalStake, insuranceTerms.maxRefund);
        } else {
            profit -= totalStake;
        }

        for (let i = 0; i < selections.length; i++) {
            const layBet = layStrategy[i];
            if (scenario[i] === 0) {
                profit += layBet.stake * (1 - this.commission);
            } else {
                profit -= layBet.liability;
            }
        }

        return profit;
    }

    generateLayStrategies(selections) {
        const strategies = [];
        const equalStrategy = selections.map(sel => ({
            stake: 10,
            liability: 10 * (sel.layOdds - 1)
        }));
        strategies.push(equalStrategy);

        const totalImpliedProb = selections.reduce((sum, sel) => sum + (1/sel.odds), 0);
        const propStrategy = selections.map(sel => {
            const weight = (1/sel.odds) / totalImpliedProb;
            const stake = weight * 50;
            return {
                stake,
                liability: stake * (sel.layOdds - 1)
            };
        });
        strategies.push(propStrategy);

        return strategies;
    }
}
Enter fullscreen mode Exit fullscreen mode

Real-World Performance Optimizations

Handling High-Frequency Calculations

class OptimizedCalculationEngine {
    constructor() {
        this.calculationCache = new Map();
        this.updateQueue = [];
        this.batchSize = 100;
    }

    debouncedCalculate = this.debounce(this.performCalculations.bind(this), 50);

    queueCalculation(selectionId, odds, type) {
        this.updateQueue.push({ selectionId, odds, type, timestamp: Date.now() });
        this.debouncedCalculate();
    }

    performCalculations() {
        const batch = this.updateQueue.splice(0, this.batchSize);
        const groupedUpdates = this.groupUpdatesByType(batch);
        Object.keys(groupedUpdates).forEach(type => {
            this.processBatch(type, groupedUpdates[type]);
        });
    }

    memoizedCalculation(key, calculationFn, ...args) {
        if (this.calculationCache.has(key)) {
            const cached = this.calculationCache.get(key);
            if (Date.now() - cached.timestamp < 5000) {
                return cached.result;
            }
        }

        const result = calculationFn(...args);
        this.calculationCache.set(key, {
            result,
            timestamp: Date.now()
        });

        return result;
    }

    debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    calculateInWorker(data) {
        return new Promise((resolve, reject) => {
            const worker = new Worker('calculation-worker.js');
            worker.postMessage(data);

            worker.onmessage = (e) => {
                resolve(e.data);
                worker.terminate();
            };

            worker.onerror = (error) => {
                reject(error);
                worker.terminate();
            };

            setTimeout(() => {
                worker.terminate();
                reject(new Error('Calculation timeout'));
            }, 5000);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing Complex Betting Mathematics

const assert = require('assert');

describe('ACCA Insurance Calculator', () => {
    let calculator;

    beforeEach(() => {
        calculator = new ACCAInsuranceCalculator();
    });

    it('should calculate correct expected value for insurance offer', () => {
        const selections = [
            { odds: 1.50, layOdds: 1.52 },
            { odds: 1.80, layOdds: 1.82 },
            { odds: 2.00, layOdds: 2.02 },
            { odds: 1.90, layOdds: 1.92 }
        ];

        const insuranceTerms = {
            refundType: 'freeBet',
            maxRefund: 50
        };

        const result = calculator.calculateInsuranceStrategy(selections, insuranceTerms, 50);

        assert(result.expectedValue > 0, 'Should show positive expected value');
        assert(result.strategy.length === selections.length, 'Should have strategy for each selection');
    });

    it('should handle edge case of all selections at minimum odds', () => {
        const selections = Array(5).fill({ odds: 1.01, layOdds: 1.02 });

        const insuranceTerms = {
            refundType: 'cash',
            maxRefund: 100
        };

        assert.doesNotThrow(() => {
            calculator.calculateInsuranceStrategy(selections, insuranceTerms, 100);
        });
    });

    it('should correctly calculate scenario probabilities', () => {
        const selections = [
            { odds: 2.00 },
            { odds: 2.00 },
        ];

        const allWinScenario = [1, 1];
        const oneWinScenario = [1, 0];

        const allWinProb = calculator.calculateScenarioProbability(selections, allWinScenario);
        const oneWinProb = calculator.calculateScenarioProbability(selections, oneWinScenario);

        assert.strictEqual(allWinProb, 0.25);
        assert.strictEqual(oneWinProb, 0.25);
    });

    it('should handle large accumulator calculations efficiently', function(done) {
        this.timeout(1000);

        const selections = Array(15).fill().map((_, i) => ({
            odds: 1.5 + (i * 0.1),
            layOdds: 1.52 + (i * 0.1)
        }));

        const start = Date.now();
        calculator.calculateInsuranceStrategy(selections, { refundType: 'freeBet', maxRefund: 100 }, 100);
        const duration = Date.now() - start;

        assert(duration < 500, `Calculation took ${duration}ms, should be under 500ms`);
        done();
    });
});
Enter fullscreen mode Exit fullscreen mode

Real-World Implementation Reference

For developers looking to understand how these calculations work in practice, the ACCA Matched Betting Calculator provides an excellent real-world example. This tool demonstrates:

  • Multi-leg odds aggregation
  • Commission modeling
  • Free bet conversion rates
  • Scenario analysis
  • Real-time performance optimization

Industry Applications Beyond Betting

Financial Derivatives

  • Options pricing
  • Portfolio optimization
  • Risk modeling

Insurance Technology

  • Actuarial calculations
  • Premium setting
  • Claims analysis

Algorithmic Trading

  • Arbitrage strategies
  • Market making
  • Risk-adjusted returns

Conclusion

Building sophisticated betting calculators requires mastering complex mathematical algorithms, performance optimization, and real-time data processing. These principles apply far beyond gambling—touching fintech, insurance, and trading industries alike.

Top comments (0)