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
mkdir arb-calculator && cd arb-calculator
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install requestsStep 2: Create arb_calculator.py
# 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
export SPORTSGAMEODDS_KEY=your_api_key_here
python arb_calculator.pyExpected 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
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
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.909Why decimal? Makes arbitrage math easier.
3. Process Odds by Bookmaker
The API returns odds with a nested byBookmaker structure:
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
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
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) * 100Math 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
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
| Bookmaker | Market | Odds | Decimal | Implied Prob |
|---|---|---|---|---|
| FanDuel | Lakers -5.5 | +105 | 2.05 | 48.78% |
| DraftKings | Celtics +6.0 | +100 | 2.00 | 50.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
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:
# 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
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
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:
- Efficient markets - Bookmakers adjust quickly
- Not enough bookmakers - Need odds from 5+ books
- 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:
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']}"