Market-movement chart: the bookmaker line vs our model, per match
The odds history we have been capturing every 3h finally gets a face: de-vigged DraftKings probabilities drift as solid lines toward kickoff, with the model's numbers as dashed references — the honest benchmark framing, right on the match page. Renders only when at least two pre-kickoff lines exist. EN/DE copy included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,13 @@ import { ArrowLeft, ArrowRightLeft, CircleDot, Shield, Shirt, Volleyball } from
|
||||
import { Card, CardBody, CardHeader } from '@/components/ui/Card';
|
||||
import { WinProbBar } from '@/components/WinProbBar';
|
||||
import { WinProbTimeline, type TimelinePoint } from '@/components/WinProbTimeline';
|
||||
import { OddsMovement } from '@/components/OddsMovement';
|
||||
import { FormChips } from '@/components/FormChips';
|
||||
import { teamFlag } from '@/lib/teams';
|
||||
import { fmt, useFormat, useT } from '@/lib/i18n';
|
||||
import { inPlayProbs } from '@/lib/model/inplay';
|
||||
import { useTournamentStore } from '@/stores/tournamentStore';
|
||||
import type { Fixture, KeyPlayer, MatchPreview } from '@/lib/types';
|
||||
import type { Fixture, KeyPlayer, MatchPreview, OddsPoint } from '@/lib/types';
|
||||
|
||||
const num = (s: string | undefined) => (s ? Number(s.replace('%', '')) || 0 : 0);
|
||||
|
||||
@@ -84,6 +85,7 @@ export function MatchPreviewPage() {
|
||||
const { t, kickoffDay, kickoffTime } = useFormat();
|
||||
const [preview, setPreview] = useState<MatchPreview | null>(null);
|
||||
const [timeline, setTimeline] = useState<TimelinePoint[]>([]);
|
||||
const [oddsHistory, setOddsHistory] = useState<OddsPoint[]>([]);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
// Live score/minute arrive over the WebSocket snapshot — overlay them on the
|
||||
@@ -108,6 +110,11 @@ export function MatchPreviewPage() {
|
||||
.catch(() => {});
|
||||
void load();
|
||||
void loadTimeline();
|
||||
// line history changes on a 3h sweep — once per visit is plenty
|
||||
fetch(`/api/odds/${numParam}`)
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject()))
|
||||
.then((d: { history: OddsPoint[] }) => alive && setOddsHistory(d.history))
|
||||
.catch(() => {});
|
||||
const id = setInterval(() => {
|
||||
if (useTournamentStore.getState().snapshot?.fixtures.find((f) => f.num === Number(numParam))?.status === 'live') {
|
||||
void load();
|
||||
@@ -140,6 +147,8 @@ export function MatchPreviewPage() {
|
||||
const homeName = f.home.label;
|
||||
const awayName = f.away.label;
|
||||
const finished = f.status === 'finished';
|
||||
const koT = new Date(f.kickoff).getTime();
|
||||
const hasOddsChart = oddsHistory.filter((o) => o.home_ml != null && o.draw_ml != null && o.away_ml != null && o.captured_at <= koT).length >= 2;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -239,6 +248,16 @@ export function MatchPreviewPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* bookmaker line movement vs the model (benchmark only) */}
|
||||
{preview.prediction && hasOddsChart && (
|
||||
<Card>
|
||||
<CardHeader><span className="font-display font-bold text-ink">{t.match.oddsMovement}</span></CardHeader>
|
||||
<CardBody>
|
||||
<OddsMovement history={oddsHistory} model={preview.prediction.probs} kickoff={f.kickoff} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* form */}
|
||||
{(preview.form.home.length > 0 || preview.form.away.length > 0) && (
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user