tutorial

How to Log Arb Legs for Compliance and Debugging

Building an arbitrage betting system means dealing with constantly shifting pre-match football odds. When you find an arb, you need to react fast. But speed isn't the only challenge; you also need a robust way to log arb legs for compliance and debugging. This isn't just good practice; it's crucial for understanding your system's performance and meeting any regulatory requirements.

Proper logging helps you reconstruct events, verify calculations, and pinpoint issues when something goes wrong. Whether you're tracking successful bets, failed attempts, or just the odds snapshots that triggered an alert, having a clear audit trail is invaluable. This guide will walk you through integrating logging into your arb detection workflow using a reliable UK bookmaker odds API.

Prerequisites

Before you dive into logging arbitrage legs, make sure you have these components ready:

  • ukoddsapi.com API Key: You'll need an active API key to access pre-match football odds data. The Business tier includes access to the Arbitrage API.
  • Python 3.x: Our code examples will use Python, a common language for data processing and API integrations.
  • requests library: For making HTTP requests to the UK Odds API. Install it via pip install requests.
  • Basic understanding of JSON: The API responses are in JSON format.
  • A logging solution: This could be as simple as writing to a file, or more advanced like a database or a dedicated logging service. For this tutorial, we'll focus on file-based logging.

Step 1: Fetching Arbitrage Opportunities

The first step is to retrieve potential arbitrage opportunities from the UK Odds API. The /v1/football/arbitrage endpoint provides a feed of pre-match football odds where an arb has been detected across various UK bookmakers. This endpoint is specifically designed to help you identify these scenarios without needing to manually aggregate and compare odds from different sources.

Here's how you can fetch the arbitrage feed for a specific date using Python:

import os
import requests
import datetime

API_KEY = os.environ.get("UKODDSAPI_KEY", "YOUR_API_KEY") # Replace YOUR_API_KEY or set env var
BASE_URL = "https://api.ukoddsapi.com"
HEADERS = {"X-Api-Key": API_KEY}

def fetch_arbitrage_opportunities(date_str: str):
    """Fetches arbitrage opportunities for a given date."""
    endpoint = f"{BASE_URL}/v1/football/arbitrage"
    params = {"date": date_str, "min_profit": "0.01", "round_to": "2"}
    try:
        response = requests.get(endpoint, headers=HEADERS, params=params, timeout=60)
        response.raise_for_status() # Raise an exception for HTTP errors
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error fetching arbitrage opportunities: {e}")
        return None

if __name__ == "__main__":
    today = datetime.date.today().isoformat()
    arb_data = fetch_arbitrage_opportunities(today)

    if arb_data and arb_data.get("arbitrage_opportunities"):
        print(f"Fetched {len(arb_data['arbitrage_opportunities'])} arbitrage opportunities for {today}.")
        # Example of an arb leg structure
        # print(arb_data['arbitrage_opportunities'][0])
    else:
        print(f"No arbitrage opportunities found for {today} or an error occurred.")

This Python snippet defines a function fetch_arbitrage_opportunities that queries the /v1/football/arbitrage endpoint. It includes parameters for date, min_profit, and round_to. The min_profit parameter helps filter for opportunities above a certain threshold, while round_to specifies the decimal places for profit calculation. The API returns a JSON object containing a list of arbitrage_opportunities, each detailing the event, market, selections, and participating bookmakers.

developer's screen showing Python code for fetching arb data, with a subtle background of football stadium lights

Step 2: Designing Your Log Schema for Arb Legs

Before you start logging, define what information you need to capture. A well-structured log schema is critical for both compliance and effective debugging. For arbitrage legs, you typically want to record details about the event, the market, each selection (the "leg"), the odds at the time of detection, and any associated actions or outcomes.

Here’s a sample JSON structure for a single arbitrage leg log entry:

{
  "timestamp": "2026-04-25T14:30:00Z",
  "log_id": "arb-log-12345",
  "event_id": "EV00123456",
  "event_title": "Manchester United vs Liverpool",
  "kickoff_utc": "2026-04-25T15:00:00Z",
  "market_name": "Match Winner",
  "arb_profit_percentage": 2.5,
  "legs": [
    {
      "bookmaker_code": "UO001",
      "bookmaker_name": "10Bet",
      "selection_name": "Manchester United",
      "odds": 2.10,
      "stake": 48.08,
      "implied_probability": 0.476,
      "bet_placed": true,
      "bet_id": "BET78901",
      "placed_at": "2026-04-25T14:31:05Z",
      "status": "success"
    },
    {
      "bookmaker_code": "UO027",
      "bookmaker_name": "William Hill",
      "selection_name": "Draw",
      "odds": 3.50,
      "stake": 28.57,
      "implied_probability": 0.286,
      "bet_placed": false,
      "error_message": "Insufficient funds"
    },
    {
      "bookmaker_code": "UO015",
      "bookmaker_name": "Ladbrokes",
      "selection_name": "Liverpool",
      "odds": 4.00,
      "stake": 23.81,
      "implied_probability": 0.250,
      "bet_placed": true,
      "bet_id": "BET78902",
      "placed_at": "2026-04-25T14:31:10Z",
      "status": "pending_confirmation"
    }
  ],
  "detection_method": "ukoddsapi_arbitrage_feed",
  "system_version": "v1.2.0",
  "environment": "production"
}

This schema captures:

  • timestamp: When the arb was detected and logged.
  • log_id: A unique identifier for this specific log entry.
  • event_id, event_title, kickoff_utc: Core details about the football match.
  • market_name: The betting market (e.g., "Match Winner").
  • arb_profit_percentage: The calculated profit margin.
  • legs: An array of objects, one for each selection in the arb. Each leg includes: bookmaker_code, bookmaker_name, selection_name, odds: Details from the API. stake, implied_probability: Your calculated betting parameters. bet_placed, bet_id, placed_at, status, error_message: Crucial for tracking the outcome of your automated betting attempts. detection_method: How the arb was found (e.g., ukoddsapi_arbitrage_feed).
  • system_version, environment: Context for debugging and compliance.

This detailed structure allows you to trace exactly what happened with each potential arb, from detection to attempted placement, providing a clear trail for auditing and troubleshooting.

Step 3: Implementing the Logging Mechanism

Now, let's integrate this logging schema into your Python application. We'll create a simple file-based logger that writes each arb opportunity, along with its processing status, to a JSON Lines file. This format is easy to parse later and append to.

import json
import logging
import datetime
import uuid

# Configure basic logging for console output
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def setup_arb_logger(log_file_path: str):
    """Sets up a dedicated logger for arbitrage events."""
    arb_logger = logging.getLogger("arbitrage_logger")
    arb_logger.setLevel(logging.INFO)
    
    # Prevent adding multiple handlers if called multiple times
    if not arb_logger.handlers:
        file_handler = logging.FileHandler(log_file_path)
        formatter = logging.Formatter('%(message)s') # We want raw JSON, not default log format
        file_handler.setFormatter(formatter)
        arb_logger.addHandler(file_handler)
    
    return arb_logger

def log_arbitrage_opportunity(arb_logger, opportunity: dict, bet_outcomes: dict):
    """
    Logs a detected arbitrage opportunity and its betting outcomes.
    
    :param arb_logger: The configured logger instance.
    :param opportunity: The raw arbitrage opportunity from the API.
    :param bet_outcomes: A dictionary containing outcomes for each leg, e.g.,
                         {
                             "leg_key_from_api": {
                                 "bet_placed": true,
                                 "bet_id": "BET123",
                                 "placed_at": "...",
                                 "status": "success",
                                 "stake": 50.00
                             },
                             ...
                         }
    """
    timestamp = datetime.datetime.utcnow().isoformat(timespec='seconds') + 'Z'
    log_id = f"arb-log-{uuid.uuid4().hex}"

    # Extract core event and market details
    event_id = opportunity.get("event_id")
    event_title = opportunity.get("event_title")
    kickoff_utc = opportunity.get("kickoff_utc")
    market_name = opportunity.get("market_name")
    arb_profit_percentage = opportunity.get("profit_percentage")

    logged_legs = []
    for leg in opportunity.get("legs", []):
        bookmaker_code = leg.get("bookmaker_code")
        bookmaker_name = leg.get("bookmaker_name")
        selection_name = leg.get("selection_name")
        odds = leg.get("odds")
        implied_probability = leg.get("implied_probability")

        # Map API leg to its outcome
        # Assuming a unique key for each leg can be constructed, e.g., bookmaker_code + selection_name
        leg_key = f"{bookmaker_code}-{selection_name}"
        outcome = bet_outcomes.get(leg_key, {})

        logged_legs.append({
            "bookmaker_code": bookmaker_code,
            "bookmaker_name": bookmaker_name,
            "selection_name": selection_name,
            "odds": odds,
            "stake": outcome.get("stake"),
            "implied_probability": implied_probability,
            "bet_placed": outcome.get("bet_placed", False),
            "bet_id": outcome.get("bet_id"),
            "placed_at": outcome.get("placed_at"),
            "status": outcome.get("status"),
            "error_message": outcome.get("error_message")
        })

    log_entry = {
        "timestamp": timestamp,
        "log_id": log_id,
        "event_id": event_id,
        "event_title": event_title,
        "kickoff_utc": kickoff_utc,
        "market_name": market_name,
        "arb_profit_percentage": arb_profit_percentage,
        "legs": logged_legs,
        "detection_method": "ukoddsapi_arbitrage_feed",
        "system_version": "v1.0.0", # Your system version
        "environment": "development" # or "production"
    }

    arb_logger.info(json.dumps(log_entry))
    print(f"Logged arb opportunity {log_id} for event {event_title}")

if __name__ == "__main__":
    # Example usage:
    arb_log_file = "arbitrage_logs.jsonl"
    arb_logger = setup_arb_logger(arb_log_file)

    # Simulate fetching an opportunity (using a dummy structure for demonstration)
    dummy_opportunity = {
        "event_id": "EV00123456",
        "event_title": "Manchester United vs Liverpool",
        "kickoff_utc": "2026-04-25T15:00:00Z",
        "market_name": "Match Winner",
        "profit_percentage": 2.5,
        "legs": [
            {
                "bookmaker_code": "UO001",
                "bookmaker_name": "10Bet",
                "selection_name": "Manchester United",
                "odds": 2.10,
                "implied_probability": 0.476
            },
            {
                "bookmaker_code": "UO027",
                "bookmaker_name": "William Hill",
                "selection_name": "Draw",
                "odds": 3.50,
                "implied_probability": 0.286
            },
            {
                "bookmaker_code": "UO015",
                "bookmaker_name": "Ladbrokes",
                "selection_name": "Liverpool",
                "odds": 4.00,
                "implied_probability": 0.250
            }
        ]
    }

    # Simulate betting outcomes for the dummy opportunity
    dummy_bet_outcomes = {
        "UO001-Manchester United": {
            "bet_placed": True,
            "bet_id": "BET78901",
            "placed_at": datetime.datetime.utcnow().isoformat(timespec='seconds') + 'Z',
            "status": "success",
            "stake": 48.08
        },
        "UO027-Draw": {
            "bet_placed": False,
            "error_message": "API timeout during placement",
            "stake": 28.57
        },
        "UO015-Liverpool": {
            "bet_placed": True,
            "bet_id": "BET78902",
            "placed_at": datetime.datetime.utcnow().isoformat(timespec='seconds') + 'Z',
            "status": "pending_confirmation",
            "stake": 23.81
        }
    }

    log_arbitrage_opportunity(arb_logger, dummy_opportunity, dummy_bet_outcomes)

    # You would typically integrate this into your main loop
    # where you fetch and process arbitrage opportunities.

This code sets up a Python logging instance specifically for arbitrage events. The log_arbitrage_opportunity function takes the raw API opportunity and a dictionary of simulated betting outcomes for each leg. It then constructs a comprehensive log entry following our defined schema and writes it as a single JSON line to the specified file. This approach ensures that each log entry is self-contained and easily parsable.

a close-up of a terminal window showing log entries in JSON format, with timestamps and event details

Step 4: Integrating Logging into Your Arb System Workflow

Logging isn't a standalone task; it needs to be an integral part of your arbitrage detection and execution workflow. Here's a conceptual outline of how you'd weave the logging mechanism into your system:

  1. Poll for Arbitrage Opportunities: Regularly call fetch_arbitrage_opportunities using the UK Odds API's /v1/football/arbitrage endpoint.
  2. Process Each Opportunity: For every detected arb, perform your internal calculations (e.g., optimal stakes).
  3. Attempt Bet Placements: Initiate API calls to bookmakers to place bets for each leg. This is where you'd track the bet_placed, bet_id, status, and error_message for each leg.
  4. Consolidate Outcomes: Gather all the outcomes for each leg (success, failure, pending, error details).
  5. Log the Full Arb: Call log_arbitrage_opportunity with the original arb data and the consolidated outcomes.

This workflow ensures that every arb, regardless of its outcome, generates a detailed log entry. This is vital for how to log arb legs for compliance and debugging integration.

# This is a conceptual example, actual bet placement logic would be complex.
# This assumes you have functions like place_bet_with_bookmaker(bookmaker_code, selection, stake, odds)

def process_and_log_arb(arb_logger, opportunity: dict):
    """
    Processes a single arbitrage opportunity, attempts bets, and logs outcomes.
    """
    event_title = opportunity.get("event_title", "Unknown Event")
    print(f"Processing arbitrage for: {event_title}")

    bet_outcomes = {}
    for leg in opportunity.get("legs", []):
        bookmaker_code = leg.get("bookmaker_code")
        selection_name = leg.get("selection_name")
        odds = leg.get("odds")
        
        # Calculate optimal stake for this leg (simplified for example)
        # In a real system, this would involve complex calculations based on total stake, profit, etc.
        stake = 100 / (leg.get("implied_probability", 1) * 100) # Placeholder for stake calculation

        leg_key = f"{bookmaker_code}-{selection_name}"
        outcome = {
            "bet_placed": False,
            "stake": round(stake, 2) # Example stake
        }

        try:
            # Simulate bet placement. In reality, this would be an API call to a bookmaker.
            # response = place_bet_with_bookmaker(bookmaker_code, selection_name, stake, odds)
            # if response.get("success"):
            #     outcome["bet_placed"] = True
            #     outcome["bet_id"] = response.get("bet_id")
            #     outcome["placed_at"] = datetime.datetime.utcnow().isoformat(timespec='seconds') + 'Z'
            #     outcome["status"] = "success"
            # else:
            #     outcome["error_message"] = response.get("error")
            
            # For demonstration, simulate some failures
            if "Draw" in selection_name and bookmaker_code == "UO027":
                raise ValueError("Simulated API error for Draw leg")
            
            outcome["bet_placed"] = True
            outcome["bet_id"] = f"SIMBET-{uuid.uuid4().hex[:8]}"
            outcome["placed_at"] = datetime.datetime.utcnow().isoformat(timespec='seconds') + 'Z'
            outcome["status"] = "success"

        except Exception as e:
            outcome["bet_placed"] = False
            outcome["error_message"] = str(e)
            outcome["status"] = "failed"
        
        bet_outcomes[leg_key] = outcome
    
    log_arbitrage_opportunity(arb_logger, opportunity, bet_outcomes)

if __name__ == "__main__":
    arb_log_file = "arbitrage_logs.jsonl"
    arb_logger = setup_arb_logger(arb_log_file)

    # Fetch real arbitrage opportunities (or use dummy if API not available/tier not supported)
    today = datetime.date.today().isoformat()
    arb_feed = fetch_arbitrage_opportunities(today)

    if arb_feed and arb_feed.get("arbitrage_opportunities"):
        for arb_opportunity in arb_feed["arbitrage_opportunities"]:
            process_and_log_arb(arb_logger, arb_opportunity)
    else:
        print("No arbitrage opportunities to process or an error occurred during fetch.")
        # If no real data, process the dummy one to show logging
        print("Processing dummy opportunity for logging demonstration...")
        dummy_opportunity = {
            "event_id": "EV00123456",
            "event_title": "Manchester United vs Liverpool",
            "kickoff_utc": "2026-04-25T15:00:00Z",
            "market_name": "Match Winner",
            "profit_percentage": 2.5,
            "legs": [
                {
                    "bookmaker_code": "UO001",
                    "bookmaker_name": "10Bet",
                    "selection_name": "Manchester United",
                    "odds": 2.10,
                    "implied_probability": 0.476
                },
                {
                    "bookmaker_code": "UO027",
                    "bookmaker_name": "William Hill",
                    "selection_name": "Draw",
                    "odds": 3.50,
                    "implied_probability": 0.286
                },
                {
                    "bookmaker_code": "UO015",
                    "bookmaker_name": "Ladbrokes",
                    "selection_name": "Liverpool",
                    "odds": 4.00,
                    "implied_probability": 0.250
                }
            ]
        }
        process_and_log_arb(arb_logger, dummy_opportunity)

This expanded example shows a process_and_log_arb function. It iterates through the legs of an arbitrage opportunity, simulates (or would make) bet placement calls, and collects the outcomes. Finally, it calls log_arbitrage_opportunity to persist the full context. This demonstrates a complete cycle of how to log arb legs for compliance and debugging.

Common Mistakes

When implementing logging for arbitrage systems, developers often encounter similar pitfalls. Avoiding these can save significant debugging time and ensure compliance.

  • Incomplete Data Capture: Only logging the arb detection, but not the actual bet placement status or errors. This leaves a crucial gap in your audit trail. Fix: Ensure your log schema includes fields for bet_placed, bet_id, status, and error_message for each leg. Lack of Unique Identifiers: Not assigning unique IDs to each arb opportunity or log entry. This makes it hard to correlate events across different logs or systems. Fix: Generate a log_id (e.g., using UUIDs) for each log entry and include event_id from the API. Over-logging or Under-logging: Logging too much irrelevant data clutters logs and consumes storage, while logging too little makes debugging impossible. Fix: Stick to a well-defined schema. Log only what's necessary to reconstruct the event and debug, but ensure that "necessary" is comprehensive enough. Ignoring Timezones: Not using UTC for all timestamps. This leads to confusion and errors when correlating events across different geographical locations or server times. Fix: Always use UTC timestamps (e.g., datetime.datetime.utcnow().isoformat(timespec='seconds') + 'Z'). Poor Log Rotation/Retention: Letting log files grow indefinitely or deleting them too soon. This can lead to disk space issues or loss of critical compliance data. Fix: Implement a log rotation strategy (e.g., logging.handlers.RotatingFileHandler in Python) and define a clear retention policy. Not Testing Logging: Assuming logging works without verifying that entries are correctly formatted and contain the expected data.
    • Fix: Include logging verification in your test suite. Parse sample log entries and assert their structure and content.

Options and Alternatives

While file-based JSON Lines logging is a solid starting point, other options exist depending on your scale and requirements for how to log arb legs for compliance and debugging.

Feature / Approach File-based JSON Lines Database (SQL/NoSQL) Centralized Logging Service (e.g., ELK, Splunk)
Ease of Setup Very Easy Moderate Moderate to Complex
Scalability Low (single file) High Very High
Querying/Search Manual parsing/grep SQL/NoSQL queries Powerful search & analytics tools
Real-time Analysis No Possible Excellent
Data Retention Manual/OS tools Configurable Configurable, often with archiving
Cost Low (storage) Moderate (DB hosting) High (subscription, data volume)
Compliance Requires custom tools Built-in features Advanced audit trails, access control

For smaller projects or initial development, file-based logging is often sufficient. As your system grows, or if you need more sophisticated querying, real-time analysis, or strict compliance features, migrating to a database or a centralized logging service becomes more appealing. A database offers structured storage and powerful querying capabilities, making it easier to analyze historical data. Centralized logging services provide a full suite of tools for aggregation, searching, visualization, and alerting across distributed systems, which is ideal for large-scale operations.

FAQ

How often should I log arbitrage opportunities?

You should log every detected arbitrage opportunity as soon as it's identified, and then update its status as you attempt to place bets on each leg. This ensures a complete audit trail.

What's the best way to handle sensitive data in logs for compliance?

Avoid logging sensitive information like API keys or personal user data directly. If you must log parts of it for debugging, ensure it's masked, encrypted, or hashed. Implement strict access controls for your log storage.

Can I use the UK Odds API to get historical arbitrage data for backtesting?

Yes, the UK Odds API offers historical odds data on Pro and Business tiers. While the /v1/football/arbitrage endpoint provides current opportunities, you can use historical odds data to backtest your own arb detection algorithms.

How does logging help with debugging failed bet placements?

Detailed logs allow you to trace the exact odds, bookmaker responses, and any errors encountered during each leg's bet placement. This helps pinpoint whether the issue was with your logic, the bookmaker's API, or network conditions.

Is it necessary to log the implied probability for each leg?

Logging the implied probability for each leg is highly recommended. It helps verify your arbitrage calculations and ensures that the detected opportunity was mathematically sound at the time of detection, which is useful for both debugging and compliance.

Conclusion

Implementing robust logging for arbitrage legs is a non-negotiable part of building a reliable and compliant betting system. By capturing detailed pre-match football odds JSON data, event information, and bet placement outcomes, you create an invaluable audit trail. This not only aids in debugging when things inevitably go sideways but also provides the necessary records for any compliance requirements.

A well-structured logging approach, like the one outlined here using the UK Odds API, helps you maintain control and understanding over your automated processes. Start simple with file-based logging, and scale up to databases or centralized services as your needs evolve. Get started with your own arbitrage system and integrate comprehensive logging today by exploring the UK Odds API.