Skip to main content
Bot de Arbitragem em Python: Tutorial Completo
Voltar ao blog

Bot de Arbitragem em Python: Tutorial Completo

James Whitfield

James Whitfield

5 min de leitura

Arbitragem em apostas significa cobrir todos os resultados de um evento em casas de apostas diferentes cujas odds discordam o suficiente para você lucrar seja qual for o desfecho. Encontrar essas janelas na mão é impossível. Elas aparecem e somem em minutos. Então vamos construir um bot que as encontra, calcula os stakes e avisa você, em cerca de 100 linhas de Python.

Se você quer os fundamentos primeiro, leia nosso tutorial de scanner de arbitragem. Aquele post cobre a teoria e um script de escanear e imprimir em profundidade. Este aqui é o ângulo da automação: um bot de longa duração com deduplicação, divisão de stakes e alertas.

O que você precisa

  • Uma chave de API gratuita da Odds-API.io (100 requisições/hora, sem cartão de crédito)
  • Python 3.10+ e a biblioteca requests
  • Contas em duas ou mais casas de apostas, como Betano e Bet365, se você pretende realmente apostar
pip install requests

A matemática da arbitragem de dois resultados

Para um mercado de dois resultados (vencedor de partida de tênis, handicap asiático, totais), some o inverso das melhores odds disponíveis de cada lado:

margin = 1/odds_a + 1/odds_b

Se a soma fica abaixo de 1.0, existe uma arbitragem. Digamos que a Casa A tem o mandante a 2.15 e a Casa B tem o visitante a 2.10:

1/2.15 + 1/2.10 = 0.4651 + 0.4762 = 0.9413

Isso é uma vantagem de 5,87% antes de limites e taxas. Para travar o lucro, você divide o stake total na proporção da probabilidade implícita de cada lado. Com R$ 100, ficaria assim:

def split_stakes(total, odds_a, odds_b):
    inv_a, inv_b = 1 / odds_a, 1 / odds_b
    s = inv_a + inv_b
    stake_a = total * inv_a / s
    stake_b = total * inv_b / s
    payout = stake_a * odds_a  # equal on both sides
    return round(stake_a, 2), round(stake_b, 2), round(payout - total, 2)

print(split_stakes(100, 2.15, 2.10))
# (49.41, 50.59, 6.24)  -> bet 49.41 on A, 50.59 on B, ~6.24 profit either way

O caminho fácil: o endpoint /arbitrage-bets

Você poderia puxar as odds brutas de cada evento e rodar essa matemática em cada par de casas por conta própria. A Odds-API.io também faz isso no servidor. O endpoint /arbitrage-bets escaneia as casas que você especificar e retorna oportunidades ao vivo com os stakes ótimos já calculados:

import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.odds-api.io/v3"

resp = requests.get(f"{BASE}/arbitrage-bets", params={
    "apiKey": API_KEY,
    "bookmakers": "Betano,Bet365,SingBet",
    "includeEventDetails": "true",
    "limit": 50,
})
resp.raise_for_status()

for arb in resp.json():
    ev = arb.get("event", {})
    print(f"{ev.get('home')} vs {ev.get('away')} | {arb['market']['name']} "
          f"| margin {arb['profitMargin']:.2%}")
    for leg, stake in zip(arb["legs"], arb["optimalStakes"]):
        print(f"  {leg['side']:>4} @ {leg['odds']} on {leg['bookmaker']} "
              f"-> stake {stake['stake']}")

Cada oportunidade inclui as pernas, a casa de apostas de cada lado, a margem de lucro, os stakes ótimos para uma banca de 100 unidades (escale linearmente para a sua) e links diretos para o mercado exato em cada casa.

Verificando com odds brutas

Antes de apostar dinheiro de verdade, confira os preços você mesmo. As odds se movem, e os segundos entre o escaneamento e a sua aposta importam. Puxe as odds atuais do evento com /odds:

def verify(event_id, bookmakers):
    resp = requests.get(f"{BASE}/odds", params={
        "apiKey": API_KEY,
        "eventId": event_id,
        "bookmakers": ",".join(bookmakers),
    })
    resp.raise_for_status()
    data = resp.json()
    for book, markets in data["bookmakers"].items():
        for market in markets:
            if market["name"] == "ML":
                print(book, market["odds"], "updated", market["updatedAt"])

verify(123456, ["Betano", "Bet365"])

Recalcule a margem com o que você vê. Se ela passou de 1.0, a janela fechou. Siga em frente, o bot vai achar a próxima.

Montando o bot

Aqui está o loop completo: consultar, deduplicar para você não ser alertado duas vezes pela mesma oportunidade, filtrar por margem mínima e notificar via Telegram.

import time
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.odds-api.io/v3"
BOOKMAKERS = "Betano,Bet365,SingBet"
MIN_MARGIN = 0.01          # ignore arbs under 1%
BANKROLL = 200.0
POLL_SECONDS = 120         # 30 polls/hour, well inside the free tier

TG_TOKEN = "YOUR_TELEGRAM_BOT_TOKEN"
TG_CHAT = "YOUR_CHAT_ID"

seen = {}

def notify(text):
    requests.post(
        f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
        json={"chat_id": TG_CHAT, "text": text},
    )

def fetch_arbs():
    resp = requests.get(f"{BASE}/arbitrage-bets", params={
        "apiKey": API_KEY,
        "bookmakers": BOOKMAKERS,
        "includeEventDetails": "true",
    })
    if resp.status_code == 429:
        reset = resp.headers.get("x-ratelimit-reset", "")
        print("rate limited until", reset)
        time.sleep(300)
        return []
    resp.raise_for_status()
    return resp.json()

def run():
    while True:
        for arb in fetch_arbs():
            if arb["profitMargin"] < MIN_MARGIN or arb["id"] in seen:
                continue
            seen[arb["id"]] = time.time()
            ev = arb.get("event", {})
            scale = BANKROLL / arb["totalStake"]
            lines = [
                f"ARB {arb['profitMargin']:.2%} | "
                f"{ev.get('home')} vs {ev.get('away')} | {arb['market']['name']}"
            ]
            for leg, stake in zip(arb["legs"], arb["optimalStakes"]):
                lines.append(
                    f"{leg['side']}: {stake['stake'] * scale:.2f} "
                    f"@ {leg['odds']} ({leg['bookmaker']})"
                )
            notify("\n".join(lines))
        # drop dedupe entries older than an hour
        cutoff = time.time() - 3600
        for key in [k for k, t in seen.items() if t < cutoff]:
            del seen[key]
        time.sleep(POLL_SECONDS)

if __name__ == "__main__":
    run()

Execute com:

python arb_bot.py

Polling vs WebSocket

Consultar a cada dois minutos funciona bem para arbitragens pré-jogo, que costumam durar vários minutos. Para arbitragem ao vivo as janelas fecham em segundos, e o polling perde a corrida. Os planos pagos incluem o feed WebSocket (wss://api.odds-api.io/v3/ws), que envia as mudanças de odds para você em cerca de um segundo. A lógica do bot continua a mesma, você só recalcula as margens a cada atualização recebida em vez de usar um timer.

Ressalvas realistas

  • Limites de requisição. O plano gratuito é 100 requisições/hora, os pagos são 5.000/hora. Observe o header x-ratelimit-remaining e recue ao receber um 429.
  • Latência mata arbitragens marginais. Uma arbitragem de 1% que levou 90 segundos para ser apostada costuma virar uma arbitragem de 0%. Verifique os preços imediatamente antes de apostar.
  • Restrições de conta são o limitador real. Casas recreativas limitam ou fecham contas que pegam preços de arbitragem com frequência, e isso vale também para as casas licenciadas no Brasil. Casas sharp e exchanges toleram vencedores; leve isso em conta ao escolher quais casas escanear.
  • Arredondamento de stakes. Apostar R$ 49,41 e R$ 50,59 parece exatamente arbitragem. Muitos arbitradores arredondam para valores naturais e aceitam um lucro levemente desigual.
  • Uma perna pode ser anulada ou rejeitada, deixando você exposto na outra. Tenha um plano (hedge em uma exchange) antes que isso aconteça.

Próximos passos

Pegue uma chave de API gratuita, rode o bot contra odds reais e leia o tutorial do scanner para a teoria mais profunda, mais tipos de mercado e o código completo da versão focada em escaneamento. Os dois scripts compartilham a mesma matemática e se complementam bem.