import { useMemo, useState } from "react"; import type { SuperbetLiveAdvice } from "@/domain/entities"; import { formatPercent } from "@/presentation/theme"; type MarketScanRow = NonNullable["marketScan"][number]; type Verdict = "apostar" | "quase" | "sem_valor" | "sem_odds"; interface MarketGroup { id: string; superbetName: string; matchMarket: (market: string) => boolean; note?: string; } const MARKET_GROUPS: MarketGroup[] = [ { id: "h2h", superbetName: "Resultado Final", matchMarket: (m) => m === "h2h", note: "Pode suspender ao vivo — use Total ou 2º Gol se sumir.", }, { id: "next_goal", superbetName: "2º Gol", matchMarket: (m) => m === "next_goal", }, { id: "totals", superbetName: "Total de Gols", matchMarket: (m) => m.startsWith("over_"), }, { id: "btts", superbetName: "Ambas as Equipes Marcam", matchMarket: (m) => m === "btts", }, ]; const VERDICT_STYLES: Record = { apostar: { label: "Apostar", className: "border-neon-green/40 bg-neon-green/15 text-neon-green", }, quase: { label: "Quase", className: "border-amber-500/35 bg-amber-500/10 text-amber-300", }, sem_valor: { label: "Sem valor", className: "border-white/12 bg-white/5 text-slate-400", }, sem_odds: { label: "Sem odds", className: "border-white/10 bg-white/[0.02] text-slate-500", }, }; interface RadarRow { group: MarketGroup; best: MarketScanRow | null; verdict: Verdict; verdictDetail: string; alternatives: MarketScanRow[]; } function resolveVerdict( best: MarketScanRow | null, threshold: number, tier?: string, ): { verdict: Verdict; detail: string } { if (!best) { return { verdict: "sem_odds", detail: "Superbet não enviou odd neste refresh" }; } const evPct = best.expectedValue * 100; const gap = threshold * 100 - evPct; if (best.meetsThreshold) { const stake = tier === "forte" || tier === "moderada" ? ` · stake sugerido R$ ${best.suggestedStakeValue.toFixed(0)}` : ""; return { verdict: "apostar", detail: `EV +${evPct.toFixed(1)}% · edge ${best.edgePp >= 0 ? "+" : ""}${best.edgePp.toFixed(1)} pp${stake}`, }; } if (best.expectedValue > 0) { return { verdict: "quase", detail: `EV +${evPct.toFixed(1)}% · faltam +${gap.toFixed(1)} pp para o limiar`, }; } return { verdict: "sem_valor", detail: `EV ${evPct.toFixed(1)}% · modelo abaixo da odd da casa`, }; } function buildFallbackScan(data: SuperbetLiveAdvice): MarketScanRow[] { const s = data.inplaySummary; const threshold = data.strategy?.minEdgeThreshold ?? 0.04; const rows: MarketScanRow[] = []; const push = ( market: string, outcome: string, label: string, prob: number | undefined, odd: number | undefined, ) => { if (prob == null || prob <= 0 || odd == null || odd <= 1) return; const implied = 1 / odd; const ev = prob * odd - 1; rows.push({ market, outcome, label, modelProb: prob, marketOdd: odd, impliedProb: implied, expectedValue: ev, edgePp: (prob - implied) * 100, suggestedStakePct: 0, suggestedStakeValue: 0, meetsThreshold: ev >= threshold, }); }; push("h2h", "1", `${data.homeTeam} vence`, s.probFinalHome, data.h2hOdds["1"]); push("h2h", "X", "Empate", s.probFinalDraw, data.h2hOdds["X"]); push("h2h", "2", `${data.awayTeam} vence`, s.probFinalAway, data.h2hOdds["2"]); push("over_2_5", "yes", "Over 2.5 gols", s.over25, data.aportes.find((a) => a.market === "over_2_5")?.marketOdd); push("over_3_5", "yes", "Over 3.5 gols", undefined, undefined); push("btts", "yes", "Ambos marcam", s.btts, data.bttsOdds.yes); push("btts", "no", "Ambos não marcam", s.btts != null ? 1 - s.btts : undefined, data.bttsOdds.no); push("next_goal", "home", `Próximo gol ${data.homeTeam}`, s.probNextGoalHome, data.nextGoalOdds.home); push("next_goal", "away", `Próximo gol ${data.awayTeam}`, s.probNextGoalAway, data.nextGoalOdds.away); return rows; } function buildRadar(data: SuperbetLiveAdvice): RadarRow[] { const scan = (data.strategy?.marketScan?.length ?? 0) > 0 ? data.strategy!.marketScan : buildFallbackScan(data); const threshold = data.strategy?.minEdgeThreshold ?? 0.04; const oppByKey = new Map( (data.strategy?.opportunities ?? []).map((o) => [`${o.market}:${o.outcome}`, o]), ); return MARKET_GROUPS.map((group) => { const rows = scan.filter((row) => group.matchMarket(row.market)); if (rows.length === 0) { return { group, best: null, verdict: "sem_odds" as Verdict, verdictDetail: "Superbet não enviou odd neste refresh", alternatives: [], }; } const best = rows.reduce((a, b) => (a.expectedValue > b.expectedValue ? a : b)); const opp = oppByKey.get(`${best.market}:${best.outcome}`); const { verdict, detail } = resolveVerdict(best, threshold, opp?.tier); return { group, best, verdict, verdictDetail: detail, alternatives: rows.filter((r) => r !== best).slice(0, 2), }; }); } function formatCapturedAt(iso: string | null): string { if (!iso) return "agora"; try { return new Intl.DateTimeFormat("pt-BR", { hour: "2-digit", minute: "2-digit", }).format(new Date(iso)); } catch { return "agora"; } } interface LiveMarketsGuidePanelProps { data: SuperbetLiveAdvice; } export function LiveMarketsGuidePanel({ data }: LiveMarketsGuidePanelProps) { const [showAvoid, setShowAvoid] = useState(false); const threshold = data.strategy?.minEdgeThreshold ?? 0.04; const radar = useMemo(() => buildRadar(data), [data]); const apostarCount = radar.filter((r) => r.verdict === "apostar").length; const hasCombos = (data.analysisCoverage?.combos.length ?? 0) > 0; return (

Modelo vs Superbet — mercados ao vivo

Cruzamento direto: odd da casa, P(modelo), EV e veredito · refresh{" "} {formatCapturedAt(data.capturedAt)} · limiar EV +{(threshold * 100).toFixed(0)}%

{data.strategy?.waitReason && apostarCount === 0 && (

{data.strategy.waitReason}

)}
{radar.map((row) => { const style = VERDICT_STYLES[row.verdict]; const highlighted = row.verdict === "apostar"; return ( ); })} {hasCombos && ( )}
Mercado Superbet Melhor palpite Odd Modelo EV Veredito

{row.group.superbetName}

{row.group.note && (

{row.group.note}

)} {row.alternatives.length > 0 && (

Também:{" "} {row.alternatives .map( (a) => `${a.label.split(" ").slice(-2).join(" ")} EV ${(a.expectedValue * 100).toFixed(0)}%`, ) .join(" · ")}

)}
{row.best?.label ?? "—"} {row.best ? row.best.marketOdd.toFixed(2) : "—"} {row.best ? formatPercent(row.best.modelProb) : "—"} {row.best ? ( = threshold ? "text-neon-green" : row.best.expectedValue > 0 ? "text-amber-300" : "text-slate-500" } > {row.best.expectedValue >= 0 ? "+" : ""} {(row.best.expectedValue * 100).toFixed(1)}% ) : ( "—" )} {style.label}

{row.verdictDetail}

Combos (&) Odds na casa ({data.analysisCoverage?.combos.length} tipos) — scan automático em breve Manual

Apostar = EV ≥ limiar ·{" "} Quase = EV positivo mas abaixo ·{" "} Sem valor = casa precificou acima do modelo. Siga também o bloco "O que fazer agora" para stake e correlações.

{showAvoid && (

Total por time, asiático, resultado correto, último gol, janelas de 5/10/15 min, 2º tempo, ímpar/par, método do gol, combos com "ou" — fora do modelo in-play.

)}
); }