/* QUANTA · Detail enrichments + Alerts panel - QAlertsPanel: slide-in side panel listing recent edge events - QKellyCard: best-edge → Kelly/3 stake suggestion - QEventLog: recent ticks/score events for a match - QEvolutionChart: multi-bookmaker line chart per side Loads after quanta-app.jsx and assigns components on window. */ const { useEffect: useEffect_d, useState: useState_d, useMemo: useMemo_d, useRef: useRef_d } = React; /* ────────────────────────────────────────────────────────── ALERTS PANEL ────────────────────────────────────────────────────────── */ function QAlertsPanel({ open, onClose, alerts, tick, onOpenMatch }) { const closeRef = useRef_d(null); // Lock body scroll while open useEffect_d(() => { if (open) { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; } }, [open]); // Close on Esc useEffect_d(() => { if (!open) return; const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); // Focus trap: transfer focus to the close button on open so keyboard // users land inside the dialog instead of the trigger button outside. useEffect_d(() => { if (open && closeRef.current) closeRef.current.focus(); }, [open]); return ( <>
); } /* ────────────────────────────────────────────────────────── KELLY / 3 STAKE CARD ────────────────────────────────────────────────────────── */ function QKellyCard({ match, divisor = 3 }) { // Find best positive edge across all BMs and both sides. // Kelly fraction = edge / (odds - 1). We display Kelly/divisor (user-set). const candidates = []; for (const [name, b] of Object.entries(match.bm)) { if (b.e1 != null && b.e1 > 0 && b.p1 != null) candidates.push({ bm: name, side: "p1", odds: b.p1, edge: b.e1 }); if (b.e2 != null && b.e2 > 0 && b.p2 != null) candidates.push({ bm: name, side: "p2", odds: b.p2, edge: b.e2 }); // Football 1X2 markets expose a draw side. Tennis dicts never carry // eDraw so this branch is a natural no-op there. if (b.eDraw != null && b.eDraw > 0 && b.draw != null) candidates.push({ bm: name, side: "draw", odds: b.draw, edge: b.eDraw }); } candidates.sort((a, b) => b.edge - a.edge); const best = candidates[0]; if (!best) { return (
Kelly / {divisor} · stake suggéré
0%
Aucun edge positif disponible — ne rien parier.
Le système ne recommande de mise que lorsqu'un BM offre une cote au-dessus de la baseline Betfair LAY.
); } const pickName = best.side === "draw" ? "Match nul" : (best.side === "p1" ? match.p1 : match.p2); const pickShortName = best.side === "draw" ? "X (match nul)" : pickName.split(",")[0]; const kelly = best.edge / (best.odds - 1); const kellyDiv = kelly / divisor; const bfBaseline = match.bf ? (best.side === "draw" ? match.bf.draw : (best.side === "p1" ? match.bf.p1 : match.bf.p2)) : null; // HelpTip lives on window (defined in quanta-app.jsx, assigned globally by babel). const Help = (typeof HelpTip !== "undefined") ? HelpTip : (({children}) => children); return (
Kelly / {divisor} · stake suggéré
{(kellyDiv * 100).toFixed(2)}%
de la bankroll · {pickShortName} @ {best.odds.toFixed(2)} chez {best.bm}
Edge
+{(best.edge * 100).toFixed(2)}%
Kelly full
{(kelly * 100).toFixed(2)}%
Sur 1000 €
{(kellyDiv * 1000).toFixed(2)} €
Profit attendu
+{(kellyDiv * 1000 * (best.odds - 1)).toFixed(2)} €
f* = edge / (odds − 1) · on prend f* / {divisor} (modifiable dans ton profil) · base : BF LAY {bfBaseline != null ? bfBaseline.toFixed(2) : "—"}
); } /* ────────────────────────────────────────────────────────── EVENT LOG ────────────────────────────────────────────────────────── */ function QEventLog({ match, tick }) { const events = match.events || []; return (
Event log
{events.length === 0 ? (
) : events.map((ev, i) => { const age = Math.max(0, Date.now() / 1000 - ev.ts); const t = age < 60 ? `${Math.round(age)}s` : `${Math.round(age/60)}m`; return (
{t}
{ev.kind || "tick"} {ev.text}
); })}
); } /* ────────────────────────────────────────────────────────── EVOLUTION CHART (multi-bookmaker line chart per side) ────────────────────────────────────────────────────────── */ function QEvolutionChart({ match, side, height = 220 }) { // Collect all series for this side; BF goes dashed as baseline. const series = match.history[side]; const names = Object.keys(series); if (!names.length) return null; const allPts = names.flatMap(n => series[n]); const min = Math.min(...allPts); const max = Math.max(...allPts); const pad = 0.04 * (max - min || 1); const yMin = min - pad; const yMax = max + pad; const range = yMax - yMin; const n = series[names[0]].length; const w = 520, h = height; const padding = { l: 38, r: 8, t: 8, b: 22 }; const innerW = w - padding.l - padding.r; const innerH = h - padding.t - padding.b; const xFor = (i) => padding.l + (i / Math.max(1, n - 1)) * innerW; const yFor = (v) => padding.t + innerH - ((v - yMin) / range) * innerH; // y-axis ticks const ySteps = 4; const yTicks = []; for (let k = 0; k <= ySteps; k++) { const v = yMin + (range * k / ySteps); yTicks.push({ v, y: yFor(v) }); } const playerName = side === "p1" ? match.p1.split(",")[0] : match.p2.split(",")[0]; const currentVal = match.bf[side]; return (
{playerName} · cotes BF {currentVal != null ? currentVal.toFixed(2) : "—"}
{/* y gridlines */} {yTicks.map((t, i) => ( {t.v.toFixed(2)} ))} {/* x labels (only first / last) */} -24t now {/* Lines */} {names.map(name => { const isBF = name === "Betfair"; const color = isBF ? "var(--warn)" : (window.MOCK.BMS[name] || {}).color; const pts = series[name].map((v, i) => `${xFor(i).toFixed(1)},${yFor(v).toFixed(1)}`).join(" "); return ( ); })} {/* Dot at last point of BF */}
BF LAY {Object.keys(match.bm).map(name => ( {name} ))}
); } function QChartPanel({ match }) { const [chartSide, setChartSide] = useState_d("both"); // 'both' | 'p1' | 'p2' return (
Évolution des cotes · 24 derniers ticks
{(chartSide === "both" || chartSide === "p1") && } {(chartSide === "both" || chartSide === "p2") && }
); } Object.assign(window, { QAlertsPanel, QKellyCard, QEventLog, QEvolutionChart, QChartPanel });