Skip to content

Commit 59e89ee

Browse files
valvesssclaude
andcommitted
feat(worldcup): reconcile R32 first results — frozen predictions vs reality
First 3 knockout games resolved (web-sourced, real). The pre-registered ledger reconciles them: Brazil 2-1 Japan (favourite won), Germany 1-1 Paraguay (out on pens — our biggest favourite), Netherlands 1-1 Morocco (out on pens — our only toss-up). A knockout draw settles the favourite 1X2 bet as a loss; the shootout decides who advances, kept separately. - wc_manual_results.json: persistent, sourced KO results applied in reconciliation. - predict_worldcup: manual reconcile + resolved_detail payload. - /worldcup: 'first results vs prediction' section (score, pens, our call, ✓/✗). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0815ed0 commit 59e89ee

5 files changed

Lines changed: 241 additions & 104 deletions

File tree

site/src/data/wc_live.json

Lines changed: 122 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,115 +2,37 @@
22
"meta": {
33
"data_date": "2026-06-28",
44
"n_played_wc": 1037,
5-
"n_upcoming": 15,
5+
"n_upcoming": 12,
66
"source": "martj42/international_results",
77
"sha256": "df6a30676640"
88
},
99
"next": {
10-
"match_id": "2026-06-29|Brazil|Japan",
11-
"date": "2026-06-29",
12-
"home": "Brazil",
13-
"away": "Japan",
14-
"neutral": true,
15-
"fav_team": "Brazil",
16-
"fav_pick": "H",
17-
"p_fav": 0.556,
18-
"skew_pred": -0.2255,
19-
"win_payoff": 0.8,
20-
"lose_payoff": -1.0,
21-
"verdict": "neg",
22-
"p_home": 0.556,
23-
"p_draw": 0.252,
24-
"p_away": 0.192,
25-
"home_flag": "br",
26-
"away_flag": "jp",
27-
"adv_home": 0.7066,
28-
"adv_away": 0.2934,
29-
"pen_home": 0.5973,
30-
"xg_home": 1.82,
31-
"xg_away": 0.56,
32-
"score_home": 1,
33-
"score_away": 0
34-
},
35-
"upcoming": [
36-
{
37-
"match_id": "2026-06-29|Brazil|Japan",
38-
"date": "2026-06-29",
39-
"home": "Brazil",
40-
"away": "Japan",
41-
"neutral": true,
42-
"fav_team": "Brazil",
43-
"fav_pick": "H",
44-
"p_fav": 0.556,
45-
"skew_pred": -0.2255,
46-
"win_payoff": 0.8,
47-
"lose_payoff": -1.0,
48-
"verdict": "neg",
49-
"p_home": 0.556,
50-
"p_draw": 0.252,
51-
"p_away": 0.192,
52-
"home_flag": "br",
53-
"away_flag": "jp",
54-
"adv_home": 0.7066,
55-
"adv_away": 0.2934,
56-
"pen_home": 0.5973,
57-
"xg_home": 1.82,
58-
"xg_away": 0.56,
59-
"score_home": 1,
60-
"score_away": 0
61-
},
62-
{
63-
"match_id": "2026-06-29|Germany|Paraguay",
64-
"date": "2026-06-29",
65-
"home": "Germany",
66-
"away": "Paraguay",
10+
"match_id": "2026-06-30|France|Sweden",
11+
"date": "2026-06-30",
12+
"home": "France",
13+
"away": "Sweden",
6714
"neutral": true,
68-
"fav_team": "Germany",
15+
"fav_team": "France",
6916
"fav_pick": "H",
70-
"p_fav": 0.6233,
71-
"skew_pred": -0.5087,
72-
"win_payoff": 0.6,
17+
"p_fav": 0.7776,
18+
"skew_pred": -1.3352,
19+
"win_payoff": 0.29,
7320
"lose_payoff": -1.0,
7421
"verdict": "neg_strong",
75-
"p_home": 0.6233,
76-
"p_draw": 0.2285,
77-
"p_away": 0.1483,
78-
"home_flag": "de",
79-
"away_flag": "py",
80-
"adv_home": 0.7656,
81-
"adv_away": 0.2344,
82-
"pen_home": 0.6231,
83-
"xg_home": 1.48,
84-
"xg_away": 0.73,
85-
"score_home": 1,
86-
"score_away": 0
87-
},
88-
{
89-
"match_id": "2026-06-29|Netherlands|Morocco",
90-
"date": "2026-06-29",
91-
"home": "Netherlands",
92-
"away": "Morocco",
93-
"neutral": true,
94-
"fav_team": "Netherlands",
95-
"fav_pick": "H",
96-
"p_fav": 0.4809,
97-
"skew_pred": 0.0764,
98-
"win_payoff": 1.08,
99-
"lose_payoff": -1.0,
100-
"verdict": "flat",
101-
"p_home": 0.4809,
102-
"p_draw": 0.2712,
103-
"p_away": 0.2479,
104-
"home_flag": "nl",
105-
"away_flag": "ma",
106-
"adv_home": 0.6338,
107-
"adv_away": 0.3662,
108-
"pen_home": 0.5639,
109-
"xg_home": 1.0,
110-
"xg_away": 0.8,
22+
"p_home": 0.7776,
23+
"p_draw": 0.154,
24+
"p_away": 0.0683,
25+
"home_flag": "fr",
26+
"away_flag": "se",
27+
"adv_home": 0.8805,
28+
"adv_away": 0.1195,
29+
"pen_home": 0.6677,
30+
"xg_home": 1.5,
31+
"xg_away": 0.74,
11132
"score_home": 1,
11233
"score_away": 0
11334
},
35+
"upcoming": [
11436
{
11537
"match_id": "2026-06-30|France|Sweden",
11638
"date": "2026-06-30",
@@ -433,9 +355,74 @@
433355
"brier": 0.232
434356
},
435357
"ledger": {
436-
"n_pending": 15,
437-
"n_resolved": 0
358+
"n_pending": 12,
359+
"n_resolved": 3,
360+
"resolved_skew_real": 0.7071,
361+
"resolved_p_fav": 0.5534,
362+
"resolved_hit_rate": 0.3333
438363
},
364+
"resolved_detail": [
365+
{
366+
"home": "Brazil",
367+
"away": "Japan",
368+
"home_flag": "br",
369+
"away_flag": "jp",
370+
"fav_team": "Brazil",
371+
"p_fav": 0.556,
372+
"skew_pred": -0.2255,
373+
"xg_home": 1.82,
374+
"xg_away": 0.56,
375+
"score_pred": "1-0",
376+
"adv_home": 0.7066,
377+
"score": "2-1",
378+
"reg_result": "H",
379+
"fav_won": true,
380+
"advanced": "Brazil",
381+
"pens": false,
382+
"pens_score": null,
383+
"note": "Martinelli 96', won in regulation"
384+
},
385+
{
386+
"home": "Germany",
387+
"away": "Paraguay",
388+
"home_flag": "de",
389+
"away_flag": "py",
390+
"fav_team": "Germany",
391+
"p_fav": 0.6233,
392+
"skew_pred": -0.5087,
393+
"xg_home": 1.48,
394+
"xg_away": 0.73,
395+
"score_pred": "1-0",
396+
"adv_home": 0.7656,
397+
"score": "1-1",
398+
"reg_result": "D",
399+
"fav_won": false,
400+
"advanced": "Paraguay",
401+
"pens": true,
402+
"pens_score": "3-4",
403+
"note": "1-1 after 120'; Paraguay won 4-3 on penalties — Germany's first-ever WC shootout loss"
404+
},
405+
{
406+
"home": "Netherlands",
407+
"away": "Morocco",
408+
"home_flag": "nl",
409+
"away_flag": "ma",
410+
"fav_team": "Netherlands",
411+
"p_fav": 0.4809,
412+
"skew_pred": 0.0764,
413+
"xg_home": 1.0,
414+
"xg_away": 0.8,
415+
"score_pred": "1-0",
416+
"adv_home": 0.6338,
417+
"score": "1-1",
418+
"reg_result": "D",
419+
"fav_won": false,
420+
"advanced": "Morocco",
421+
"pens": true,
422+
"pens_score": "2-3",
423+
"note": "1-1 after 120'; Morocco won 3-2 on penalties (Bounou save, Saibari decisive)"
424+
}
425+
],
439426
"recent_games": [
440427
{
441428
"home": "South Africa",
@@ -534,5 +521,39 @@
534521
"skew_pred": -0.5923
535522
}
536523
],
537-
"recent_resolved": []
524+
"recent_resolved": [
525+
{
526+
"home": "Brazil",
527+
"away": "Japan",
528+
"fav_team": "Brazil",
529+
"home_flag": "br",
530+
"away_flag": "jp",
531+
"p_fav": 0.556,
532+
"skew_pred": -0.2255,
533+
"fav_won": true,
534+
"ret_fav": 0.7985
535+
},
536+
{
537+
"home": "Germany",
538+
"away": "Paraguay",
539+
"fav_team": "Germany",
540+
"home_flag": "de",
541+
"away_flag": "py",
542+
"p_fav": 0.6233,
543+
"skew_pred": -0.5087,
544+
"fav_won": false,
545+
"ret_fav": -1.0
546+
},
547+
{
548+
"home": "Netherlands",
549+
"away": "Morocco",
550+
"fav_team": "Netherlands",
551+
"home_flag": "nl",
552+
"away_flag": "ma",
553+
"p_fav": 0.4809,
554+
"skew_pred": 0.0764,
555+
"fav_won": false,
556+
"ret_fav": -1.0
557+
}
558+
]
538559
}

site/src/data/wc_predictions.json

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,18 @@
1616
"o_fair": 1.7985,
1717
"predicted_asof": "2026-06-28",
1818
"model": "elo-results-only",
19-
"realized": null
19+
"realized": {
20+
"result": "H",
21+
"fav_won": true,
22+
"ret_fav": 0.7985,
23+
"score": "2-1",
24+
"advanced": "Brazil",
25+
"pens": false,
26+
"pens_score": null,
27+
"note": "Martinelli 96', won in regulation",
28+
"source": "ESPN/FIFA/CBS",
29+
"settled_asof": "2026-06-28"
30+
}
2031
},
2132
{
2233
"match_id": "2026-06-29|Germany|Paraguay",
@@ -31,7 +42,18 @@
3142
"o_fair": 1.6045,
3243
"predicted_asof": "2026-06-28",
3344
"model": "elo-results-only",
34-
"realized": null
45+
"realized": {
46+
"result": "D",
47+
"fav_won": false,
48+
"ret_fav": -1.0,
49+
"score": "1-1",
50+
"advanced": "Paraguay",
51+
"pens": true,
52+
"pens_score": "3-4",
53+
"note": "1-1 after 120'; Paraguay won 4-3 on penalties — Germany's first-ever WC shootout loss",
54+
"source": "ESPN/CBS/VAVEL",
55+
"settled_asof": "2026-06-28"
56+
}
3557
},
3658
{
3759
"match_id": "2026-06-29|Netherlands|Morocco",
@@ -46,7 +68,18 @@
4668
"o_fair": 2.0794,
4769
"predicted_asof": "2026-06-28",
4870
"model": "elo-results-only",
49-
"realized": null
71+
"realized": {
72+
"result": "D",
73+
"fav_won": false,
74+
"ret_fav": -1.0,
75+
"score": "1-1",
76+
"advanced": "Morocco",
77+
"pens": true,
78+
"pens_score": "2-3",
79+
"note": "1-1 after 120'; Morocco won 3-2 on penalties (Bounou save, Saibari decisive)",
80+
"source": "ESPN/Yahoo/Outlook",
81+
"settled_asof": "2026-06-28"
82+
}
5083
},
5184
{
5285
"match_id": "2026-06-30|France|Sweden",

site/src/pages/worldcup.astro

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,30 @@ const pct = (x) => (x * 100).toFixed(0) + '%';
152152
</div>
153153
</div> : <p class="lede">No upcoming fixtures in the dataset right now — the bracket is complete.</p>}
154154

155+
{wcl.resolved_detail && wcl.resolved_detail.length > 0 && <div class="rv reveal">
156+
<div class="rv-head">● First results in — frozen prediction vs reality</div>
157+
{wcl.resolved_detail.map(r => <div class={`rv-row ${r.fav_won ? 'ok' : 'no'}`}>
158+
<div class="rv-match">
159+
<div class="rv-team">
160+
{r.home_flag && <img src={`https://flagcdn.com/w40/${r.home_flag}.png`} alt="" width="28" height="19" />}
161+
<span class={r.advanced===r.home ? 'rv-adv' : ''}>{r.home}</span>
162+
</div>
163+
<div class="rv-score">{r.score}{r.pens && <i class="rv-pen">pens {r.pens_score}</i>}</div>
164+
<div class="rv-team rv-away">
165+
<span class={r.advanced===r.away ? 'rv-adv' : ''}>{r.away}</span>
166+
{r.away_flag && <img src={`https://flagcdn.com/w40/${r.away_flag}.png`} alt="" width="28" height="19" />}
167+
</div>
168+
</div>
169+
<div class="rv-call">
170+
<span class="rv-pred">we said: <b>{r.fav_team} {Math.round(r.p_fav*100)}%</b> · advance {Math.round(r.adv_home*100)}%{r.score_pred && ` · ~${r.score_pred}`}</span>
171+
<span class={`rv-tag ${r.fav_won ? 'ok' : 'no'}`}>{r.fav_won ? '✓ favourite won' : (r.pens ? '✗ out on penalties' : '✗ upset')}</span>
172+
</div>
173+
</div>)}
174+
<p class="note">The favourite-bet skewness is a <em>shape</em> call, not a tip: Germany was our most left-skewed
175+
bet (−0.51, "wins small, rarely loses big") — and the rare big loss is exactly what happened. Netherlands
176+
was our only near-symmetric toss-up (+0.08) — and it went to penalties. Full breakdown below.</p>
177+
</div>}
178+
155179
{wcl.recent_games && wcl.recent_games.length > 0 && <div class="nm-results reveal">
156180
<div class="nm-results-head">Recent results · 2026 group stage</div>
157181
<div class="nm-results-grid">
@@ -499,6 +523,23 @@ python analysis/export_worldcup_data.py # regenerate this page's data</code></
499523
.rg-score span { color: var(--ink-faint); margin: 0 2px; }
500524
.rg-mark { font-weight: 700; width: 20px; text-align: center; }
501525
.rg-mark.ok { color: var(--pos); } .rg-mark.no { color: var(--neg); }
526+
/* resolved retrospective */
527+
.rv { margin-top: 26px; }
528+
.rv-head { font-size: .8rem; font-weight: 600; color: var(--accent); margin-bottom: 12px; }
529+
.rv-row { border: 1px solid var(--line-soft); border-left: 3px solid var(--line); border-radius: 10px; padding: 12px 16px; margin-bottom: 10px; }
530+
.rv-row.ok { border-left-color: var(--pos); } .rv-row.no { border-left-color: var(--neg); }
531+
.rv-match { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; }
532+
.rv-team { display: flex; align-items: center; gap: 9px; font-size: 1.05rem; font-weight: 600; min-width: 0; }
533+
.rv-team span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
534+
.rv-away { justify-content: flex-end; }
535+
.rv-adv { color: var(--accent); }
536+
.rv-team img { border-radius: 3px; flex: 0 0 auto; box-shadow: 0 1px 2px rgba(0,0,0,.15); }
537+
.rv-score { font-family: var(--mono); font-weight: 700; font-size: 1.3rem; text-align: center; white-space: nowrap; }
538+
.rv-pen { display: block; font-size: .64rem; font-weight: 500; color: var(--ink-faint); font-style: normal; letter-spacing: .02em; }
539+
.rv-call { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 9px; padding-top: 9px; border-top: 1px dashed var(--line-soft); flex-wrap: wrap; }
540+
.rv-pred { font-size: .82rem; color: var(--ink-soft); } .rv-pred b { color: var(--ink); }
541+
.rv-tag { font-size: .8rem; font-weight: 600; }
542+
.rv-tag.ok { color: var(--pos); } .rv-tag.no { color: var(--neg); }
502543
.nm-viz { display: grid; grid-template-columns: 340px 1fr; gap: 24px; align-items: center; margin-top: 14px; }
503544
@media (max-width: 680px) { .nm-viz { grid-template-columns: 1fr; } #nm-payoff { max-width: 340px; } }
504545
#nm-payoff { width: 100%; height: auto; }

0 commit comments

Comments
 (0)