|
| 1 | +--- |
| 2 | +import Base from '../layouts/Base.astro'; |
| 3 | +import Footer from '../components/Footer.astro'; |
| 4 | +import wcl from '../data/wc_live.json'; |
| 5 | +
|
| 6 | +const T = wcl.triangulation || []; |
| 7 | +const M = wcl.model_vs_book || null; |
| 8 | +const pct = (x) => Math.round(x * 100) + '%'; |
| 9 | +const sgn = (x) => (x >= 0 ? '+' : '') + x.toFixed(2); |
| 10 | +
|
| 11 | +// ── the structural law, plotted: skew = (1−2p)/√(p(1−p)) ── |
| 12 | +const law = (p) => (1 - 2 * p) / Math.sqrt(p * (1 - p)); |
| 13 | +const P0 = 0.33, P1 = 0.95, S0 = 0.7, S1 = -1.25; // axis domains |
| 14 | +const L = 64, Rr = 688, Tp = 34, Bt = 320; // plot box (viewBox 720×372) |
| 15 | +const X = (p) => L + ((p - P0) / (P1 - P0)) * (Rr - L); |
| 16 | +const Y = (s) => Tp + ((S0 - s) / (S0 - S1)) * (Bt - Tp); |
| 17 | +// law polyline |
| 18 | +const curve = Array.from({ length: 125 }, (_, i) => { |
| 19 | + const p = P0 + (i / 124) * (P1 - P0); |
| 20 | + return `${X(p).toFixed(1)},${Y(law(p)).toFixed(1)}`; |
| 21 | +}).join(' '); |
| 22 | +// axis ticks |
| 23 | +const pTicks = [0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95]; |
| 24 | +const sTicks = [0.5, 0, -0.5, -1.0]; |
| 25 | +--- |
| 26 | +<Base title="Model vs Market — the law is observer-invariant | skew-dynamic" |
| 27 | + description="Our odds-free Elo and a real bookmaker price the same World Cup bets. Two probabilities, one law: both land on (1−2p)/√(p(1−p)). And whose p was better calibrated." |
| 28 | + path="/odds"> |
| 29 | + <header class="o-top"> |
| 30 | + <div class="container o-top-inner"> |
| 31 | + <a class="brand" href="/"><span class="brand-mark"></span>skew<span class="brand-dim">-dynamic</span></a> |
| 32 | + <div class="o-top-links"> |
| 33 | + <a href="/worldcup">World Cup</a><a href="/goals">Goals</a><a href="/retro">Retro</a> |
| 34 | + <a href="https://github.com/cold-code-labs/skew-dynamic" target="_blank" rel="noopener">GitHub ↗</a> |
| 35 | + </div> |
| 36 | + </div> |
| 37 | + </header> |
| 38 | + |
| 39 | + <article class="container o-wrap"> |
| 40 | + <div class="eyebrow reveal">● Model vs Market · same bet, two prices, one law</div> |
| 41 | + <h1 class="reveal">Our odds, the bookmaker's odds, and the law that holds for both.</h1> |
| 42 | + <p class="o-lede reveal">For these knockout games we have <em>three</em> readings of each favourite bet: |
| 43 | + our <b>odds-free Elo</b> probability (results only — no market), the <b>bookmaker's</b> de-vigged |
| 44 | + probability (a real published line, frozen pre-match), and the <b>realised</b> outcome. The structural |
| 45 | + claim is not that we beat the market. It is sharper and stranger: <strong>whatever probability you |
| 46 | + believe, the skewness of the bet is forced</strong> — both our number and the market's land on the same |
| 47 | + curve <span class="mono">(1−2p)/√(p(1−p))</span>. They disagree on <em>where</em> on the curve a game sits, |
| 48 | + never on the curve itself.</p> |
| 49 | + |
| 50 | + {M && <div class="o-scoreboard reveal"> |
| 51 | + <div class="o-sb-card"> |
| 52 | + <span class="o-sb-k">Agreement (rank corr)</span> |
| 53 | + <b class="o-sb-v pos">{M.agree_corr.toFixed(3)}</b> |
| 54 | + <span class="o-sb-d">our p vs the market's p — near-identical ordering</span> |
| 55 | + </div> |
| 56 | + <div class="o-sb-card"> |
| 57 | + <span class="o-sb-k">Mean disagreement</span> |
| 58 | + <b class="o-sb-v">{pct(M.mean_abs_edge)}</b> |
| 59 | + <span class="o-sb-d">average gap in level (|p<sub>model</sub> − p<sub>book</sub>|)</span> |
| 60 | + </div> |
| 61 | + <div class="o-sb-card"> |
| 62 | + <span class="o-sb-k">Brier — model vs book</span> |
| 63 | + <b class="o-sb-v"><span class={M.brier_model<=M.brier_book?'pos':''}>{M.brier_model.toFixed(3)}</span> <i>/</i> {M.brier_book.toFixed(3)}</b> |
| 64 | + <span class="o-sb-d">lower is better · n = {M.n} (far too few to conclude)</span> |
| 65 | + </div> |
| 66 | + <div class="o-sb-card"> |
| 67 | + <span class="o-sb-k">Log-loss — model vs book</span> |
| 68 | + <b class="o-sb-v"><span class={M.logloss_model<=M.logloss_book?'pos':''}>{M.logloss_model.toFixed(3)}</span> <i>/</i> {M.logloss_book.toFixed(3)}</b> |
| 69 | + <span class="o-sb-d">same direction — our humility on the upsets paid</span> |
| 70 | + </div> |
| 71 | + </div>} |
| 72 | + |
| 73 | + <!-- ── the law curve with model (○) and market (●) points ── --> |
| 74 | + <figure class="o-fig reveal"> |
| 75 | + <svg viewBox="0 0 720 372" role="img" aria-label="Skewness law with model and bookmaker points"> |
| 76 | + <!-- grid --> |
| 77 | + {sTicks.map(s => <g> |
| 78 | + <line x1={L} y1={Y(s)} x2={Rr} y2={Y(s)} class={s===0?'ax0':'grid'} /> |
| 79 | + <text x={L-10} y={Y(s)+4} class="tk" text-anchor="end">{sgn(s)}</text> |
| 80 | + </g>)} |
| 81 | + {pTicks.map(p => <g> |
| 82 | + <line x1={X(p)} y1={Tp} x2={X(p)} y2={Bt} class={Math.abs(p-0.5)<1e-9?'ax0':'grid'} /> |
| 83 | + <text x={X(p)} y={Bt+18} class="tk" text-anchor="middle">{p.toFixed(2)}</text> |
| 84 | + </g>)} |
| 85 | + <text x={(L+Rr)/2} y={362} class="axlab" text-anchor="middle">favourite probability p</text> |
| 86 | + <text x={16} y={(Tp+Bt)/2} class="axlab" text-anchor="middle" transform={`rotate(-90 16 ${(Tp+Bt)/2})`}>return skewness</text> |
| 87 | + <!-- the law --> |
| 88 | + <polyline points={curve} class="law" /> |
| 89 | + <text x={X(0.9)} y={Y(law(0.9))-10} class="lawlab" text-anchor="end">(1−2p)/√(p(1−p))</text> |
| 90 | + <!-- per-match: connect model ○ and book ● (both on the curve) --> |
| 91 | + {T.map(t => <g> |
| 92 | + <line x1={X(t.p_model)} y1={Y(t.skew_model)} x2={X(t.p_book)} y2={Y(t.skew_book)} |
| 93 | + class={`conn ${t.fav_won?'ok':'no'}`} /> |
| 94 | + <circle cx={X(t.p_model)} cy={Y(t.skew_model)} r="6" class={`pt model ${t.fav_won?'ok':'no'}`} /> |
| 95 | + <circle cx={X(t.p_book)} cy={Y(t.skew_book)} r="6" class={`pt book ${t.fav_won?'ok':'no'}`} /> |
| 96 | + <text x={X(t.p_book)} y={Y(t.skew_book)+ (t.skew_book < t.skew_model ? 20 : -12)} class="ptlab" text-anchor="middle">{t.fav_team}</text> |
| 97 | + </g>)} |
| 98 | + </svg> |
| 99 | + <figcaption> |
| 100 | + <span class="lg"><span class="dot model"></span> our Elo (results-only)</span> |
| 101 | + <span class="lg"><span class="dot book"></span> {T[0]?.book || 'bookmaker'} (de-vigged)</span> |
| 102 | + <span class="lg"><span class="bar ok"></span> favourite advanced</span> |
| 103 | + <span class="lg"><span class="bar no"></span> favourite out</span> |
| 104 | + </figcaption> |
| 105 | + </figure> |
| 106 | + |
| 107 | + <!-- ── triangulation table ── --> |
| 108 | + <div class="o-tbl reveal"> |
| 109 | + <div class="o-tr o-th"> |
| 110 | + <span>Match</span><span>p model</span><span>p market</span><span>skew model</span><span>skew market</span><span>outcome</span> |
| 111 | + </div> |
| 112 | + {T.map(t => <div class={`o-tr ${t.fav_won?'ok':'no'}`}> |
| 113 | + <span class="o-m"> |
| 114 | + {t.home_flag && <img src={`https://flagcdn.com/w20/${t.home_flag}.png`} alt="" width="18" height="12" />} |
| 115 | + {t.fav_team}<i> fav</i> |
| 116 | + </span> |
| 117 | + <span class="mono">{t.p_model.toFixed(3)}</span> |
| 118 | + <span class="mono">{t.p_book.toFixed(3)} <em class={t.edge>=0?'up':'dn'}>{t.edge>=0?'+':''}{(t.edge*100).toFixed(1)}</em></span> |
| 119 | + <span class="mono">{sgn(t.skew_model)}</span> |
| 120 | + <span class="mono">{sgn(t.skew_book)}</span> |
| 121 | + <span class="o-o">{t.fav_won ? '✓ won' : (t.pens ? '✗ pens' : '✗ upset')} <i>{t.score}</i></span> |
| 122 | + </div>)} |
| 123 | + </div> |
| 124 | + |
| 125 | + <section class="o-body reveal"> |
| 126 | + <h2>What the three games say</h2> |
| 127 | + <p><b>Brazil — the market and the model are the same bet.</b> Our p = 0.556, the book's de-vigged |
| 128 | + p = 0.562: a six-thousandths gap. Both put Brazil just past a coin-flip, both imply a mildly |
| 129 | + left-skewed bet (−0.23 / −0.25), and the favourite won. When everyone agrees, the law is invisible |
| 130 | + — it just sits there being satisfied.</p> |
| 131 | + <p><b>Germany — the market was the over-confident one.</b> The book priced Germany at <b>0.742</b>; |
| 132 | + our results-only Elo, at <b>0.623</b>, was far more humble. That gap is the whole story: a higher p |
| 133 | + means a <em>more</em> left-skewed bet — skew <b>−1.11</b> on the market's number vs −0.51 on ours, i.e. |
| 134 | + "wins small, rarely loses <b>catastrophically</b>." Germany lost on penalties. The catastrophic tail |
| 135 | + the market's own skew was screaming about is exactly the one that hit — and our lower p ate a smaller |
| 136 | + loss in the scorecard.</p> |
| 137 | + <p><b>Netherlands — both called the toss-up, the market more loudly.</b> Here we were the bolder one |
| 138 | + (0.481) and the book the cautious one (0.398, skew <b>+0.42</b> — a <em>positively</em> skewed, |
| 139 | + longshot-shaped favourite). Both readings said "do not trust this favourite"; it went to penalties, |
| 140 | + Morocco through. Different p, same warning, same curve.</p> |
| 141 | + |
| 142 | + <h2>The point — and the line we are not crossing</h2> |
| 143 | + <p>This is a test of <b>agreement and shape</b>, not of edge. The rank correlation between our p and the |
| 144 | + market's is <b>{M ? M.agree_corr.toFixed(3) : '—'}</b>: the odds-free engine reconstructs what a real |
| 145 | + sportsbook priced, from results alone. That is the same result as the World Cup annex's |
| 146 | + <span class="mono"> +0.998</span> on realised skew, now against a <em>second observer</em> instead of |
| 147 | + against frequency.</p> |
| 148 | + <blockquote>The difference <span class="mono">p<sub>model</sub> − p<sub>book</sub></span> is, literally, |
| 149 | + a betting signal. We compute it, we show it — and we stop there. Turning that gap into staked "value" |
| 150 | + would convert this study from a structural law into an alpha claim, with a completely different burden |
| 151 | + of proof (market efficiency, transaction costs, years of out-of-sample). The whole credibility of the |
| 152 | + thesis rests on it being <em>descriptive history</em>, not a tip sheet. So the edge column is a |
| 153 | + diagnostic, never an instruction.</blockquote> |
| 154 | + <p>And the calibration scoreboard — model Brier {M ? M.brier_model.toFixed(3) : '—'} vs market |
| 155 | + {M ? ' ' + M.brier_book.toFixed(3) : ''} — is <b>not</b> evidence we beat the market. With |
| 156 | + <b> n = {M ? M.n : 0}</b> and two penalty lotteries it is noise. It becomes a real number at n = 16, |
| 157 | + the full Round of 32, and it is reported here only so the comparison is honest in both directions.</p> |
| 158 | + <p class="o-foot">Book odds: real pre-match lines, hand-recorded and frozen in |
| 159 | + <a href="https://github.com/cold-code-labs/skew-dynamic/blob/main/study/wc_book_odds.json" target="_blank" rel="noopener">wc_book_odds.json</a> |
| 160 | + (FanDuel via CBS Sports / ESPN, 2026-06-29) · de-vig: Shin · method in |
| 161 | + <a href="/worldcup">the World Cup annex</a> · this page updates as more games resolve.</p> |
| 162 | + </section> |
| 163 | + </article> |
| 164 | + |
| 165 | + <Footer /> |
| 166 | +</Base> |
| 167 | + |
| 168 | +<style> |
| 169 | + .o-top { position: sticky; top: 0; z-index: 40; backdrop-filter: saturate(160%) blur(12px); background: rgba(252,253,255,.82); border-bottom: 1px solid var(--line-soft); } |
| 170 | + .o-top-inner { display: flex; align-items: center; height: 60px; } |
| 171 | + .o-top-links { margin-left: auto; display: flex; gap: 18px; } |
| 172 | + .o-top-links a { color: var(--ink-soft); font-size: .9rem; font-weight: 500; } |
| 173 | + .o-top-links a:hover { color: var(--accent); text-decoration: none; } |
| 174 | + .brand { font-weight: 650; letter-spacing: -.02em; color: var(--ink); display: flex; align-items: center; gap: 9px; } |
| 175 | + .brand:hover { text-decoration: none; } .brand-dim { color: var(--ink-faint); font-weight: 500; } |
| 176 | + .brand-mark { width: 16px; height: 16px; border-radius: 5px; background: linear-gradient(135deg, var(--accent), var(--navy)); box-shadow: 0 0 0 3px var(--accent-soft); } |
| 177 | + |
| 178 | + .o-wrap { max-width: 820px; padding-top: 56px; padding-bottom: 40px; } |
| 179 | + .o-wrap h1 { font-size: clamp(2rem, 5vw, 2.8rem); letter-spacing: -.03em; line-height: 1.06; margin: 14px 0 18px; } |
| 180 | + .o-lede { font-size: 1.12rem; color: var(--ink-soft); line-height: 1.6; } |
| 181 | + .mono { font-family: var(--mono); } |
| 182 | + |
| 183 | + .o-scoreboard { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 30px 0; } |
| 184 | + @media (min-width: 720px) { .o-scoreboard { grid-template-columns: repeat(4, 1fr); } } |
| 185 | + .o-sb-card { border: 1px solid var(--line-soft); border-radius: 12px; padding: 14px 16px; background: var(--surface, #fff); } |
| 186 | + .o-sb-k { font-size: .72rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .04em; } |
| 187 | + .o-sb-v { font-family: var(--mono); font-size: 1.5rem; font-weight: 650; color: var(--ink); display: block; margin: 4px 0 2px; } |
| 188 | + .o-sb-v i { color: var(--ink-faint); font-style: normal; font-weight: 400; } |
| 189 | + .o-sb-v .pos, .pos { color: var(--pos); } |
| 190 | + .o-sb-d { font-size: .74rem; color: var(--ink-faint); line-height: 1.3; } |
| 191 | + |
| 192 | + .o-fig { margin: 32px 0 8px; } |
| 193 | + .o-fig svg { width: 100%; height: auto; display: block; } |
| 194 | + .grid { stroke: var(--line-soft); stroke-width: 1; } |
| 195 | + .ax0 { stroke: var(--line); stroke-width: 1.3; } |
| 196 | + .tk { fill: var(--ink-faint); font-size: 11px; font-family: var(--mono); } |
| 197 | + .axlab { fill: var(--ink-soft); font-size: 12px; font-weight: 500; } |
| 198 | + .law { fill: none; stroke: var(--accent); stroke-width: 2.4; opacity: .9; } |
| 199 | + .lawlab { fill: var(--accent); font-size: 12px; font-family: var(--mono); font-weight: 600; } |
| 200 | + .conn { stroke-width: 2; stroke-dasharray: 3 3; opacity: .55; } |
| 201 | + .conn.ok { stroke: var(--pos); } .conn.no { stroke: var(--neg); } |
| 202 | + .pt { stroke-width: 2; } |
| 203 | + .pt.model { fill: #fff; } .pt.book { fill: currentColor; } |
| 204 | + .pt.ok { color: var(--pos); stroke: var(--pos); } .pt.no { color: var(--neg); stroke: var(--neg); } |
| 205 | + .ptlab { fill: var(--ink-soft); font-size: 11px; font-weight: 600; } |
| 206 | + .o-fig figcaption { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 12px; font-size: .8rem; color: var(--ink-soft); justify-content: center; } |
| 207 | + .lg { display: inline-flex; align-items: center; gap: 6px; } |
| 208 | + .dot { width: 11px; height: 11px; border-radius: 50%; border: 2px solid var(--ink-soft); } |
| 209 | + .dot.model { background: #fff; } .dot.book { background: var(--ink-soft); } |
| 210 | + .bar { width: 14px; height: 3px; border-radius: 2px; } .bar.ok { background: var(--pos); } .bar.no { background: var(--neg); } |
| 211 | + |
| 212 | + .o-tbl { margin: 26px 0; border: 1px solid var(--line-soft); border-radius: 12px; overflow: hidden; } |
| 213 | + .o-tr { display: grid; grid-template-columns: 1.6fr 1fr 1.2fr 1fr 1fr 1.1fr; align-items: center; gap: 8px; padding: 11px 16px; font-size: .86rem; border-top: 1px solid var(--line-soft); } |
| 214 | + .o-tr:first-child { border-top: none; } |
| 215 | + .o-th { background: var(--accent-soft); color: var(--ink-soft); font-size: .72rem; text-transform: uppercase; letter-spacing: .03em; font-weight: 600; } |
| 216 | + .o-tr.ok { border-left: 3px solid var(--pos); } .o-tr.no { border-left: 3px solid var(--neg); } |
| 217 | + .o-m { display: flex; align-items: center; gap: 7px; font-weight: 600; color: var(--ink); } |
| 218 | + .o-m i { color: var(--ink-faint); font-style: normal; font-weight: 400; font-size: .78rem; } |
| 219 | + .o-m img { border-radius: 2px; } |
| 220 | + .o-tr em { font-family: var(--mono); font-size: .72rem; font-style: normal; } .o-tr .up { color: var(--pos); } .o-tr .dn { color: var(--neg); } |
| 221 | + .o-o { font-weight: 600; } .o-o i { font-style: normal; font-weight: 400; color: var(--ink-faint); font-family: var(--mono); } |
| 222 | + |
| 223 | + .o-body h2 { font-size: 1.4rem; letter-spacing: -.02em; margin: 36px 0 12px; } |
| 224 | + .o-body p { color: var(--ink-soft); line-height: 1.7; margin-bottom: 14px; } |
| 225 | + .o-body b { color: var(--ink); } |
| 226 | + .o-body blockquote { margin: 20px 0; padding: 14px 20px; border-left: 3px solid var(--accent); background: var(--accent-soft); border-radius: 0 8px 8px 0; font-size: 1.04rem; color: var(--ink); line-height: 1.6; } |
| 227 | + .o-foot { font-size: .82rem; color: var(--ink-faint); margin-top: 28px; padding-top: 16px; border-top: 1px solid var(--line-soft); } |
| 228 | +</style> |
0 commit comments