Pular para o conteúdo
Data Science

Bot de Apostas Esportivas com Python: Do Zero ao Deploy

A automação não precisa apostar por você — ela precisa te avisar quando uma oportunidade aparece. Nesse guia você constrói um bot completo: coleta de odds, motor de

Automatize a análise, não a aposta

Bot de apostas em Python: projeto técnico com risco legal — “deploy zero” não zera variance.

Arquitetura do sistema

O bot tem 4 componentes principais que rodam em loop contínuo. Cada um tem responsabilidade única e pode ser substituído ou melhorado independentemente.

Componente 1 — Coletor de Odds: busca odds de múltiplas fontes a cada N minutos. Usa The Odds API como fonte primária e pode ter scraping como fallback. Salva em cache local e banco SQLite para histórico.

Componente 2 — Motor de Análise: recebe as odds coletadas e calcula expected value para cada outcome usando seu modelo de probabilidade. Filtra por threshold mínimo de EV e compara com histórico para detectar movimentações suspeitas.

Componente 3 — Sistema de Alertas: envia mensagens formatadas via Telegram quando encontra oportunidades. Inclui od, EV calculado, casas recomendadas e link para o evento. Evita repetir o mesmo alerta a cada ciclo.

Componente 4 — Scheduler: orquestra todo o ciclo com intervalo configurável. Monitora saúde dos componentes e reinicia automaticamente em caso de falha.

Coleta de odds: API + fallback scraping

O coletor precisa ser resiliente. APIs caem, limites de rate são atingidos, e você não pode ficar sem dados no meio do dia.

python
import sqlite3
import requests
import os
import time
import logging
from datetime import datetime
from pathlib import Path

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler('bot.log'),
        logging.StreamHandler(),
    ]
)
logger = logging.getLogger(__name__)

DB_PATH = 'odds_history.db'

def init_db():
    """Inicializa banco SQLite para histórico de odds."""
    conn = sqlite3.connect(DB_PATH)
    conn.execute('''
        CREATE TABLE IF NOT EXISTS odds_snapshots (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            captured_at TEXT NOT NULL,
            sport TEXT NOT NULL,
            home_team TEXT NOT NULL,
            away_team TEXT NOT NULL,
            outcome TEXT NOT NULL,
            bookmaker TEXT NOT NULL,
            odd REAL NOT NULL,
            event_id TEXT
        )
    ''')
    conn.execute('''
        CREATE TABLE IF NOT EXISTS alerts_sent (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            sent_at TEXT NOT NULL,
            event_key TEXT NOT NULL UNIQUE,
            ev_at_send REAL NOT NULL
        )
    ''')
    conn.commit()
    conn.close()

class OddsCollector:
    def __init__(self, api_key: str, sports: list[str]):
        self.api_key = api_key
        self.sports = sports
        self.base_url = 'https://api.the-odds-api.com/v4'
    
    def fetch_sport(self, sport: str) -> list[dict]:
        """Busca odds de um esporte. Retorna lista vazia se falhar."""
        try:
            resp = requests.get(
                f'{self.base_url}/sports/{sport}/odds',
                params={
                    'apiKey': self.api_key,
                    'regions': 'eu,uk',
                    'markets': 'h2h',
                    'oddsFormat': 'decimal',
                },
                timeout=15,
            )
            resp.raise_for_status()
            remaining = resp.headers.get('x-requests-remaining', '?')
            logger.info(f'{sport}: {len(resp.json())} jogos | Requests restantes: {remaining}')
            return resp.json()
        except Exception as e:
            logger.error(f'Erro ao buscar odds de {sport}: {e}')
            return []
    
    def collect_all(self) -> list[dict]:
        """Coleta de todos os esportes configurados."""
        all_games = []
        for sport in self.sports:
            games = self.fetch_sport(sport)
            for game in games:
                game['sport'] = sport
            all_games.extend(games)
            time.sleep(0.5)  # Respeita rate limit
        return all_games
    
    def save_to_db(self, games: list[dict]) -> int:
        """Salva snapshot atual das odds no banco. Retorna count de rows inseridas."""
        conn = sqlite3.connect(DB_PATH)
        now = datetime.utcnow().isoformat()
        count = 0
        
        for game in games:
            home = game['home_team']
            away = game['away_team']
            sport = game.get('sport', 'unknown')
            event_id = game.get('id')
            
            for bookmaker in game.get('bookmakers', []):
                for market in bookmaker.get('markets', []):
                    for outcome in market.get('outcomes', []):
                        conn.execute(
                            'INSERT INTO odds_snapshots VALUES (NULL,?,?,?,?,?,?,?,?)',
                            (now, sport, home, away,
                             outcome['name'], bookmaker['title'],
                             outcome['price'], event_id)
                        )
                        count += 1
        
        conn.commit()
        conn.close()
        logger.info(f'Salvou {count} registros de odds no banco')
        return count

Com o histórico de odds no SQLite, você consegue depois analisar como as odds se movimentaram antes de cada jogo — valioso para features de ML e para entender padrões de mercado.

Motor de análise e filtros de value

O motor recebe os dados brutos e calcula EV para cada oportunidade, aplicando filtros configuráveis.

python
from dataclasses import dataclass, field

@dataclass
class BettingOpportunity:
    home_team: str
    away_team: str
    sport: str
    outcome: str
    best_odd: float
    bookmaker: str
    my_probability: float
    implied_probability: float
    expected_value: float
    event_key: str = ''
    commence_time: str = ''

class AnalysisEngine:
    def __init__(
        self,
        probability_model,  # Qualquer objeto com .predict_probabilities(home, away) -> dict
        min_ev: float = 0.04,       # EV mínimo de 4%
        min_odd: float = 1.5,       # Odd mínima para considerar
        max_odd: float = 8.0,       # Odds muito altas são ruído
        min_bookmakers: int = 3,    # Mínimo de casas com esse jogo
    ):
        self.model = probability_model
        self.min_ev = min_ev
        self.min_odd = min_odd
        self.max_odd = max_odd
        self.min_bookmakers = min_bookmakers
    
    def _get_best_odds(self, game: dict) -> dict[str, dict]:
        """Retorna a melhor odd por outcome com nome da casa."""
        best = {}
        home, away = game['home_team'], game['away_team']
        
        for bookmaker in game.get('bookmakers', []):
            for market in bookmaker.get('markets', []):
                if market['key'] != 'h2h':
                    continue
                for o in market['outcomes']:
                    key = 'home' if o['name'] == home else ('away' if o['name'] == away else 'draw')
                    if key not in best or o['price'] > best[key]['odd']:
                        best[key] = {'odd': o['price'], 'bookmaker': bookmaker['title']}
        return best
    
    def analyze_game(self, game: dict) -> list[BettingOpportunity]:
        """Analisa um jogo e retorna oportunidades com EV positivo."""
        home, away = game['home_team'], game['away_team']
        bookmaker_count = len(game.get('bookmakers', []))
        
        if bookmaker_count < self.min_bookmakers:
            return []  # Mercado raso, menos confiável
        
        best_odds = self._get_best_odds(game)
        if len(best_odds) < 2:
            return []
        
        try:
            my_probs = self.model.predict_probabilities(home, away)
        except Exception:
            return []
        
        opportunities = []
        for outcome, info in best_odds.items():
            odd = info['odd']
            if not (self.min_odd <= odd <= self.max_odd):
                continue
            
            my_prob = my_probs.get(outcome, 0)
            if my_prob <= 0:
                continue
            
            implied_prob = 1 / odd
            ev = (my_prob * odd) - 1
            
            if ev < self.min_ev:
                continue
            
            event_key = f"{game.get('id', '')}_{outcome}"
            
            opportunities.append(BettingOpportunity(
                home_team=home,
                away_team=away,
                sport=game.get('sport', ''),
                outcome=outcome,
                best_odd=round(odd, 2),
                bookmaker=info['bookmaker'],
                my_probability=round(my_prob, 3),
                implied_probability=round(implied_prob, 3),
                expected_value=round(ev, 4),
                event_key=event_key,
                commence_time=game.get('commence_time', ''),
            ))
        
        return opportunities
    
    def analyze_all(self, games: list[dict]) -> list[BettingOpportunity]:
        """Analisa todos os jogos e retorna oportunidades ordenadas por EV."""
        all_opportunities = []
        for game in games:
            all_opportunities.extend(self.analyze_game(game))
        return sorted(all_opportunities, key=lambda x: x.expected_value, reverse=True)

O filtro min_bookmakers=3 é importante: odds em jogos com poucas casas tendem a ser menos confiáveis e podem indicar eventos de menor qualidade ou baixo volume de apostas.

Sistema de alertas via Telegram

O Telegram é a melhor opção para alertas — é gratuito, tem API excelente e chega no celular na hora. Cria um bot pelo @BotFather em 2 minutos.

python
import sqlite3
import requests
from datetime import datetime

class TelegramAlerter:
    def __init__(self, bot_token: str, chat_id: str):
        self.bot_token = bot_token
        self.chat_id = chat_id
        self.api_url = f'https://api.telegram.org/bot{bot_token}'
    
    def send_message(self, text: str, parse_mode: str = 'HTML') -> bool:
        """Envia mensagem para o chat. Retorna True se sucesso."""
        try:
            resp = requests.post(
                f'{self.api_url}/sendMessage',
                json={'chat_id': self.chat_id, 'text': text, 'parse_mode': parse_mode},
                timeout=10,
            )
            return resp.status_code == 200
        except Exception as e:
            logger.error(f'Falha ao enviar Telegram: {e}')
            return False
    
    def format_opportunity(self, opp: 'BettingOpportunity') -> str:
        """Formata uma oportunidade como mensagem Telegram."""
        outcome_labels = {'home': 'Casa', 'away': 'Fora', 'draw': 'Empate'}
        ev_pct = opp.expected_value * 100
        edge = (opp.my_probability - opp.implied_probability) * 100
        
        return (
            f'<b>VALUE BET ENCONTRADO</b>\n'
            f'<b>Jogo:</b> {opp.home_team} vs {opp.away_team}\n'
            f'<b>Esporte:</b> {opp.sport}\n'
            f'<b>Aposta:</b> {outcome_labels.get(opp.outcome, opp.outcome)}\n'
            f'<b>Odd:</b> {opp.best_odd} ({opp.bookmaker})\n'
            f'<b>EV:</b> +{ev_pct:.1f}%\n'
            f'<b>Minha prob:</b> {opp.my_probability:.1%} vs casa {opp.implied_probability:.1%} '
            f'(edge: +{edge:.1f}pp)\n'
            f'<b>Começa:</b> {opp.commence_time[:16].replace("T", " ")} UTC'
        )
    
    def alert_opportunity(self, opp: 'BettingOpportunity') -> bool:
        """Envia alerta se essa oportunidade ainda não foi alertada."""
        conn = sqlite3.connect(DB_PATH)
        already_sent = conn.execute(
            'SELECT 1 FROM alerts_sent WHERE event_key = ?', (opp.event_key,)
        ).fetchone()
        
        if already_sent:
            conn.close()
            return False  # Já avisou esse evento
        
        message = self.format_opportunity(opp)
        sent = self.send_message(message)
        
        if sent:
            conn.execute(
                'INSERT OR IGNORE INTO alerts_sent VALUES (NULL, ?, ?, ?)',
                (datetime.utcnow().isoformat(), opp.event_key, opp.expected_value)
            )
            conn.commit()
            logger.info(f'Alerta enviado: {opp.home_team} vs {opp.away_team} | EV: {opp.expected_value:.2%}')
        
        conn.close()
        return sent

# Configuração via variáveis de ambiente
TELEGRAM_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID')

# Como pegar seu CHAT_ID:
# 1. Crie o bot com @BotFather
# 2. Mande qualquer mensagem para o bot
# 3. Acesse: https://api.telegram.org/bot<TOKEN>/getUpdates
# 4. Copie o 'id' dentro de 'chat'

O sistema de deduplicação via alerts_sent no SQLite garante que você não receba 50 mensagens do mesmo jogo ao longo do dia.

Deploy em Railway ou VPS

  1. Cria o arquivo requirements.txt: inclua requests, python-dotenv e qualquer outra dependência do projeto. Railway detecta automaticamente e instala.
  2. Adiciona um Procfile: na raiz do projeto, crie o arquivo Procfile com o conteúdo worker: python main.py. Isso define que é um processo worker, não um servidor web.
  3. Cria conta no Railway (railway.app). Conecta seu repositório GitHub. Cada push para main faz deploy automático.
  4. Configura variáveis de ambiente no Railway: em Settings → Variables, adicione ODDS_API_KEY, TELEGRAM_BOT_TOKEN e TELEGRAM_CHAT_ID. Nunca suba essas variáveis no git.
  5. Adiciona persistência de dados: por padrão Railway reseta o filesystem. Adicione um volume para a pasta onde está o SQLite e os logs. Em Settings → Volumes, monta em /app/data.
  6. Monitora os logs: Railway tem painel de logs em tempo real. Verifique nas primeiras horas se o bot está coletando dados e enviando alertas corretamente.
  7. Alternativa VPS: se preferir controle total, use qualquer VPS de R$30-60/mês (Hostinger, Digital Ocean). Instala Python, faz clone do repo, configura o .env e roda com screen python main.py ou um systemd service.

Leia antes de colocar dinheiro real

Apostas esportivas são ilegais ou reguladas de formas diferentes dependendo do país e estado. No Brasil, a regulamentação de apostas esportivas entrou em vigor em 2025 — verifique as regras vigentes. Este código é para fins educacionais e de pesquisa em Data Science. Nenhum sistema de apostas garante lucro. Expected Value positivo é uma expectativa estatística de longo prazo — no curto prazo você pode perder dinheiro mesmo com edge matemático. Nunca aposte mais do que você pode perder completamente. Automação de apostas viola os termos de serviço da maioria das casas e pode resultar em banimento permanente. O autor não se responsabiliza por perdas financeiras decorrentes do uso deste código.

Perguntas frequentes

Bot de apostas com Python: legal/ético?

Apostas têm risco legal e financeiro. Trate como projeto técnico educativo, não conselho.

Deploy zero?

“Zero” quase sempre esconde trade-offs de custo e ops.

IA no bot?

Pode ranquear sinais. Não elimina variance.

Quando parar?

Se cruzar ToS, lei ou bankroll que você não aguenta.

Continue explorando

Perguntas frequentes

Bot de apostas com Python: legal/ético?

Apostas têm risco legal e financeiro. Trate como projeto técnico educativo, não conselho.

Deploy zero?

“Zero” quase sempre esconde trade-offs de custo e ops.

IA no bot?

Pode ranquear sinais. Não elimina variance.

Quando parar?

Se cruzar ToS, lei ou bankroll que você não aguenta.