Skip to content

Build an Arbitrage Calculator

Find guaranteed profit opportunities by identifying odds discrepancies across bookmakers.

What You’ll Build

A Python script that:

  • Scans all bookmakers for odds discrepancies
  • Calculates arbitrage opportunities
  • Shows optimal bet sizing
  • Calculates guaranteed profit percentage

Perfect for: Finding risk-free profit opportunities, comparing bookmaker odds, beating the vig

What is Arbitrage?

Arbitrage (or “arbing”) is betting on all possible outcomes across different bookmakers to guarantee profit regardless of the result.

Example:

  • Book A: Lakers -5.5 @ +105
  • Book B: Celtics +6.0 @ -105

By betting both sides across different books, you can sometimes lock in profit.

Prerequisites

  • Python 3.8+
  • SportsGameOdds API key (Get one free)
  • Basic Python knowledge

Complete Code

Step 1: Setup Project

bash
mkdir arb-calculator && cd arb-calculator
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install requests

Step 2: Create arb_calculator.py

python
# arb_calculator.py
import requests
import os
from collections import defaultdict

API_KEY = os.environ.get('SPORTSGAMEODDS_KEY')
API_BASE = 'https://api.sportsgameodds.com/v2'

def fetch_events(league='NBA'):
    """Fetch upcoming events with odds"""
    try:
        response = requests.get(
            f'{API_BASE}/events',
            params={
                'leagueID': league,
                'finalized': 'false',
                'oddsAvailable': 'true',
                'limit': 100
            },
            headers={'x-api-key': API_KEY}
        )
        response.raise_for_status()
        return response.json()['data']
    except requests.exceptions.RequestException as e:
        print(f'Error fetching events: {e}')
        return []

def american_to_decimal(american_odds):
    """Convert American odds to decimal odds"""
    if american_odds > 0:
        return (american_odds / 100) + 1
    else:
        return (100 / abs(american_odds)) + 1

def calculate_arbitrage(odds_list):
    """
    Calculate if arbitrage exists and profit percentage

    odds_list: List of decimal odds for all outcomes
    Returns: (has_arb, profit_percentage, stake_distribution)
    """
    # Calculate implied probability sum
    implied_prob_sum = sum(1 / odd for odd in odds_list)

    # Arbitrage exists when sum < 1 (overround is negative)
    has_arb = implied_prob_sum < 1

    if not has_arb:
        return False, 0, []

    # Calculate profit percentage
    profit_pct = ((1 / implied_prob_sum) - 1) * 100

    # Calculate optimal stake distribution (for $100 total)
    total_stake = 100
    stakes = [(total_stake / odd) / implied_prob_sum for odd in odds_list]

    return True, profit_pct, stakes

def find_arbitrage_opportunities(events):
    """Find arbitrage opportunities in events"""
    opportunities = []

    for event in events:
        # Get team names - API returns names in a nested structure
        away_name = event['teams']['away']['names']['long']
        home_name = event['teams']['home']['names']['long']
        matchup = f"{away_name} @ {home_name}"

        # Group odds by market type and side
        # Structure: markets[betType][periodID][side] = list of {bookmaker, price, line, decimal}
        markets = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))

        for odd_id, odd in (event.get('odds') or {}).items():
            bet_type = odd['betTypeID']
            side = odd['sideID']
            period_id = odd.get('periodID', 'game')

            # Only process full game odds for arbitrage
            if period_id != 'game':
                continue

            # Iterate through each bookmaker's odds
            for bookmaker_id, bookmaker_data in (odd.get('byBookmaker') or {}).items():
                # Skip if not available
                if not bookmaker_data.get('available', True):
                    continue

                # Get odds value (API returns as string)
                odds_str = bookmaker_data.get('odds')
                if not odds_str:
                    continue

                try:
                    price = int(odds_str)
                except (ValueError, TypeError):
                    continue

                # Get line value based on bet type
                if bet_type == 'sp':
                    line = bookmaker_data.get('spread')
                elif bet_type == 'ou':
                    line = bookmaker_data.get('overUnder')
                else:
                    line = None

                decimal_odds = american_to_decimal(price)

                markets[bet_type][period_id][side].append({
                    'bookmaker': bookmaker_id,
                    'american': price,
                    'decimal': decimal_odds,
                    'line': line
                })

        # Check each market for arbitrage
        for bet_type, periods in markets.items():
            for period_id, sides in periods.items():
                # For spread markets
                if bet_type == 'sp':
                    if sides.get('home') and sides.get('away'):
                        # Find best odds for each side
                        best_home = max(sides['home'], key=lambda x: x['decimal'])
                        best_away = max(sides['away'], key=lambda x: x['decimal'])

                        # Calculate arbitrage
                        has_arb, profit_pct, stakes = calculate_arbitrage([
                            best_home['decimal'],
                            best_away['decimal']
                        ])

                        if has_arb:
                            opportunities.append({
                                'matchup': matchup,
                                'market': 'spread',
                                'profit_pct': profit_pct,
                                'legs': [
                                    {
                                        'side': 'home',
                                        'bookmaker': best_home['bookmaker'],
                                        'odds': best_home['american'],
                                        'line': best_home['line'],
                                        'stake_pct': stakes[0]
                                    },
                                    {
                                        'side': 'away',
                                        'bookmaker': best_away['bookmaker'],
                                        'odds': best_away['american'],
                                        'line': best_away['line'],
                                        'stake_pct': stakes[1]
                                    }
                                ]
                            })

                # For total (over/under) markets
                elif bet_type == 'ou':
                    if sides.get('over') and sides.get('under'):
                        best_over = max(sides['over'], key=lambda x: x['decimal'])
                        best_under = max(sides['under'], key=lambda x: x['decimal'])

                        has_arb, profit_pct, stakes = calculate_arbitrage([
                            best_over['decimal'],
                            best_under['decimal']
                        ])

                        if has_arb:
                            opportunities.append({
                                'matchup': matchup,
                                'market': 'total',
                                'profit_pct': profit_pct,
                                'legs': [
                                    {
                                        'side': 'over',
                                        'bookmaker': best_over['bookmaker'],
                                        'odds': best_over['american'],
                                        'line': best_over['line'],
                                        'stake_pct': stakes[0]
                                    },
                                    {
                                        'side': 'under',
                                        'bookmaker': best_under['bookmaker'],
                                        'odds': best_under['american'],
                                        'line': best_under['line'],
                                        'stake_pct': stakes[1]
                                    }
                                ]
                            })

                # For moneyline markets
                elif bet_type == 'ml':
                    if sides.get('home') and sides.get('away'):
                        best_home = max(sides['home'], key=lambda x: x['decimal'])
                        best_away = max(sides['away'], key=lambda x: x['decimal'])

                        has_arb, profit_pct, stakes = calculate_arbitrage([
                            best_home['decimal'],
                            best_away['decimal']
                        ])

                        if has_arb:
                            opportunities.append({
                                'matchup': matchup,
                                'market': 'moneyline',
                                'profit_pct': profit_pct,
                                'legs': [
                                    {
                                        'side': 'home',
                                        'bookmaker': best_home['bookmaker'],
                                        'odds': best_home['american'],
                                        'stake_pct': stakes[0]
                                    },
                                    {
                                        'side': 'away',
                                        'bookmaker': best_away['bookmaker'],
                                        'odds': best_away['american'],
                                        'stake_pct': stakes[1]
                                    }
                                ]
                            })

    return opportunities

def display_opportunities(opportunities, total_stake=100):
    """Display arbitrage opportunities"""
    if not opportunities:
        print('No arbitrage opportunities found')
        print('\nNote: True arbitrage is rare. The market is usually efficient.')
        return

    # Sort by profit percentage (highest first)
    opportunities.sort(key=lambda x: x['profit_pct'], reverse=True)

    print(f'\nFound {len(opportunities)} ARBITRAGE OPPORTUNIT{"Y" if len(opportunities) == 1 else "IES"}!\n')

    for i, opp in enumerate(opportunities, 1):
        print('=' * 60)
        print(f'#{i} - {opp["matchup"]}')
        print(f'Market: {opp["market"].upper()}')
        print(f'Guaranteed Profit: {opp["profit_pct"]:.2f}%')
        print('-' * 60)

        for leg in opp['legs']:
            stake = total_stake * (leg['stake_pct'] / 100)
            payout = stake * american_to_decimal(leg['odds'])

            line_str = f" {leg['line']}" if leg.get('line') else ""

            print(f"  {leg['side'].upper()}{line_str} @ {leg['odds']:+d} ({leg['bookmaker']})")
            print(f"  Bet: ${stake:.2f} -> Payout: ${payout:.2f}")
            print()

        profit = total_stake * (opp['profit_pct'] / 100)
        print(f"Total Stake: ${total_stake:.2f}")
        print(f"Guaranteed Profit: ${profit:.2f}")
        print('=' * 60)
        print()

def main():
    print('Scanning for arbitrage opportunities...\n')

    # Scan multiple leagues
    leagues = ['NBA', 'NFL', 'NHL']
    all_opportunities = []

    for league in leagues:
        print(f'Fetching {league} events...')
        events = fetch_events(league)

        if events:
            print(f'Found {len(events)} {league} events')
            opportunities = find_arbitrage_opportunities(events)
            all_opportunities.extend(opportunities)
        else:
            print(f'No {league} events found')

        print()

    display_opportunities(all_opportunities, total_stake=100)

if __name__ == '__main__':
    main()

Step 3: Run It

bash
export SPORTSGAMEODDS_KEY=your_api_key_here
python arb_calculator.py

Expected Output

Scanning for arbitrage opportunities...

Fetching NBA events...
Found 8 NBA events

Fetching NFL events...
Found 14 NFL events

Fetching NHL events...
Found 6 NHL events

Found 2 ARBITRAGE OPPORTUNITIES!

============================================================
#1 - Boston Celtics @ Los Angeles Lakers
Market: SPREAD
Guaranteed Profit: 2.34%
------------------------------------------------------------
  HOME -5.5 @ +105 (fanduel)
  Bet: $48.78 -> Payout: $100.00

  AWAY +6.0 @ +100 (draftkings)
  Bet: $51.22 -> Payout: $102.44

Total Stake: $100.00
Guaranteed Profit: $2.34
============================================================

============================================================
#2 - Tampa Bay Lightning @ Florida Panthers
Market: TOTAL
Guaranteed Profit: 1.15%
------------------------------------------------------------
  OVER 6.0 @ -105 (betmgm)
  Bet: $51.22 -> Payout: $100.00

  UNDER 6.5 @ +110 (caesars)
  Bet: $48.78 -> Payout: $102.44

Total Stake: $100.00
Guaranteed Profit: $1.15
============================================================

How It Works

1. Fetch Events with Odds

python
response = requests.get(
    f'{API_BASE}/events',
    params={
        'leagueID': league,
        'finalized': 'false',  # Upcoming games only
        'oddsAvailable': 'true'  # Must have odds
    }
)

2. Convert American to Decimal Odds

python
def american_to_decimal(american_odds):
    if american_odds > 0:
        return (american_odds / 100) + 1  # e.g., +150 -> 2.50
    else:
        return (100 / abs(american_odds)) + 1  # e.g., -110 -> 1.909

Why decimal? Makes arbitrage math easier.

3. Process Odds by Bookmaker

The API returns odds with a nested byBookmaker structure:

python
for odd_id, odd in event.get('odds').items():
    bet_type = odd['betTypeID']  # 'sp', 'ml', 'ou'
    side = odd['sideID']         # 'home', 'away', 'over', 'under'

    # Each odd contains prices from multiple bookmakers
    for bookmaker_id, bookmaker_data in odd.get('byBookmaker', {}).items():
        price = int(bookmaker_data['odds'])
        decimal_odds = american_to_decimal(price)

4. Find Best Odds for Each Side

python
best_home = max(sides['home'], key=lambda x: x['decimal'])
best_away = max(sides['away'], key=lambda x: x['decimal'])

We want the highest odds (best payout) for each outcome.

5. Calculate Arbitrage

python
implied_prob_sum = sum(1 / odd for odd in [home_decimal, away_decimal])

if implied_prob_sum < 1:
    # Arbitrage exists!
    profit_pct = ((1 / implied_prob_sum) - 1) * 100

Math explained:

  • Normal market: implied probabilities sum to >100% (bookmaker edge)
  • Arbitrage: implied probabilities sum to <100% (your edge)

Example:

  • Home @ 2.10 (decimal) = 47.62% implied probability
  • Away @ 2.10 (decimal) = 47.62% implied probability
  • Sum = 95.24% < 100% -> 4.76% arbitrage!

6. Calculate Optimal Stakes

python
stakes = [(100 / odd) / implied_prob_sum for odd in odds_list]

This distributes your total stake to guarantee equal profit on all outcomes.

Real-World Example

Scenario: Lakers vs Celtics

BookmakerMarketOddsDecimalImplied Prob
FanDuelLakers -5.5+1052.0548.78%
DraftKingsCeltics +6.0+1002.0050.00%

Sum: 48.78% + 50.00% = 98.78%

Profit: (1 / 0.9878) - 1 = 1.23%

Stakes (for $100 total):

  • Lakers: $100 x (48.78 / 98.78) = $49.38
  • Celtics: $100 x (50.00 / 98.78) = $50.62

Outcome 1: Lakers cover -5.5

  • Win $49.38 x 2.05 = $101.23
  • Profit: $101.23 - $100 = $1.23

Outcome 2: Celtics cover +6.0

  • Win $50.62 x 2.00 = $101.24
  • Profit: $101.24 - $100 = $1.24

Guaranteed profit either way!

Enhancements

Add Minimum Profit Filter

python
MIN_PROFIT_PCT = 1.0  # Only show 1%+ opportunities

opportunities = [
    opp for opp in opportunities
    if opp['profit_pct'] >= MIN_PROFIT_PCT
]

Include Middle Opportunities

A “middle” is when you can win both bets:

python
# Lakers -5.5 @ Book A
# Celtics +6.0 @ Book B
# If Lakers win by exactly 6, you win BOTH bets!

def find_middles(sides):
    for home_odd in sides['home']:
        for away_odd in sides['away']:
            home_line = float(home_odd['line'] or 0)
            away_line = float(away_odd['line'] or 0)
            gap = abs(home_line) - abs(away_line)
            if gap >= 0.5:  # Middle exists
                yield {
                    'gap': gap,
                    'home': home_odd,
                    'away': away_odd
                }

Real-Time Monitoring

python
import time

def monitor_arbitrage(interval=60):
    """Check for arbitrage every minute"""
    while True:
        opportunities = find_all_arbitrage()
        display_opportunities(opportunities)
        time.sleep(interval)

monitor_arbitrage()

Send Alerts

python
import requests

def send_telegram_alert(opportunity):
    """Send Telegram notification"""
    bot_token = os.environ.get('TELEGRAM_BOT_TOKEN')
    chat_id = os.environ.get('TELEGRAM_CHAT_ID')

    message = f"Arbitrage Alert!\n"
    message += f"{opportunity['matchup']}\n"
    message += f"Profit: {opportunity['profit_pct']:.2f}%"

    requests.post(
        f'https://api.telegram.org/bot{bot_token}/sendMessage',
        json={'chat_id': chat_id, 'text': message}
    )

Important Considerations

Account Limitations

Warning: Bookmakers may limit or ban accounts that consistently arbitrage.

Tips to avoid detection:

  • Don’t bet only arbitrage opportunities
  • Vary bet sizes
  • Use round numbers ($50 not $49.38)
  • Space out bets across time
  • Use different devices/IPs

Execution Speed

Arbitrage opportunities disappear quickly (seconds to minutes).

Solutions:

  • Use our Real-time Streaming (AllStar plan)
  • Automate bet placement (requires bookmaker APIs)
  • Set up price alerts

Line Differences

In our example:

  • Book A: -5.5
  • Book B: +6.0

The 0.5 point difference creates the arbitrage opportunity (a “middle”).

Commission & Fees

Remember to account for:

  • Withdrawal fees
  • Currency conversion
  • Deposit bonuses with rollover requirements

Troubleshooting

“No arbitrage opportunities found”

Causes:

  1. Efficient markets - Bookmakers adjust quickly
  2. Not enough bookmakers - Need odds from 5+ books
  3. Wrong timing - Arbitrage appears closer to game time

Solutions:

  • Scan more leagues
  • Check closer to event start
  • Look for less popular markets

Negative profit calculation

Cause: Bug in odds conversion or stake calculation.

Solution: Add validation:

python
def validate_arbitrage(opportunity):
    """Verify calculations are correct"""
    total_stake = sum(leg['stake_pct'] for leg in opportunity['legs'])
    assert abs(total_stake - 100) < 0.01, "Stakes don't sum to 100%"

    # Verify profit on all outcomes
    for leg in opportunity['legs']:
        payout = leg['stake_pct'] * american_to_decimal(leg['odds'])
        profit = payout - 100
        assert profit > 0, f"Negative profit on {leg['side']}"