Pular para o conteúdo
Backend

Como criar um bot de apostas esportivas

Dá pra construir um bot de apostas funcional com Python, scikit-learn e uma API de odds — e eu vou te mostrar como do zero ao deploy. Sem

Por que isso é importante

Como criar um bot de apostas esportivas. Dá pra construir um bot de apostas funcional com Python, scikit-learn e uma API de odds — e eu vou te mostrar como do zero ao deploy. Sem enrolação, só código que roda.

Galera, vou direto ao ponto: construir um bot de apostas com Python não é nenhum bicho de sete cabeças. Você precisa de uma fonte de odds, um modelo de previsão e uma lógica de decisão. Isso é tudo. A parte difícil não é o código — é ter um modelo que realmente funcione.

Aviso antes de começar: este conteúdo é puramente educacional e técnico. Não é recomendação de apostas nem aconselhamento financeiro. Apostas envolvem risco real de perda de capital.

Se você quiser entender a lógica matemática por trás de uma estratégia de IA para apostas, o artigo hub deste cluster cobre o fundamento teórico com profundidade. Aqui o foco é implementação.

O que é um bot de apostas e como funciona

Um bot de apostas é um programa que coleta odds de uma ou mais casas, compara com a probabilidade que ele próprio calcula e decide se aquela aposta tem valor positivo esperado (EV+). Se sim, ele pode executar a aposta automaticamente — ou apenas sinalizar para o usuário decidir.

O fluxo tem quatro etapas fixas: coleta de dados de odds em tempo real, cálculo da probabilidade real pelo modelo, comparação entre probabilidade do modelo e probabilidade implícita na odd, e decisão de apostar ou não com base no edge calculado. Simples assim na teoria, trabalhoso na prática.

O que diferencia um bot que funciona de um que não funciona

A qualidade do modelo de previsão é o que determina tudo

Sem edge real no modelo, o bot só vai automatizar perdas

Backtesting rigoroso antes de colocar dinheiro real é obrigatório

Gestão de banca com Kelly ou percentual fixo precisa estar no código

Stack técnico necessário

Python e as libs principais

Python 3.11+ é o ponto de partida. Para o modelo preditivo, você vai precisar de pandas para manipular dados históricos, scikit-learn para os algoritmos de ML e numpy para as contas. Para integrar com APIs, requests ou httpx resolvem. Para agendamento, schedule ou APScheduler.

bash
# Criando o ambiente virtual e instalando as dependências
python -m venv .venv
source .venv/bin/activate  # ou .venv\Scripts\activate no Windows

pip install pandas scikit-learn numpy requests schedule python-dotenv
pip install xgboost lightgbm  # modelos mais potentes
pip install sqlalchemy psycopg2-binary  # banco de dados

API de odds: The Odds API

The Odds API é a opção mais usada por devs. Tem plano gratuito com 500 requests por mês, cobre centenas de ligas e esportes, e a documentação é boa. Para um bot em produção, o plano pago a partir de US$ 79/mês dá 30.000 requests. Para ver as APIs de odds disponíveis com comparativo completo de preços e limites, tem um guia específico aqui no cluster.

Banco de dados para histórico

Você vai precisar guardar histórico de odds, resultados dos jogos e o registro de todas as apostas do bot. PostgreSQL é o que eu recomendo. SQLite serve pra desenvolvimento, mas em produção com consultas de séries temporais de odds o Postgres é muito mais rápido.

python
# Python 3.11+
# models.py — estrutura do banco com SQLAlchemy

from sqlalchemy import create_engine, Column, String, Float, DateTime, Boolean, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from datetime import datetime

class Base(DeclarativeBase):
    pass


class OddsSnapshot(Base):
    """Snapshot de odds em determinado momento."""
    __tablename__ = "odds_snapshots"
    
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    evento_id: Mapped[str] = mapped_column(String(100), index=True)
    sport: Mapped[str] = mapped_column(String(50))
    home_team: Mapped[str] = mapped_column(String(100))
    away_team: Mapped[str] = mapped_column(String(100))
    bookmaker: Mapped[str] = mapped_column(String(50))
    market: Mapped[str] = mapped_column(String(50))  # h2h, totals, spreads
    outcome: Mapped[str] = mapped_column(String(50))  # home, away, draw
    odd: Mapped[float] = mapped_column(Float)
    captured_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)


class BetRecord(Base):
    """Registro de apostas do bot."""
    __tablename__ = "bet_records"
    
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    evento_id: Mapped[str] = mapped_column(String(100))
    bookmaker: Mapped[str] = mapped_column(String(50))
    outcome: Mapped[str] = mapped_column(String(50))
    odd: Mapped[float] = mapped_column(Float)
    prob_modelo: Mapped[float] = mapped_column(Float)
    edge: Mapped[float] = mapped_column(Float)
    stake: Mapped[float] = mapped_column(Float)
    resultado: Mapped[str | None] = mapped_column(String(10), nullable=True)  # win/loss
    lucro: Mapped[float | None] = mapped_column(Float, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)


# Criando as tabelas
engine = create_engine("postgresql://user:pass@localhost/betsbot")
Base.metadata.create_all(engine)

Coletando dados de odds em tempo real

Aqui começa o código que realmente importa. O coletor vai chamar a API periodicamente, gravar os snapshots de odds no banco e identificar oportunidades. Você pode complementar isso com dados coletados via scraping — tem um tutorial completo sobre coletar dados via scraping das casas de apostas neste cluster.

python
# Python 3.11+
# odds_collector.py — coleta odds da The Odds API

import requests
import os
from datetime import datetime
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("ODDS_API_KEY")
BASE_URL = "https://api.the-odds-api.com/v4"


def fetch_odds(
    sport: str = "soccer_brazil_campeonato",
    regions: str = "eu",
    markets: str = "h2h",
    bookmakers: str | None = None
) -> list[dict]:
    """
    Busca odds atuais para o esporte e mercado especificados.
    
    Returns:
        Lista de dicts com dados dos eventos e odds
    """
    params = {
        "apiKey": API_KEY,
        "regions": regions,
        "markets": markets,
        "oddsFormat": "decimal",
        "dateFormat": "iso",
    }
    if bookmakers:
        params["bookmakers"] = bookmakers
    
    resp = requests.get(f"{BASE_URL}/sports/{sport}/odds", params=params, timeout=10)
    resp.raise_for_status()
    
    # Mostra quantos requests restam na cota
    remaining = resp.headers.get("x-requests-remaining", "?")
    print(f"Requests restantes na API: {remaining}")
    
    return resp.json()


def parse_odds(raw_events: list[dict]) -> list[dict]:
    """Normaliza os dados brutos da API."""
    rows = []
    for event in raw_events:
        for bookmaker in event.get("bookmakers", []):
            for market in bookmaker.get("markets", []):
                for outcome in market.get("outcomes", []):
                    rows.append({
                        "evento_id": event["id"],
                        "home_team": event["home_team"],
                        "away_team": event["away_team"],
                        "commence_time": event["commence_time"],
                        "bookmaker": bookmaker["key"],
                        "market": market["key"],
                        "outcome": outcome["name"],
                        "odd": outcome["price"],
                        "captured_at": datetime.utcnow().isoformat(),
                    })
    return rows


# Teste rápido
if __name__ == "__main__":
    raw = fetch_odds(sport="soccer_brazil_campeonato")
    odds = parse_odds(raw)
    print(f"{len(odds)} entradas de odds coletadas")
    print(odds[0] if odds else "Sem dados")

Treinando o modelo de previsão

Features relevantes

Quais features usar no modelo? Começa pelas básicas: aproveitamento dos times nos últimos 5 e 10 jogos, média de gols marcados e sofridos, histórico de confrontos diretos, posição na tabela, e se é jogo em casa ou fora. Depois você pode adicionar coisas mais avançadas como forma recente por posição no campo, descanso entre jogos e lesões em posições-chave.

Escolhendo o algoritmo

Para previsão de resultados esportivos, Random Forest e XGBoost costumam superar modelos mais simples. O Logistic Regression ainda é útil como baseline. O que muda mais os resultados não é o algoritmo — é a qualidade das features. Dá pra ter um Random Forest bem tunado que bate um XGBoost com features ruins.

python
# Python 3.11+
# model_trainer.py — treinando o modelo de previsão

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import log_loss, brier_score_loss
import pickle


def preparar_features(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]:
    """
    Prepara features e target para o modelo.
    Assume que df tem colunas de stats dos times.
    """
    feature_cols = [
        "home_win_rate_5", "away_win_rate_5",  # últimos 5 jogos
        "home_win_rate_10", "away_win_rate_10",  # últimos 10 jogos
        "home_goals_scored_avg", "home_goals_conceded_avg",
        "away_goals_scored_avg", "away_goals_conceded_avg",
        "h2h_home_win_rate",  # histórico entre si
        "home_position", "away_position",  # tabela
        "home_rest_days", "away_rest_days",  # descanso
    ]
    
    X = df[feature_cols].fillna(df[feature_cols].median())
    
    # Resultado: 0=away, 1=draw, 2=home
    le = LabelEncoder()
    y = le.fit_transform(df["resultado"])  # W/D/L do time da casa
    
    return X, pd.Series(y)


def treinar_modelo(X: pd.DataFrame, y: pd.Series) -> RandomForestClassifier:
    """
    Treina e valida o modelo com TimeSeriesSplit.
    IMPORTANTE: nunca usar k-fold padrão com dados temporais.
    """
    modelo = RandomForestClassifier(
        n_estimators=300,
        max_depth=8,
        min_samples_leaf=20,  # evita overfitting
        random_state=42,
        n_jobs=-1
    )
    
    # Validação temporal — respeita a ordem cronológica
    tscv = TimeSeriesSplit(n_splits=5)
    scores_ll = cross_val_score(modelo, X, y, cv=tscv, scoring="neg_log_loss")
    
    print(f"Log-loss médio: {-scores_ll.mean():.4f} (+/- {scores_ll.std():.4f})")
    
    modelo.fit(X, y)
    return modelo


def salvar_modelo(modelo, caminho: str = "models/predictor.pkl") -> None:
    with open(caminho, "wb") as f:
        pickle.dump(modelo, f)
    print(f"Modelo salvo em {caminho}")


# Uso
# df_historico = pd.read_csv("dados/historico_campeonato.csv")
# X, y = preparar_features(df_historico)
# modelo = treinar_modelo(X, y)
# salvar_modelo(modelo)

Implementando a lógica de apostas

O coração do bot é a função que decide se aposta ou não. Ela compara a probabilidade calculada pelo modelo com a probabilidade implícita na odd da casa. Se a diferença for maior que um threshold mínimo (o edge), a aposta tem valor positivo esperado.

python
# Python 3.11+
# bet_logic.py — lógica de decisão de apostas

import pickle
import numpy as np
from dataclasses import dataclass


@dataclass
class BetDecision:
    apostar: bool
    edge: float
    prob_modelo: float
    prob_implicita: float
    stake_pct: float
    stake_valor: float
    motivo: str


def odd_para_probabilidade(odd: float) -> float:
    """Converte odd decimal para probabilidade implícita."""
    return 1 / odd


def kelly_fraction(prob_win: float, odd: float, fraction: float = 0.25) -> float:
    """Kelly fracionário para calcular o stake."""
    b = odd - 1
    q = 1 - prob_win
    kelly = (b * prob_win - q) / b
    return max(kelly * fraction, 0)


def decidir_aposta(
    features: dict,
    odd: float,
    banca: float,
    modelo_path: str = "models/predictor.pkl",
    edge_minimo: float = 0.03,  # 3% de edge mínimo
    kelly_frac: float = 0.25
) -> BetDecision:
    """
    Decide se uma aposta deve ser feita.
    
    Args:
        features: dict com as features do jogo
        odd: odd decimal oferecida pela casa
        banca: banca atual em reais
        edge_minimo: edge mínimo para considerar a aposta
    """
    with open(modelo_path, "rb") as f:
        modelo = pickle.load(f)
    
    import pandas as pd
    X = pd.DataFrame([features])
    probs = modelo.predict_proba(X)[0]
    
    # Assume que índice 2 = vitória do time da casa
    prob_vitoria = probs[2]
    prob_implicita = odd_para_probabilidade(odd)
    
    edge = prob_vitoria - prob_implicita
    
    if edge < edge_minimo:
        return BetDecision(
            apostar=False,
            edge=edge,
            prob_modelo=prob_vitoria,
            prob_implicita=prob_implicita,
            stake_pct=0,
            stake_valor=0,
            motivo=f"Edge insuficiente: {edge:.2%} < {edge_minimo:.2%}"
        )
    
    stake_pct = kelly_fraction(prob_vitoria, odd, kelly_frac)
    stake_valor = banca * stake_pct
    
    return BetDecision(
        apostar=True,
        edge=edge,
        prob_modelo=prob_vitoria,
        prob_implicita=prob_implicita,
        stake_pct=stake_pct,
        stake_valor=stake_valor,
        motivo=f"Edge positivo: {edge:.2%}. Stake: R$ {stake_valor:.2f}"
    )

Deploy e monitoramento

Para produção, o setup mais simples é um VPS com Ubuntu (DigitalOcean, Linode ou Vultr), Python instalado, e o bot rodando via cron job ou com APScheduler dentro do próprio processo. Use supervisord para manter o processo vivo após crashes.

Checklist de deploy mínimo viável

  • VPS com pelo menos 1GB RAM e 1 vCPU
  • PostgreSQL configurado com backups automáticos
  • Variáveis de ambiente em .env, nunca hardcoded no código
  • Logging estruturado com loguru ou logging padrão
  • Alerta por Telegram ou email quando o bot detecta uma aposta ou encontra erro
  • Supervisord para reiniciar o processo automaticamente
  • Dashboard simples com métricas de ROI e número de apostas

Resultados e backtesting

Backtesting é o que separa o bot sério da brincadeira. A regra de ouro: nunca use dados que o modelo viu no treino para medir performance. Sempre reserve pelo menos 20% do histórico para teste fora da amostra. E use TimeSeriesSplit — nunca k-fold comum, que vaza dados do futuro pro passado.

Métricas que importam no backtesting: ROI total, ROI por mês, número de apostas, drawdown máximo, yield médio por aposta e gráfico de evolução da banca. Se o drawdown passou de 30% em algum período do histórico, o modelo vai sofrer mais em produção — variância real é sempre pior que backtesting.

Falácia do backtesting perfeito

Um backtesting com ROI de 20% ao mês é quase certamente overfitting.

Backtesting bom de verdade mostra períodos longos de drawdown e recuperação.

Quanto mais simples o modelo, menos chance de overfitting — mais confiável o backtest.

Performance real costuma ser 30-50% pior que o backtesting no mesmo período.