Pular para o conteúdo
Tutoriais

Dashboard de Apostas Esportivas com React

Tutorial completo para construir um dashboard que mostra odds em tempo real, detecta arbitragens e exibe tendências de mercado. React + Chart.js + APIs de odds.

Por que um dashboard muda tudo

Dashboard de Apostas Esportivas com React. Tutorial completo para construir um dashboard que mostra odds em tempo real, detecta arbitragens e exibe tendências de mercado. React + Chart.js + APIs de odds.

Dashboard visual muda o jogo

Galera, vou contar uma coisa que aprendi na prática: dados em tabela são ótimos pra armazenar informação, mas péssimos pra tomar decisão rápida. Quando uma odd de arbitragem aparece, ela dura minutos — às vezes segundos. Você não vai ficar scrollando uma planilha com 500 linhas tentando achar o número certo enquanto a oportunidade desaparece.

Um dashboard bem feito mostra pra você em 2 segundos o que importa. Gráfico de linha mostrando pra onde a odd está indo. Cards coloridos destacando arbitragens ativas. Tabela filtrada com só as oportunidades relevantes. Tudo atualizado em tempo real.

A stack que vamos usar aqui é React com TypeScript no front, Chart.js pra gráficos (via react-chartjs-2), e The Odds API como fonte de dados. Dá pra deployar como PWA, instalar no celular e usar como um app nativo. Custo de infraestrutura: zero se você usar Vercel ou Netlify no plano gratuito.

O projeto final tem três telas: overview com cards de métricas e gráfico de odds, tabela de arbitragens em tempo real, e tela de detalhes por jogo com histórico de movimento. Vamos construir tudo isso passo a passo.

Arquitetura React + Chart.js

Antes de sair codando, vale pensar na arquitetura. A estrutura de pastas que funciona bem pra esse tipo de projeto separa as preocupações em camadas: hooks pra lógica de dados, components pra UI, e services pra comunicação com APIs.

  1. Crie o projeto com Vite: npm create vite@latest betting-dashboard -- --template react-ts
  2. Instale as dependências: npm install chart.js react-chartjs-2 axios tailwindcss @headlessui/react
  3. Configure Tailwind CSS no projeto seguindo a documentação oficial
  4. Crie a estrutura de pastas: src/hooks, src/components, src/services, src/types
  5. Defina os tipos TypeScript para odds, games e arbitragens em src/types/betting.ts
  6. Implemente o service de API em src/services/oddsApi.ts
  7. Crie os hooks customizados: useOdds, useArbitrages, useOddsHistory

Estrutura de componentes

O componente raiz é o Dashboard, que orquestra três painéis: StatsOverview (cards com métricas), OddsChart (gráfico de linha) e ArbitrageTable (tabela interativa). Cada componente recebe dados via props e não faz fetch sozinho — toda a lógica de dados fica nos hooks.

typescript
// src/types/betting.ts
export interface OddsData {
  gameId: string;
  homeTeam: string;
  awayTeam: string;
  commenceTime: string;
  bookmaker: string;
  oddHome: number;
  oddAway: number;
  oddDraw: number;
  collectedAt: string;
}

export interface Arbitrage {
  gameId: string;
  homeTeam: string;
  awayTeam: string;
  book1: string;
  book2: string;
  book3?: string;
  oddHome: number;
  oddAway: number;
  oddDraw: number;
  profit: number; // percentual de lucro garantido
}

export interface StatsCard {
  label: string;
  value: string | number;
  change?: number; // variação percentual
  trend: 'up' | 'down' | 'neutral';
}

A tipagem forte com TypeScript aqui não é frescura. Quando você está lidando com dados financeiros e decisões que envolvem dinheiro, um campo undefined que passa batido pode custar caro. TypeScript pega isso no build.

Gráfico de odds em tempo real

O gráfico principal do dashboard mostra a evolução das odds de um jogo ao longo do tempo. Cada casa de apostas é uma linha no gráfico. Quando as linhas divergem muito, pode ter arbitragem. Quando convergem, o mercado está em consenso. Essa visualização sozinha já vale mais que qualquer planilha.

Hook de dados para o gráfico

typescript
// src/hooks/useOddsHistory.ts
import { useState, useEffect, useCallback } from 'react';
import { OddsData } from '../types/betting';
import { fetchOdds } from '../services/oddsApi';

export function useOddsHistory(gameId: string, intervalMs = 300000) {
  const [history, setHistory] = useState<OddsData[]>([]);
  const [loading, setLoading] = useState(true);

  const poll = useCallback(async () => {
    try {
      const data = await fetchOdds(gameId);
      setHistory(prev => [...prev, ...data]);
    } catch (err) {
      console.error('Erro ao buscar odds:', err);
    } finally {
      setLoading(false);
    }
  }, [gameId]);

  useEffect(() => {
    poll();
    const timer = setInterval(poll, intervalMs);
    return () => clearInterval(timer);
  }, [poll, intervalMs]);

  const chartData = {
    labels: [...new Set(history.map(h => 
      new Date(h.collectedAt).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
    ))],
    datasets: Object.entries(
      history.reduce((acc, h) => {
        if (!acc[h.bookmaker]) acc[h.bookmaker] = [];
        acc[h.bookmaker].push(h.oddHome);
        return acc;
      }, {} as Record<string, number[]>)
    ).map(([bookmaker, odds], i) => ({
      label: bookmaker,
      data: odds,
      borderColor: ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6'][i % 5],
      tension: 0.3,
      pointRadius: 2,
    })),
  };

  return { history, chartData, loading };
}

Componente do gráfico

typescript
// src/components/OddsChart.tsx
import { Line } from 'react-chartjs-2';
import {
  Chart as ChartJS, CategoryScale, LinearScale,
  PointElement, LineElement, Title, Tooltip, Legend,
} from 'chart.js';

ChartJS.register(
  CategoryScale, LinearScale, PointElement,
  LineElement, Title, Tooltip, Legend
);

interface OddsChartProps {
  chartData: {
    labels: string[];
    datasets: Array<{
      label: string;
      data: number[];
      borderColor: string;
      tension: number;
      pointRadius: number;
    }>;
  };
  title: string;
}

export function OddsChart({ chartData, title }: OddsChartProps) {
  const options = {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      legend: { position: 'top' as const, labels: { color: '#a3a3a3' } },
      title: { display: true, text: title, color: '#e5e5e5', font: { size: 16 } },
      tooltip: {
        callbacks: {
          label: (ctx: any) => `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(2)}`,
        },
      },
    },
    scales: {
      x: { ticks: { color: '#737373' }, grid: { color: '#262626' } },
      y: {
        ticks: { color: '#737373', callback: (v: any) => v.toFixed(2) },
        grid: { color: '#262626' },
      },
    },
  };

  return (
    <div className="bg-neutral-900 rounded-xl p-6 h-[400px]">
      <Line data={chartData} options={options} />
    </div>
  );
}

O polling de 5 minutos é um bom ponto de partida. API gratuita tem limite, então não dá pra fazer requisição a cada 10 segundos. Mas 5 minutos pega a maioria dos movimentos relevantes. Pra quem quer algo mais agressivo, WebSocket com a Betfair API é o caminho — mas aí é outro nível de complexidade.

Tabela de arbitragens em tempo real

A tabela de arbitragens é a funcionalidade mais valiosa do dashboard. Arbitragem esportiva é quando você consegue apostar em todos os resultados possíveis de um jogo, usando odds de casas diferentes, e garantir lucro independente do resultado. Acontece quando a soma dos inversos das odds de casas diferentes é menor que 1.

Lógica de detecção de arbitragem

typescript
// src/hooks/useArbitrages.ts
import { useMemo } from 'react';
import { OddsData, Arbitrage } from '../types/betting';

export function useArbitrages(oddsData: OddsData[]): Arbitrage[] {
  return useMemo(() => {
    const byGame = new Map<string, OddsData[]>();
    oddsData.forEach(o => {
      const list = byGame.get(o.gameId) || [];
      list.push(o);
      byGame.set(o.gameId, list);
    });

    const arbitrages: Arbitrage[] = [];

    for (const [gameId, odds] of byGame) {
      const bestHome = odds.reduce((best, o) =>
        o.oddHome > best.oddHome ? o : best, odds[0]);
      const bestAway = odds.reduce((best, o) =>
        o.oddAway > best.oddAway ? o : best, odds[0]);
      const bestDraw = odds.reduce((best, o) =>
        o.oddDraw > best.oddDraw ? o : best, odds[0]);

      const impliedProb = (1 / bestHome.oddHome)
        + (1 / bestAway.oddAway)
        + (1 / bestDraw.oddDraw);

      if (impliedProb < 1) {
        const profit = ((1 / impliedProb) - 1) * 100;
        arbitrages.push({
          gameId,
          homeTeam: bestHome.homeTeam,
          awayTeam: bestHome.awayTeam,
          book1: bestHome.bookmaker,
          book2: bestAway.bookmaker,
          book3: bestDraw.bookmaker,
          oddHome: bestHome.oddHome,
          oddAway: bestAway.oddAway,
          oddDraw: bestDraw.oddDraw,
          profit: Math.round(profit * 100) / 100,
        });
      }
    }

    return arbitrages.sort((a, b) => b.profit - a.profit);
  }, [oddsData]);
}

Quando impliedProb é menor que 1, você tem arbitragem. Na prática, arbitragens acima de 1% de lucro são raras e duram pouco. Arbitragens de 0.5% aparecem mais, mas o lucro é tão pequeno que só vale se você opera com banca grande e tem as contas prontas nas casas certas. O dashboard te mostra tudo isso em tempo real pra você decidir se vale ou não.

Um detalhe que a maioria dos tutoriais ignora: casas de apostas cancelam apostas que fazem parte de arbitragem identificada. Não é ilegal, mas elas não gostam. Se você fizer arbitragem consistentemente, suas contas vão ser limitadas. Dito isso, o conhecimento de como detectar arbitragem é valioso pra entender a eficiência do mercado — mesmo que você não vá operar diretamente.

Deploy como PWA

O dashboard ganha muito quando roda no celular. Você recebe a notificação de arbitragem via Telegram (usando o bot que ensinamos no artigo sobre Telegram Bot para value bets) e abre o dashboard direto no celular pra analisar os detalhes.

Configurando PWA com Vite

Com o plugin vite-plugin-pwa, transformar sua app React em PWA leva 5 minutos. Instale: npm install vite-plugin-pwa. No vite.config.ts, adicione o plugin com as configurações de manifest — ícone, nome, cores de tema. O service worker é gerado automaticamente e faz cache dos assets estáticos.

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',
      manifest: {
        name: 'Betting Dashboard',
        short_name: 'BetDash',
        description: 'Dashboard de odds em tempo real',
        theme_color: '#0a0a0a',
        background_color: '#0a0a0a',
        display: 'standalone',
        icons: [
          { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
        ],
      },
    }),
  ],
});

Deploy na Vercel é um git push. Netlify também. Ambos oferecem HTTPS gratuito, que é obrigatório pra PWA funcionar. Depois do deploy, acessa pelo celular, clica em 'Adicionar à tela inicial' e pronto — vira um app.

Checklist de deploy do dashboard

  • Variáveis de ambiente configuradas (API key) no provider de deploy
  • Plugin PWA configurado com ícones nos tamanhos corretos
  • Service worker registrado e funcionando offline para assets estáticos
  • HTTPS ativo (obrigatório para PWA)
  • Responsividade testada em telas de 375px até 1440px
  • Polling de odds funcionando sem vazar timers (cleanup no useEffect)
  • Meta tags de SEO e Open Graph configuradas

Vercel

Deploy instantâneo com integração nativa de projetos React/Vite.

+ Prós

  • • Deploy automático a cada push no GitHub
  • • HTTPS gratuito e CDN global
  • • Preview deploys por branch — ótimo pra testar features
  • • Plano gratuito generoso pra projetos pessoais

− Contras

  • • Sem backend — pra rodar o polling de odds, precisa de um servidor separado
  • • Limites de bandwidth no plano free

Railway

Full-stack deploy com banco e cron jobs no mesmo lugar.

+ Prós

  • • Roda backend Node.js junto com o front
  • • Suporta cron jobs nativamente — ideal pro polling de odds
  • • Banco PostgreSQL integrado
  • • Logs e métricas no dashboard

− Contras

  • • Plano gratuito com crédito limitado por mês
  • • CDN menos robusta que Vercel pra assets estáticos

VPS (DigitalOcean / Hetzner)

Controle total do servidor. Mais trabalho, mais liberdade.

+ Prós

  • • Controle total sobre infraestrutura e schedule
  • • Custo fixo previsível — a partir de $4/mês
  • • Roda qualquer coisa: cron, banco, scripts Python e Node juntos

− Contras

  • • Você gerencia tudo: updates de segurança, SSL, monitoramento
  • • Precisa configurar Nginx/Caddy como reverse proxy
  • • Sem deploy automático — precisa configurar CI/CD manualmente

Pronto pra construir seus próprios dashboards?

Esse dashboard é um projeto de portfólio que impressiona em entrevistas. Mostra que você sabe React, TypeScript, integração com APIs, visualização de dados e deploy. Se quiser ir mais fundo, conecte com os modelos estatísticos do artigo de Poisson/ELO/xG e com o bot de Telegram pra ter um sistema completo de análise e alertas. A CrazyStack ensina essa stack inteira na prática.