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.
Leitura relacionada: Curso Node.js · Programa CrazyStack · ROI automação · Cursos CrazyStack.
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.
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 countCom 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.
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.
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
- Cria o arquivo requirements.txt: inclua requests, python-dotenv e qualquer outra dependência do projeto. Railway detecta automaticamente e instala.
- 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. - Cria conta no Railway (railway.app). Conecta seu repositório GitHub. Cada push para main faz deploy automático.
- 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.
- 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.
- 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.
- 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.pyou um systemd service.
Disclaimer legal e ético
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
Continue: Curso Node.js · Programa CrazyStack · ROI automação · Cursos CrazyStack.
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.