Building an arbitrage finder means your math must be perfect. Even a small rounding error or logic flaw can turn a supposed profit into a loss. This guide shows you how to unit-test arbitrage math in code, ensuring your calculations are robust and reliable. We'll use Python examples, leveraging static pre-match football odds JSON to validate your arbitrage detection logic.
Arbitrage betting relies on finding discrepancies in odds across different bookmakers. You buy low and sell high, locking in a guaranteed profit regardless of the outcome. The core of any arbitrage system is its calculation. It needs rigorous testing. You can't rely on volatile live data. Instead, a controlled, repeatable environment with static data is essential to unit-test arbitrage math in code explained. A reliable UK bookmaker odds API provides the consistent data structure you need, offering an odds API without scraping. This approach allows you to thoroughly vet your algorithms before deploying them to handle real-world pre-match football odds. For examples of the data structure, see our API examples.
Prerequisites for Testing Arbitrage Logic
Before you dive into writing unit tests for your arbitrage calculations, ensure you have the right tools and a foundational understanding of the concepts involved. This setup provides a stable environment for developing and validating your code.
Here's what you'll need:
- Python 3.8+: The programming language for our examples.
pytestlibrary: A popular and powerful testing framework for Python. Install it via pip install pytest.- A set of static pre-match football odds JSON data: This can be mocked data or real snapshots from a UK bookmaker odds API. It's crucial for repeatable tests.
- Basic understanding of decimal odds and implied probability: Knowing how these relate to arbitrage calculations is fundamental.
Having these prerequisites in place will allow you to focus on the core logic of how to unit-test arbitrage math in code effectively.
Step 1: Define Your Arbitrage Calculation Function
The first step in how to unit-test arbitrage math in code is to encapsulate your core logic into testable functions. For arbitrage, this primarily involves calculating the implied probability for each outcome and summing them up. If the sum of these implied probabilities is less than 1.0 (for decimal odds), an arbitrage opportunity exists. We'll also need functions to calculate the optimal stakes and the potential profit.
Create a file named arbitrage_calculator.py and add the following Python code:
# arbitrage_calculator.py
def calculate_arbitrage_percentage(odds: list[float]) -> float:
"""
Calculates the arbitrage percentage for a given set of decimal odds.
A value < 1.0 indicates an arbitrage opportunity.
"""
if not odds:
raise ValueError("Odds list cannot be empty.")
implied_probabilities = [1 / odd for odd in odds]
total_implied_probability = sum(implied_probabilities)
return total_implied_probability
def calculate_arbitrage_profit(stakes: list[float], odds: list[float], total_stake: float) -> float:
"""
Calculates the guaranteed profit from an arbitrage opportunity.
Assumes stakes are already calculated to ensure equal returns.
"""
if not stakes or not odds or len(stakes) != len(odds):
raise ValueError("Stakes and odds lists must be non-empty and equal length.")
# Calculate total return from any outcome
# For arbitrage, all returns should be equal, so we pick the first one
# We use the total_stake / arbitrage_percentage to get the implied return
# This avoids potential floating point issues if stakes * odds is slightly off
arbitrage_percentage = calculate_arbitrage_percentage(odds)
if arbitrage_percentage >= 1.0:
return 0.0 # No profit if no arbitrage
implied_return = total_stake / arbitrage_percentage
return implied_return - total_stake
def calculate_arbitrage_stakes(odds: list[float], total_stake: float) -> list[float]:
"""
Calculates the optimal stakes for each selection in an arbitrage opportunity.
Returns a list of stakes for each odd.
"""
if not odds:
raise ValueError("Odds list cannot be empty.")
arbitrage_percentage = calculate_arbitrage_percentage(odds)
if arbitrage_percentage >= 1.0:
return [] # No arbitrage opportunity, so no stakes to calculate
stakes = []
for odd in odds:
stake = (1 / odd / arbitrage_percentage) * total_stake
stakes.append(stake)
return stakes
This Python module, arbitrage_calculator.py, provides the core functions to detect and size arbitrage opportunities. The calculate_arbitrage_percentage function takes a list of decimal odds and returns the sum of their implied probabilities. If this sum is less than 1.0, an arbitrage opportunity exists. The calculate_arbitrage_stakes function then determines how much to bet on each outcome to guarantee a profit, given a total stake. Finally, calculate_arbitrage_profit confirms the profit based on the calculated implied return, which is derived from the total stake and the arbitrage percentage. This modular approach makes each piece of logic independently testable.

Step 2: Prepare Static Pre-Match Football Odds JSON Data
For how to unit-test arbitrage math in code explained, using static data is non-negotiable. Real-time odds fluctuate constantly, making your tests unreliable and non-deterministic. A static dataset ensures that your tests are repeatable and that any failures are due to changes in your code, not in the external data. This approach is fundamental to robust unit testing.
You can either create a simple mock JSON file or capture a snapshot from a UK bookmaker odds API. Here's an example of a simplified pre-match football odds JSON structure that you might use for testing. This data represents odds for a "Match Winner" market from different bookmakers.
{
"event_id": "EVT123456",
"event_title": "Man Utd vs Liverpool",
"kickoff_utc": "2026-04-29T19:00:00Z",
"markets": [
{
"market_name": "Match Winner",
"selections": [
{"selection_name": "Man Utd", "odds": 2.50, "bookmaker_code": "UO001"},
{"selection_name": "Draw", "odds": 3.40, "bookmaker_code": "UO002"},
{"selection_name": "Liverpool", "odds": 3.00, "bookmaker_code": "UO003"}
]
},
{
"market_name": "Match Winner",
"selections": [
{"selection_name": "Man Utd", "odds": 2.60, "bookmaker_code": "UO004"},
{"selection_name": "Draw", "odds": 3.30, "bookmaker_code": "UO005"},
{"selection_name": "Liverpool", "odds": 2.90, "bookmaker_code": "UO006"}
]
}
]
}
This JSON snippet represents pre-match football odds for a single event, sourced from multiple bookmakers. Notice how the "Match Winner" market appears twice, but from different bookmakers, potentially offering varying odds for the same outcome. This structure is typical of what you'd get from a UK bookmaker odds API. For unit testing, you'd save this or similar data as a static file (e.g., test_data.json) or embed it directly in your test suite. This ensures repeatable test runs without external dependencies or the need to constantly fetch new data. The goal is to isolate your math logic from the data fetching mechanism, allowing you to thoroughly test the calculations themselves.
Step 3: Write Unit Tests for Arbitrage Math
Now that your core arbitrage logic is in functions and you have static data, it's time to write the actual unit tests. We'll use pytest for this. The key to how to unit-test arbitrage math in code integration is to cover various scenarios: positive arbitrage, negative (no arbitrage), and edge cases. Remember to use pytest.approx when comparing floating-point numbers to account for precision issues.
Create a file named test_arbitrage_calculator.py in the same directory as arbitrage_calculator.py:
# test_arbitrage_calculator.py
import pytest
from arbitrage_calculator import (
calculate_arbitrage_percentage,
calculate_arbitrage_stakes,
calculate_arbitrage_profit
)
# Test cases for calculate_arbitrage_percentage
@pytest.mark.parametrize("odds, expected_percentage", [
# No arbitrage opportunity (sum > 1)
([2.10, 3.50, 4.00], 1.0119047619047619),
([2.00, 2.00, 2.00], 1.5),
([2.50, 3.40, 3.00], 1.0274509803921568),
# Arbitrage opportunity (sum < 1)
# Example: 1/2.20 + 1/3.60 + 1/4.50 = 0.45454545 + 0.27777777 + 0.22222222 = 0.95454544
([2.20, 3.60, 4.50], 0.9545454545454544),
])
def test_calculate_arbitrage_percentage(odds, expected_percentage):
actual_percentage = calculate_arbitrage_percentage(odds)
assert actual_percentage == pytest.approx(expected_percentage, rel=1e-6)
def test_calculate_arbitrage_percentage_empty_odds():
with pytest.raises(ValueError, match="Odds list cannot be empty."):
calculate_arbitrage_percentage([])
# Test cases for calculate_arbitrage_stakes
@pytest.mark.parametrize("odds, total_stake, expected_stakes", [
# Arbitrage opportunity: odds = [2.20, 3.60, 4.50], total_stake = 100
# arb_percentage = 0.9545454545454544
# Stake 1 = (1/2.20 / arb_percentage) * 100 = 47.61904761904761
# Stake 2 = (1/3.60 / arb_percentage) * 100 = 29.1005291005291
# Stake 3 = (1/4.50 / arb_percentage) * 100 = 23.28042328042328
([2.20, 3.60, 4.50], 100.0, [47.61904761904761, 29.1005291005291, 23.28042328042328]),
# No arbitrage opportunity, should return empty list
([2.50, 3.40, 3.00], 100.0, []),
])
def test_calculate_arbitrage_stakes(odds, total_stake, expected_stakes):
actual_stakes = calculate_arbitrage_stakes(odds, total_stake)
assert len(actual_stakes) == len(expected_stakes)
for actual, expected in zip(actual_stakes, expected_stakes):
assert actual == pytest.approx(expected, rel=1e-6)
def test_calculate_arbitrage_stakes_empty_odds():
with pytest.raises(ValueError, match="Odds list cannot be empty."):
calculate_arbitrage_stakes([], 100.0)
# Test cases for calculate_arbitrage_profit
@pytest.mark.parametrize("stakes, odds, total_stake, expected_profit", [
# Arbitrage opportunity: odds = [2.20, 3.60, 4.50], total_stake = 100
# arb_percentage = 0.9545454545454544
# Implied return = 100 / arb_percentage = 104.76190476190476
# Profit = Implied return - total_stake = 4.761904761904761
([47.61904761904761, 29.1005291005291, 23.28042328042328], [2.20, 3.60, 4.50], 100.0, 4.761904761904761),
# No arbitrage opportunity, should return 0.0 profit
([33.33, 33.33, 33.33], [2.50, 3.40, 3.00], 100.0, 0.0),
])
def test_calculate_arbitrage_profit(stakes, odds, total_stake, expected_profit):
actual_profit = calculate_arbitrage_profit(stakes, odds, total_stake)
assert actual_profit == pytest.approx(expected_profit, rel=1e-6)
def test_calculate_arbitrage_profit_invalid_input():
with pytest.raises(ValueError, match="Stakes and odds lists must be non-empty and equal length."):
calculate_arbitrage_profit([], [2.0], 100.0)
with pytest.raises(ValueError, match="Stakes and odds lists must be non-empty and equal length."):
calculate_arbitrage_profit([50.0], [], 100.0)
with pytest.raises(ValueError, match="Stakes and odds lists must be non-empty and equal length."):
calculate_arbitrage_profit([50.0], [2.0, 3.0], 100.0)
This test_arbitrage_calculator.py file demonstrates how to unit-test arbitrage math in code integration using pytest. We use @pytest.mark.parametrize to run the same test logic with various inputs, covering cases with and without arbitrage opportunities, as well as edge cases like empty input lists. The pytest.approx assertion is crucial for floating-point comparisons, accounting for potential precision issues that are common in financial calculations. By providing diverse pre-match football odds JSON data (even if simplified for testing), you ensure your arbitrage logic handles real-world scenarios correctly and consistently. Running these tests frequently helps catch regressions early in the development cycle.
To run these tests, navigate to the directory containing both Python files in your terminal and execute:
pytest
You should see output indicating that all tests passed, confirming the accuracy of your arbitrage math.

Step 4: Integrate with a UK Bookmaker Odds API for Data
While unit tests validate your core math with static data, real-world applications need actual odds. This is where an odds API without scraping becomes invaluable. For more comprehensive testing (often called integration testing), you'll want to fetch real pre-match football odds JSON from a reliable source. The UK Odds API provides structured, normalised data from many UK bookmakers, perfect for this purpose.
Here's how you can fetch data from ukoddsapi.com and integrate it with your arbitrage detection logic. This script will retrieve event data, then fetch detailed odds for a specific event, and finally attempt to find arbitrage opportunities within that data. For full API documentation, refer to api.ukoddsapi.com/docs.
import os
import requests
import json
from arbitrage_calculator import (
calculate_arbitrage_percentage,
calculate_arbitrage_stakes,
calculate_arbitrage_profit
)
# Replace with your actual API key or environment variable
API_KEY = os.environ.get("UKODDSAPI_KEY", "YOUR_API_KEY")
BASE_URL = "https://api.ukoddsapi.com"
def fetch_prematch_football_odds(event_id: str) -> dict:
"""Fetches pre-match football odds for a specific event from UK Odds API."""
headers = {"X-Api-Key": API_KEY}
params = {"package": "full", "odds_format": "decimal"} # Use 'full' for broader market coverage
response = requests.get(
f"{BASE_URL}/v1/football/events/{event_id}/odds",
headers=headers,
params=params,
timeout=60
)
response.raise_for_status() # Raise an exception for HTTP errors
return response.json()
def find_arbitrage_in_event(event_data: dict, total_stake: float = 100.0) -> list[dict]:
"""
Parses event data to find arbitrage opportunities across 'Match Winner' markets.
"""
arbitrage_opportunities = []
# Group odds by outcome (Home, Draw, Away) across different bookmakers
outcome_odds_map = {
"Home": [],
"Draw": [],
"Away": []
}
for market in event_data.get("markets", []):
if market.get("market_name") == "Match Winner":
for selection in market.get("selections", []):
selection_name = selection.get("selection_name")
odd = selection.get("odds")
bookmaker_code = selection.get("bookmaker_code")
# Standardize selection names if necessary (e.g., "Team A" vs "Team A to Win")
if selection_name in outcome_odds_map and odd is not None:
outcome_odds_map[selection_name].append({"odd": odd, "bookmaker": bookmaker_code})
# Find the best odds for each outcome across all bookmakers
best_odds_for_market = {}
for outcome, odds_list in outcome_odds_map.items():
if odds_list:
# We want the highest odd for each outcome to maximize arbitrage potential
best_odds_for_market[outcome] = max(odds_list, key=lambda x: x["odd"])["odd"]
if len(best_odds_for_market) == 3: # Must have Home, Draw, Away for 1X2 market
odds_for_arb = list(best_odds_for_market.values())
arb_percentage = calculate_arbitrage_percentage(odds_for_arb)
if arb_percentage < 1.0:
stakes = calculate_arbitrage_stakes(odds_for_arb, total_stake)
profit = calculate_arbitrage_profit(stakes, odds_for_arb, total_stake)
arbitrage_opportunities.append({
"event_id": event_data["event_id"],
"event_title": event_data["event_title"],
"odds_used": best_odds_for_market,
"arbitrage_percentage": arb_percentage,
"total_stake": total_stake,
"stakes_per_outcome": {outcome: s for outcome, s in zip(best_odds_for_market.keys(), stakes)},
"profit": profit
})
return arbitrage_opportunities
if __name__ == "__main__":
# Example usage: Fetch an event ID first, then its odds
# For a real scenario, you'd fetch events for a date first
# For this example, we'll use a placeholder event ID by fetching the first available.
print("Attempting to fetch a football event ID for today...")
events_response = requests.get(
f"{BASE_URL}/v1/football/events",
headers={"X-Api-Key": API_KEY},
params={"schedule_date": "2026-04-29", "has_odds": "true", "per_page": "1"}, # Use a future date for consistent examples
timeout=30,
)
events_response.raise_for_status()
events_data = events_response.json()
if events_data and events_data["events"]:
example_event_id = events_data["events"][0]["event_id"]
print(f"Successfully fetched example event ID: {example_event_id}")
try:
print(f"Fetching full pre-match football odds for event ID: {example_event_id}...")
event_odds_data = fetch_prematch_football_odds(example_event_id)
arbs = find_arbitrage_in_event(event_odds_data)
if arbs:
print("Arbitrage opportunities found:")
print(json.dumps(arbs, indent=2))
else:
print("No arbitrage opportunities found for this event with the current bookmaker odds.")
except requests.exceptions.HTTPError as e:
print(f"HTTP Error fetching odds: {e}")
if e.response.status_code == 401:
print("Check your API_KEY. It might be invalid or missing.")
elif e.response.status_code == 403:
print("Access denied. Your plan might not include the 'full' package or Arbitrage API.")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
else:
print("No football events found for the specified date with odds. Try a different date.")
This Python script demonstrates how to unit-test arbitrage math in code integration by connecting your validated logic to real-world data from a UK bookmaker odds API. The fetch_prematch_football_odds function makes a request to the /v1/football/events/{event_id}/odds endpoint, retrieving comprehensive odds data for a specific fixture. The find_arbitrage_in_event function then processes this real-world data, extracting the best odds for each outcome across different bookmakers and applying the arbitrage calculation. This approach provides an odds API without scraping, ensuring reliable data delivery and allowing you to test your entire pipeline, from data ingestion to arbitrage detection. Remember that the Arbitrage API itself is typically available on higher-tier plans, so ensure your API key has the necessary permissions.
Common Mistakes When Unit-Testing Arbitrage Math
Even with a solid plan, developers can stumble into common pitfalls when trying to how to unit-test arbitrage math in code. Avoiding these mistakes will save you significant debugging time and ensure the reliability of your arbitrage system.
- Floating-point precision errors: Directly comparing floating-point numbers with
==will almost always lead to false negatives due to minute precision differences. Always use comparison functions likepytest.approxor check if the absolute difference is less than a small epsilon (abs(a - b) < epsilon). - Using live data for unit tests: Relying on real-time odds for unit tests makes them non-deterministic. Your tests will pass or fail based on external market fluctuations, not on your code's correctness. Stick to static, controlled data for unit tests.
- Incomplete test coverage: Not testing edge cases can leave your system vulnerable. Ensure you test scenarios like empty odds lists, markets with fewer than three outcomes (for 1X2 markets), non-arbitrage scenarios, and cases where odds are extremely high or low.
- Ignoring bookmaker-specific rules: Arbitrage calculations might need adjustments for specific bookmaker payout rules, maximum stakes, or rounding policies. While unit tests focus on pure math, be aware that integration tests will need to consider these.
- Testing only the arbitrage detection: It's easy to focus solely on finding arbitrage, but the stake calculation is equally critical. If your stake distribution is off, your guaranteed profit disappears. Ensure both detection and stake calculation functions are thoroughly tested.
- Incorrectly parsing odds formats: Mixing decimal, fractional, or American odds without proper conversion will lead to incorrect calculations. Ensure your data ingestion and processing layers consistently convert all odds to a single format (e.g., decimal) before feeding them to your math functions.
Options and Alternatives for Odds Data
When building arbitrage tools, sourcing reliable odds data is paramount. You have a few options, each with distinct trade-offs in terms of reliability, cost, and maintenance. Understanding these helps you choose the right approach for your project, especially when you need pre-match football odds JSON.
| Method | Pros | Cons |
|---|---|---|
| Manual Scraping | Free (initially), full control over data extraction | High maintenance, IP blocks, rate limits, legal risks, inconsistent data formats, no odds API without scraping |
| Generic Sports Data APIs | Structured data, less maintenance than scraping, broader sport coverage | Often lack UK bookmaker depth, may not offer specific pre-match football odds JSON detail, higher latency |
| UK Odds API | UK-focused, normalised data, stable bookmaker codes, pre-match football odds JSON, low maintenance, dedicated support | Primarily football (soccer) focus, paid tiers for full coverage, not an in-play API |
FAQ
How accurate do floating-point comparisons need to be in arbitrage?
For arbitrage, floating-point comparisons should use a small epsilon (e.g., 1e-6 or 1e-9) to account for precision errors. Directly comparing with == is unreliable. pytest.approx is a good tool for this in Python.
Can I use historical odds data for testing arbitrage logic?
Yes, historical odds data is excellent for backtesting and validating arbitrage logic over longer periods. It allows you to simulate past market conditions and verify the theoretical profitability of your strategies without relying on live data.
What's the best way to handle different odds formats in arbitrage calculations?
Always convert all incoming odds to a single, consistent format (e.g., decimal odds) as early as possible in your data processing pipeline. This simplifies your arbitrage math and reduces the chance of conversion errors.
How often should I run my arbitrage unit tests?
Unit tests should be run frequently, ideally as part of your continuous integration (CI) pipeline, or at least before every major code commit. This ensures that new changes don't introduce regressions into your core arbitrage math.
What if my arbitrage logic requires more complex market data than simple Match Winner odds?
Your unit tests should reflect the complexity of your logic. If you're using advanced markets (e.g., handicaps, totals), ensure your static test data includes these, and your calculation functions can correctly process them. The UK bookmaker odds API offers a full package for broader market coverage.
Conclusion
Mastering how to unit-test arbitrage math in code is fundamental to building a reliable and profitable betting system. By isolating your core calculations, using static pre-match football odds JSON for repeatable tests, and integrating with a robust UK bookmaker odds API like ukoddsapi.com, you can ensure your arbitrage detection and stake allocation logic is flawless. This systematic approach, avoiding the pitfalls of live data in unit tests, provides the confidence you need to deploy your arbitrage finder effectively.
Ready to build your arbitrage system with reliable pre-match football odds? Explore the UK Odds API and get started today at ukoddsapi.com.