Odds Generation — point · quarter (basketball)
Enter four contemporaneous Full Time quotes in Malay — two Handicap lines (Home/Away) and two Total lines (Over/Under) — for the same match, snapshot, and full-time-including-overtime settlement convention. No FIBA/NBA ruleset, regulation duration, team, tournament, score, clock, historical volatility, or Moneyline input is required. The engine (ADR-031):
- strips all four bookmaker margins with the two-way Power method;
- back-solves Full Time Gaussian margin D and total T marginals;
- refines their four metrics against integer settlement reads so every anchor, whole-point push, and half-point line is reproduced;
- projects Half Time and Q1–Q4 with equal-duration Gaussian scaling; and
- regenerates the supported Handicap, Over/Under, Winning Margin, Odd/Even, and Full Time Total Last Digit books.
The Full Time anchors already include overtime, so the model fits final settlement directly. This removes the former FIBA-40/NBA-48 input and the unidentified regulation-to-overtime decomposition.
Why four anchors?
Let final Full Time margin be D and final Full Time total be T:
D ~ Normal(μD, σD²)
T ~ Normal(μT, σT²)
These are two marginal distributions; the model does not assert or estimate a joint distribution between them. After stripping bookmaker margin, the smooth starting identities are:
Handicap: zi = Φ⁻¹(P(home covers ℓi)) = (μD + ℓi) / σD
Total: zi = Φ⁻¹(P(over qi)) = (μT − qi) / σT
Two distinct thresholds on one marginal identify both its location and
dispersion. The engine obtains μD and σD from the Handicap pair and μT and
σT from the Total pair, then refines them against the same discrete readers
used to generate output ladders. The Full Time margin distribution is
conditioned on D ≠ 0, because an overtime-inclusive basketball match cannot
finish tied.
This keeps the previous hardcoded assumptions removed:
| Removed assumption | Replacement |
|---|---|
σD = 12.5 league prior | identified from the second Handicap line |
σT = 1.1·√μT | identified from the second Total line |
| Moneyline partial pooling | removed |
ρDT = 0 and a team-score joint | not constructed |
| FIBA/NBA overtime fraction | final Full Time settlement fitted directly |
The Gaussian choice is a practical smooth model for basketball quote surfaces. Stern's 1994 Brownian basketball work supports the Gaussian margin skeleton; it does not establish this engine's total marginal or a margin/total independence assumption.
Half Time and quarter projection
The four inputs identify Full Time distributions only. Period markets require
one additional, visible model assumption: equal-duration stationary Gaussian
increments. For clock share s:
μD,s = s·μD σD,s = √s·σD
μT,s = s·μT σT,s = √s·σT
Half Time uses s = 1/2; each of Q1, Q2, Q3, and Q4 uses s = 1/4.
Period ties remain real draw/push mass. All four quarters are therefore
exchangeable: the inputs do not identify quarter-specific pace, advantage,
rotation, foul, or game-state behavior.
This projection is suitable for generating a neutral period surface from Full Time markets. It is not a claim that a bookmaker's observed Q1–Q4 boards will be identical. Period-specific quote inputs would be required to identify those differences.
Reliability contract
The engine fails closed instead of substituting priors when:
- an anchor or one side of its price pair is missing;
- the two lines for a market are equal;
- stripped probabilities imply non-positive dispersion;
- anchors are too close in probability to identify dispersion reliably; or
- the discrete solver cannot reproduce all four anchors within tolerance.
The page exposes anchor residuals, z separations, and numerical condition numbers. They diagnose the Full Time inversion. Period outputs inherit that fit but also carry the equal-duration projection assumption; they are not period-calibrated observations.
The earlier 44-board exploratory exercise remains historical motivation only. Its source data is not in this repository, so this page makes no reproducible production-accuracy claim from it.
Score grid
| -30 | -29 | -28 | -27 | -26 | -25 | -24 | -23 | -22 | -21 | -20 | -19 | -18 | -17 | -16 | -15 | -14 | -13 | -12 | -11 | -10 | -9 | -8 | -7 | -6 | -5 | -4 | -3 | -2 | -1 | 0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 | +10 | +11 | +12 | +13 | +14 | +15 | +16 | +17 | +18 | +19 | +20 | +21 | +22 | +23 | +24 | +25 | +26 | +27 | +28 | +29 | +30 | +31 | +32 | +33 | +34 | +35 | +36 | +37 | +38 | +39 | +40 | +41 | +42 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.0 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.2 | 0.2 | 0.2 | 0.3 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1.0 | 1.1 | 1.3 | 1.4 | 1.6 | 1.7 | 1.9 | 2.1 | 2.2 | 2.4 | 2.5 | 2.7 | 2.8 | 0.0 | 3.1 | 3.2 | 3.3 | 3.3 | 3.3 | 3.4 | 3.4 | 3.3 | 3.3 | 3.2 | 3.1 | 3.0 | 2.9 | 2.7 | 2.6 | 2.4 | 2.3 | 2.1 | 1.9 | 1.8 | 1.6 | 1.4 | 1.3 | 1.2 | 1.0 | 0.9 | 0.8 | 0.7 | 0.6 | 0.5 | 0.4 | 0.4 | 0.3 | 0.2 | 0.2 | 0.2 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.0 |
| 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.0 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.2 | 0.2 | 0.2 | 0.2 | 0.2 | 0.3 | 0.3 | 0.3 | 0.4 | 0.4 | 0.5 | 0.5 | 0.6 | 0.6 | 0.7 | 0.7 | 0.8 | 0.8 | 0.9 | 1.0 | 1.0 | 1.1 | 1.2 | 1.2 | 1.3 | 1.4 | 1.4 | 1.5 | 1.6 | 1.6 | 1.7 | 1.8 | 1.8 | 1.9 | 1.9 | 2.0 | 2.0 | 2.1 | 2.1 | 2.1 | 2.1 | 2.1 | 2.1 | 2.1 | 2.1 | 2.1 | 2.1 | 2.1 | 2.1 | 2.0 | 2.0 | 1.9 | 1.9 | 1.8 | 1.8 | 1.7 | 1.7 | 1.6 | 1.5 | 1.5 | 1.4 | 1.3 | 1.2 | 1.2 | 1.1 | 1.0 | 1.0 | 0.9 | 0.8 | 0.8 | 0.7 | 0.7 | 0.6 | 0.6 | 0.5 | 0.5 | 0.4 | 0.4 | 0.3 | 0.3 | 0.3 | 0.3 | 0.2 | 0.2 | 0.2 | 0.2 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 | 0.0 | 0.0 |
Markets
| HDP | -10.0 | -9.5 | -9.0 | -8.5 | -8.0 | -7.5 | -7.0 | -6.5 | -6.0 | -5.5 |
|---|---|---|---|---|---|---|---|---|---|---|
| H | -0.67 | -0.72 | -0.76 | -0.82 | -0.86 | -0.93 | -0.98 | +0.95 | +0.88 | +0.83 |
| A | +0.59 | +0.64 | +0.68 | +0.74 | +0.78 | +0.85 | +0.90 | +0.97 | -0.96 | -0.91 |
| Points | 224.5 | 225.0 | 225.5 | 226.0 | 226.5 | 227.0 | 227.5 | 228.0 | 228.5 | 229.0 |
|---|---|---|---|---|---|---|---|---|---|---|
| Over | +0.95 | +0.99 | -0.97 | -0.93 | -0.89 | -0.85 | -0.82 | -0.79 | -0.76 | -0.72 |
| Under | +0.97 | +0.93 | +0.89 | +0.85 | +0.81 | +0.77 | +0.74 | +0.71 | +0.68 | +0.64 |
1 · Strip all four bookmaker margins (Power method)
Convert each Malay quote to decimal and priced implied probability . For each two-way book, solve one Power exponent so ; the two fair legs are then . The first fair leg is the hard target used below.
| Home | Away | |
|---|---|---|
| Malay quote | +0.95 | +0.97 |
| Decimal | 1.950000 | 1.970000 |
| Fair | 0.502628 | 0.497372 |
| Fair decimal | 1.989544 | 2.010566 |
| Home | Away | |
|---|---|---|
| Malay quote | -0.72 | +0.64 |
| Decimal | 2.388889 | 1.640000 |
| Fair | 0.403142 | 0.596858 |
| Fair decimal | 2.480513 | 1.675442 |
| Over | Under | |
|---|---|---|
| Malay quote | +0.95 | +0.97 |
| Decimal | 1.950000 | 1.970000 |
| Fair | 0.502628 | 0.497372 |
| Fair decimal | 1.989544 | 2.010566 |
| Over | Under | |
|---|---|---|
| Malay quote | -0.76 | +0.68 |
| Decimal | 2.315789 | 1.680000 |
| Fair | 0.417250 | 0.582750 |
| Fair decimal | 2.396647 | 1.716001 |
2 · Back-solve the four continuous Gaussian metrics
Transform each fair target to a standard-normal score . The two Handicap equations identify the margin pair ; the two Total equations independently identify .
| Anchor | Line | Fair target p* | z = Φ⁻¹(p*) |
|---|---|---|---|
| Handicap 1 | -6.5 | 0.502628 | 0.006587 |
| Handicap 2 | -9.5 | 0.403142 | -0.245222 |
| Total 1 | 224.5 | 0.502628 | 0.006587 |
| Total 2 | 228.5 | 0.417250 | -0.208935 |
These are starting values, not priors. They use only the four stripped quote targets.
3 · Refine against discrete settlement and reproduce every anchor
Discretize each Normal into integer score masses using CDF cell differences. A damped Newton solve adjusts and until the exact settlement reader matches both targets on each marginal. For a whole-point line, fair probability conditions away the push: . Half-point lines have .
| Anchor | Line | Target p* | Model p | W | R | |Δ| |
|---|---|---|---|---|---|---|
| Handicap 1 | -6.5 | 0.502628 | 0.502628 | 0.502628 | 0.000000 | 6.13e-8 |
| Handicap 2 | -9.5 | 0.403142 | 0.403142 | 0.403142 | 0.000000 | 2.14e-8 |
| Total 1 | 224.5 | 0.502628 | 0.502628 | 0.502628 | 0.000000 | 6.20e-12 |
| Total 2 | 228.5 | 0.417250 | 0.417250 | 0.417250 | 0.000000 | 7.26e-12 |
Conditioning checks: margin z-gap 0.2518, condition 3.4121; total z-gap 0.2155, condition 3.9275; 2 accepted Newton iterations in total.
4 · Project the selected period
The anchors already describe Full Time including OT, so no FIBA/NBA duration input is required. Full Time uses clock share . Under the equal-duration Gaussian projection, means scale by and standard deviations by .
Full Time conditions the impossible final draw away. Half Time and quarter distributions retain margin zero, so a tied period is a push where the selected line settles on zero. Q1–Q4 are exchangeable in this model: the four Full Time anchors do not identify quarter-specific pace, advantage, or game-state effects.
5 · Re-generate the supported market sheet
Full Time Handicap and Over/Under lines read and from the selected marginals and compute . Supported Winning Margin and Odd/Even books sum the same integer cells; Full Time also exposes Total Last Digit. Ladder outputs use the Power transform and Malay odds. Other Markets use the selected Shin margin and decimal odds. All margin controls are presentation-only.
| Sample output | Line | Outcome | Fair p | Fair decimal | Displayed odds |
|---|---|---|---|---|---|
| Full Time Handicap | -8.0 | Home | 0.450838 | 2.2181 | -0.86 |
| Full Time Handicap | -8.0 | Away | 0.549162 | 1.8210 | +0.78 |
| Full Time Over/Under | 226.5 | Over | 0.459706 | 2.1753 | -0.89 |
| Full Time Over/Under | 226.5 | Under | 0.540294 | 1.8508 | +0.81 |
| Full Time Winning Margin (14 Way) | — | Home 1–2 | 0.062526 | 15.9933 | 15 |
| Full Time Winning Margin (14 Way) | — | Home 3–6 | 0.132742 | 7.5334 | 7.20 |
| Full Time Winning Margin (14 Way) | — | Home 7–9 | 0.099485 | 10.0517 | 9.60 |
| Full Time Winning Margin (14 Way) | — | Home 10–13 | 0.121727 | 8.2151 | 7.90 |
| Full Time Winning Margin (14 Way) | — | Home 14–16 | 0.077450 | 12.9116 | 12 |
| Full Time Winning Margin (14 Way) | — | Home 17–20 | 0.080608 | 12.4058 | 11 |
| Full Time Winning Margin (14 Way) | — | Home 21+ | 0.123358 | 8.1065 | 7.80 |
| Full Time Winning Margin (14 Way) | — | Away 1–2 | 0.055281 | 18.0894 | 17 |
| Full Time Winning Margin (14 Way) | — | Away 3–6 | 0.091964 | 10.8738 | 10 |
| Full Time Winning Margin (14 Way) | — | Away 7–9 | 0.051677 | 19.3511 | 18 |
| Full Time Winning Margin (14 Way) | — | Away 10–13 | 0.047650 | 20.9864 | 19 |
| Full Time Winning Margin (14 Way) | — | Away 14–16 | 0.022680 | 44.0913 | 39 |
| Full Time Winning Margin (14 Way) | — | Away 17–20 | 0.017828 | 56.0920 | 49 |
| Full Time Winning Margin (14 Way) | — | Away 21+ | 0.015025 | 66.5565 | 58 |
| Full Time Odd/Even | — | Odd | 0.500000 | 2.0000 | +0.96 |
| Full Time Odd/Even | — | Even | 0.500000 | 2.0000 | +0.96 |
| Full Time Last Digit Score | — | 0 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 1 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 2 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 3 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 4 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 5 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 6 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 7 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 8 | 0.100000 | 10.0000 | 9.50 |
| Full Time Last Digit Score | — | 9 | 0.100000 | 10.0000 | 9.50 |
The primary boards contain ten whole/half-point lines each. Other Markets contains only catalog books that are direct reads of one projected marginal. Team totals, team-score markets, and cross-market combinations still require an unidentified joint distribution.
Supported and unsupported outputs
Supported:
- Full Time: Handicap, Over/Under, Winning Margin (14 Way), Odd/Even, and Total Last Digit;
- Half Time: Handicap, Over/Under, Winning Margin (13 Way), and Odd/Even;
- Q1: Handicap, Over/Under, Winning Margin (7 Way), and Odd/Even;
- Q2/Q3/Q4: Handicap, Over/Under, and Odd/Even; and
- fair probabilities plus configurable two-way Power/Malay or multi-way Shin/decimal margin presentation.
Not supported by this marginal model:
- Moneyline as an output-policy choice;
- second-half-only markets;
- team totals, team last digit, exact score, or any team-score market;
- live/current-period or remaining-time markets; and
- same-game or cross-market joint probabilities.
Moneyline could be read from D, but remains intentionally excluded. Team and joint markets require information the four marginal quote lines do not identify. A provider whose Full Time settlement excludes overtime is also outside this input contract and must not be mixed with overtime-inclusive anchors.
The pipeline in one file
The dependency-free JavaScript port uses the same stripping, four-parameter solve, period projection, settlement readers, guards, and demo anchors as the TypeScript reference engine. Run it with Node:
node basketball-point-quarter.js
Download basketball-point-quarter.js, or inspect it here:
basketball-point-quarter.js — complete listing
// Basketball four-anchor marginal reference engine.
// Generated from the tested TypeScript source; dependency-free at runtime.
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// docs/src/lib/odds/basket-markets.ts
var basket_markets_exports = {};
__export(basket_markets_exports, {
BASKET_PERIODS: () => BASKET_PERIODS,
basketPeriod: () => basketPeriod,
buildBasketMarkets: () => buildBasketMarkets,
priceBasket: () => priceBasket,
quoteBasket: () => quoteBasket
});
module.exports = __toCommonJS(basket_markets_exports);
// docs/src/lib/odds/basket-core.ts
var SQRT_2PI = Math.sqrt(2 * Math.PI);
var GRID_W = 6.5;
var MIN_Z_GAP = 0.015;
var MAX_ANCHOR_RESIDUAL = 2e-6;
var MAX_CONDITION_NUMBER = 5e3;
var MAX_SOLVE_ITERS = 40;
function normCdf(z) {
if (z < 0) return 1 - normCdf(-z);
const t = 1 / (1 + 0.2316419 * z);
const poly = t * (0.31938153 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
return 1 - Math.exp(-0.5 * z * z) / SQRT_2PI * poly;
}
function normInv(p) {
const q = Math.min(1 - 1e-12, Math.max(1e-12, p));
let lo = -12;
let hi = 12;
for (let i = 0; i < 80; i++) {
const mid = (lo + hi) / 2;
if (normCdf(mid) < q) lo = mid;
else hi = mid;
}
return (lo + hi) / 2;
}
function integerNormal(mu, sigma, lo, hi) {
if (!Number.isFinite(mu) || !Number.isFinite(sigma) || !(sigma > 0) || hi < lo) return null;
const out = new Float64Array(hi - lo + 1);
let sum = 0;
for (let k = lo; k <= hi; k++) {
const p = normCdf((k + 0.5 - mu) / sigma) - normCdf((k - 0.5 - mu) / sigma);
const value = Math.max(0, p);
out[k - lo] = value;
sum += value;
}
if (!(sum > 0) || !Number.isFinite(sum)) return null;
for (let i = 0; i < out.length; i++) out[i] /= sum;
return out;
}
function buildMarginDistribution(mu, sigma, resolveTie) {
const limit = Math.ceil(Math.abs(mu) + GRID_W * sigma) + 2;
if (!(limit >= 2 && limit <= 1500)) return null;
const pmf = integerNormal(mu, sigma, -limit, limit);
if (!pmf) return null;
const unresolvedTie = pmf[limit];
if (resolveTie) {
const nonTie = 1 - unresolvedTie;
if (!(nonTie > 1e-9)) return null;
pmf[limit] = 0;
for (let i = 0; i < pmf.length; i++) pmf[i] /= nonTie;
}
return { offset: limit, pmf, unresolvedTie };
}
function buildTotalDistribution(mu, sigma) {
if (!(mu > 0) || !(sigma > 0)) return null;
const hi = Math.ceil(mu + GRID_W * sigma) + 2;
if (!(hi >= 4 && hi <= 2e3)) return null;
return integerNormal(mu, sigma, 0, hi);
}
function buildMarginalsAtShare(metrics, share, resolveMarginTie) {
if (!(share > 0 && share <= 1)) return null;
const root = Math.sqrt(share);
const margin = buildMarginDistribution(
metrics.muMargin * share,
metrics.sigmaMargin * root,
resolveMarginTie
);
const total = buildTotalDistribution(metrics.muTotal * share, metrics.sigmaTotal * root);
if (!margin || !total) return null;
return {
marginOffset: margin.offset,
marginPmf: margin.pmf,
totalPmf: total,
share,
marginTieResolved: resolveMarginTie,
unresolvedMarginTieProbability: margin.unresolvedTie
};
}
function buildBasketMarginals(metrics) {
return buildMarginalsAtShare(metrics, 1, true);
}
function buildBasketPeriodMarginals(metrics, share) {
if (!(share > 0 && share < 1)) return null;
return buildMarginalsAtShare(metrics, share, false);
}
function isBasketLine(line) {
return Number.isFinite(line) && Math.abs(line * 2 - Math.round(line * 2)) < 1e-9;
}
function coverRead(marginPmf, offset, line) {
if (!isBasketLine(line)) return null;
let W = 0;
let R = 0;
for (let i = 0; i < marginPmf.length; i++) {
const mass = marginPmf[i];
if (!(mass > 0)) continue;
const margin = i - offset;
const diff = margin + line;
if (diff > 1e-9) W += mass;
else if (diff > -1e-9) R += mass;
}
return 1 - R > 1e-12 ? { W, R, p: W / (1 - R) } : null;
}
function overRead(totalPmf, line) {
if (!isBasketLine(line)) return null;
let W = 0;
let R = 0;
for (let total = 0; total < totalPmf.length; total++) {
const mass = totalPmf[total];
if (!(mass > 0)) continue;
const diff = total - line;
if (diff > 1e-9) W += mass;
else if (diff > -1e-9) R += mass;
}
return 1 - R > 1e-12 ? { W, R, p: W / (1 - R) } : null;
}
function allReadsPresent(values) {
return values.every((value) => value != null);
}
function conditionNumber2x2(a, b, c, d) {
const trace = a * a + b * b + c * c + d * d;
const det2 = (a * d - b * c) ** 2;
const disc = Math.sqrt(Math.max(0, trace * trace - 4 * det2));
const hi = (trace + disc) / 2;
const lo = (trace - disc) / 2;
return lo > 1e-24 ? Math.sqrt(hi / lo) : Infinity;
}
function solvePair(anchors, kind, read) {
const [a, b] = anchors;
if (!Number.isFinite(a.line) || !Number.isFinite(b.line) || Math.abs(a.line - b.line) < 1e-9)
return `${kind} anchor lines must be distinct`;
if (!Number.isFinite(a.probability) || !Number.isFinite(b.probability) || !(a.probability > 0 && a.probability < 1) || !(b.probability > 0 && b.probability < 1))
return `${kind} fair probabilities must lie strictly between 0 and 1`;
const z1 = normInv(a.probability);
const z2 = normInv(b.probability);
const zGap = Math.abs(z1 - z2);
if (zGap < MIN_Z_GAP) return `${kind} anchors are too close to identify dispersion reliably`;
const sigma0 = kind === "handicap" ? (a.line - b.line) / (z1 - z2) : (b.line - a.line) / (z1 - z2);
if (!(sigma0 > 0) || !Number.isFinite(sigma0))
return `${kind} anchors are contradictory (they imply a non-positive dispersion)`;
let mu = kind === "handicap" ? sigma0 * z1 - a.line : a.line + sigma0 * z1;
let logSigma = Math.log(sigma0);
if (!Number.isFinite(mu) || sigma0 < 0.25 || sigma0 > 200)
return `${kind} anchors imply a dispersion outside the numerical domain`;
let iterations = 0;
let finalCondition = Infinity;
for (; iterations < MAX_SOLVE_ITERS; iterations++) {
const sigma2 = Math.exp(logSigma);
const p12 = read(mu, sigma2, a.line);
const p22 = read(mu, sigma2, b.line);
if (p12 == null || p22 == null) return `${kind} settlement grid could not be built`;
const r1 = p12 - a.probability;
const r2 = p22 - b.probability;
if (Math.max(Math.abs(r1), Math.abs(r2)) <= MAX_ANCHOR_RESIDUAL) break;
const hMu2 = Math.max(1e-3, sigma2 * 1e-4);
const hLog2 = 1e-4;
const sigmaP = Math.exp(logSigma + hLog2);
const sigmaM = Math.exp(logSigma - hLog2);
const reads = [
read(mu + hMu2, sigma2, a.line),
read(mu - hMu2, sigma2, a.line),
read(mu + hMu2, sigma2, b.line),
read(mu - hMu2, sigma2, b.line),
read(mu, sigmaP, a.line),
read(mu, sigmaM, a.line),
read(mu, sigmaP, b.line),
read(mu, sigmaM, b.line)
];
if (!allReadsPresent(reads)) return `${kind} Jacobian could not be evaluated`;
const [p1MuP, p1MuM, p2MuP, p2MuM, p1SigP, p1SigM, p2SigP, p2SigM] = reads;
const j11 = (p1MuP - p1MuM) / (2 * hMu2);
const j21 = (p2MuP - p2MuM) / (2 * hMu2);
const j12 = (p1SigP - p1SigM) / (2 * hLog2);
const j22 = (p2SigP - p2SigM) / (2 * hLog2);
const det = j11 * j22 - j12 * j21;
finalCondition = conditionNumber2x2(j11, j12, j21, j22);
if (!Number.isFinite(det) || Math.abs(det) < 1e-12)
return `${kind} anchors are numerically singular`;
let dMu = (-r1 * j22 + j12 * r2) / det;
let dLog = (j21 * r1 - j11 * r2) / det;
dMu = Math.max(-2 * sigma2, Math.min(2 * sigma2, dMu));
dLog = Math.max(-0.7, Math.min(0.7, dLog));
const oldLoss = r1 * r1 + r2 * r2;
let accepted = false;
for (let step = 1; step >= 1 / 128; step /= 2) {
const nextMu = mu + step * dMu;
const nextLog = logSigma + step * dLog;
const nextSigma = Math.exp(nextLog);
if (!(nextSigma >= 0.25 && nextSigma <= 200)) continue;
const q1 = read(nextMu, nextSigma, a.line);
const q2 = read(nextMu, nextSigma, b.line);
if (q1 == null || q2 == null) continue;
const loss = (q1 - a.probability) ** 2 + (q2 - b.probability) ** 2;
if (loss < oldLoss) {
mu = nextMu;
logSigma = nextLog;
accepted = true;
break;
}
}
if (!accepted) return `${kind} solve stalled before reproducing both anchors`;
}
const sigma = Math.exp(logSigma);
const p1 = read(mu, sigma, a.line);
const p2 = read(mu, sigma, b.line);
if (p1 == null || p2 == null) return `${kind} final read failed`;
const residuals = [
Math.abs(p1 - a.probability),
Math.abs(p2 - b.probability)
];
const hMu = Math.max(1e-3, sigma * 1e-4);
const hLog = 1e-4;
const values = [
read(mu + hMu, sigma, a.line),
read(mu - hMu, sigma, a.line),
read(mu + hMu, sigma, b.line),
read(mu - hMu, sigma, b.line),
read(mu, Math.exp(logSigma + hLog), a.line),
read(mu, Math.exp(logSigma - hLog), a.line),
read(mu, Math.exp(logSigma + hLog), b.line),
read(mu, Math.exp(logSigma - hLog), b.line)
];
if (allReadsPresent(values)) {
finalCondition = conditionNumber2x2(
(values[0] - values[1]) / (2 * hMu),
(values[4] - values[5]) / (2 * hLog),
(values[2] - values[3]) / (2 * hMu),
(values[6] - values[7]) / (2 * hLog)
);
}
if (Math.max(...residuals) > MAX_ANCHOR_RESIDUAL)
return `${kind} solve did not reproduce both anchors within tolerance`;
if (!Number.isFinite(finalCondition) || finalCondition > MAX_CONDITION_NUMBER)
return `${kind} anchors are too weakly conditioned for reliable regeneration`;
return { mu, sigma, residuals, conditionNumber: finalCondition, zGap, iterations };
}
function solveBasket(input) {
const margin = solvePair(input.handicapAnchors, "handicap", (mu, sigma, line) => {
const distribution = buildMarginDistribution(mu, sigma, true);
return distribution ? coverRead(distribution.pmf, distribution.offset, line)?.p ?? null : null;
});
if (typeof margin === "string") return { ok: false, reason: margin };
const total = solvePair(input.totalAnchors, "total", (mu, sigma, line) => {
const distribution = buildTotalDistribution(mu, sigma);
return distribution ? overRead(distribution, line)?.p ?? null : null;
});
if (typeof total === "string") return { ok: false, reason: total };
const metrics = {
muMargin: margin.mu,
sigmaMargin: margin.sigma,
muTotal: total.mu,
sigmaTotal: total.sigma
};
const marginals = buildBasketMarginals(metrics);
if (!marginals) return { ok: false, reason: "full-time marginals failed at the solution" };
return {
ok: true,
metrics,
marginals,
diagnostics: {
anchorResiduals: [...margin.residuals, ...total.residuals],
conditioning: "good",
marginConditionNumber: margin.conditionNumber,
totalConditionNumber: total.conditionNumber,
marginZGap: margin.zGap,
totalZGap: total.zGap,
iterations: margin.iterations + total.iterations
}
};
}
// docs/src/lib/odds/core.ts
function malayToDecimalExact(m) {
if (!Number.isFinite(m) || m === 0 || m < -1 || m > 1) return null;
return m > 0 ? 1 + m : 1 - 1 / m;
}
function ladderMargin(mHome, mAway) {
if (mHome > 0 && mAway > 0) return 2 - (mHome + mAway);
if (mHome > 0 && mAway < 0) return -mAway - mHome;
if (mHome < 0 && mAway > 0) return -mHome - mAway;
return null;
}
function powerStrip(q1, q2) {
if (!(q1 > 0 && q2 > 0)) return null;
if (q1 + q2 <= 1) return null;
const lnQ1 = Math.log(q1);
const lnQ2 = Math.log(q2);
let x = 0.9;
for (let i = 0; i < 200; i++) {
const inv = 1 / x;
const e1 = Math.pow(q1, inv);
const e2 = Math.pow(q2, inv);
const f = e1 + e2 - 1;
if (Math.abs(f) < 1e-14) break;
const fp = (-e1 * lnQ1 - e2 * lnQ2) / (x * x);
if (fp === 0) break;
x = x - f / fp;
if (x < 1e-6) x = 1e-6;
if (x > 1 - 1e-12) x = 1 - 1e-12;
}
const inv2 = 1 / x;
return { p1: Math.pow(q1, inv2), p2: Math.pow(q2, inv2), exponent: x };
}
function stripTwoWayMalay(mHome, mAway) {
const dH = malayToDecimalExact(mHome);
const dA = malayToDecimalExact(mAway);
if (dH === null || dA === null) return null;
const qH = 1 / dH;
const qA = 1 / dA;
if (qH + qA <= 1) return null;
const strip = powerStrip(qH, qA);
if (!strip) return null;
return {
pHome: strip.p1,
pAway: strip.p2,
exponent: strip.exponent,
decimalHome: dH,
decimalAway: dA,
pricedHome: qH,
pricedAway: qA,
overround: qH + qA,
ladderMargin: ladderMargin(mHome, mAway)
};
}
// docs/src/lib/odds/basket-markets.ts
var BASKET_PERIODS = {
full: { label: "Full Time", share: 1 },
p12: { label: "Half Time", share: 0.5 },
q1: { label: "Q1", share: 0.25 },
q2: { label: "Q2", share: 0.25 },
q3: { label: "Q3", share: 0.25 },
q4: { label: "Q4", share: 0.25 }
};
function basketPeriod(metrics, id) {
const definition = BASKET_PERIODS[id];
if (!definition) return null;
const share = definition.share;
const periodMetrics = id === "full" ? metrics : {
muMargin: metrics.muMargin * share,
sigmaMargin: metrics.sigmaMargin * Math.sqrt(share),
muTotal: metrics.muTotal * share,
sigmaTotal: metrics.sigmaTotal * Math.sqrt(share)
};
const marginals = id === "full" ? buildBasketMarginals(metrics) : buildBasketPeriodMarginals(metrics, definition.share);
return marginals ? { id, definition, metrics: periodMetrics, marginals } : null;
}
function stripAnchor(anchor, label) {
if (!isBasketLine(anchor.line))
return `${label} line must be a whole or half point (no .25/.75)`;
const strip = stripTwoWayMalay(anchor.prices[0], anchor.prices[1]);
if (!strip)
return `${label} price pair invalid (Malay must be non-zero in [\u22121,+1] with positive overround)`;
return { ...anchor, strip };
}
function isAnchorPair(value) {
return Array.isArray(value) && value.length === 2 && value.every(
(anchor) => anchor != null && typeof anchor === "object" && "line" in anchor && "prices" in anchor && Array.isArray(anchor.prices) && anchor.prices.length === 2
);
}
function priceBasket(input) {
const unchecked = input;
if (!isAnchorPair(unchecked.handicapAnchors) || !isAnchorPair(unchecked.totalAnchors))
return { ok: false, reason: "exactly two Handicap and two Total anchors are required" };
const handicapAnchors = unchecked.handicapAnchors;
const totalAnchors = unchecked.totalAnchors;
const h1 = stripAnchor(handicapAnchors[0], "Handicap anchor 1");
if (typeof h1 === "string") return { ok: false, reason: h1 };
const h2 = stripAnchor(handicapAnchors[1], "Handicap anchor 2");
if (typeof h2 === "string") return { ok: false, reason: h2 };
const t1 = stripAnchor(totalAnchors[0], "Total anchor 1");
if (typeof t1 === "string") return { ok: false, reason: t1 };
const t2 = stripAnchor(totalAnchors[1], "Total anchor 2");
if (typeof t2 === "string") return { ok: false, reason: t2 };
if (!(t1.line > 0 && t2.line > 0))
return { ok: false, reason: "Total lines must be positive" };
const solved = solveBasket({
handicapAnchors: [
{ line: h1.line, probability: h1.strip.pHome },
{ line: h2.line, probability: h2.strip.pHome }
],
totalAnchors: [
{ line: t1.line, probability: t1.strip.pHome },
{ line: t2.line, probability: t2.strip.pHome }
]
});
if (!solved.ok) return solved;
return {
ok: true,
anchors: { handicap: [h1, h2], total: [t1, t2] },
metrics: solved.metrics,
diagnostics: solved.diagnostics,
marginals: solved.marginals
};
}
function quoteBasket(marginals, request) {
if (!Number.isFinite(request.line)) return null;
return request.market === "handicap" ? coverRead(marginals.marginPmf, marginals.marginOffset, request.line) : overRead(marginals.totalPmf, request.line);
}
var LADDER_LINE_COUNT = 10;
function ladderLines(center, step) {
const below = Math.floor((LADDER_LINE_COUNT - 1) / 2);
const halfPointCenter = Math.round(center * 2) / 2;
return Array.from(
{ length: LADDER_LINE_COUNT },
(_, index) => halfPointCenter + (index - below) * step
);
}
function handicapMarket(marginals, center, period) {
const groups = ladderLines(center, 0.5).map((line) => {
const read = quoteBasket(marginals, { market: "handicap", line });
return {
label: (line > 0 ? "+" : "") + line.toFixed(1),
method: "ladder",
outcomes: [
{ label: "Home", p: read?.p ?? NaN },
{ label: "Away", p: read ? 1 - read.p : NaN }
]
};
});
return {
key: "handicap",
title: `${period.label} Handicap`,
note: marginals.marginTieResolved ? "full-time settlement includes OT; the final draw is conditioned away" : "a tied period pushes a zero or whole-point line; fair odds condition away the refund",
groups
};
}
function totalMarket(marginals, center, period) {
const groups = ladderLines(center, 0.5).map((line) => {
const read = quoteBasket(marginals, { market: "total", line });
return {
label: line.toFixed(1),
method: "ladder",
outcomes: [
{ label: "Over", p: read?.p ?? NaN },
{ label: "Under", p: read ? 1 - read.p : NaN }
]
};
});
return {
key: "over-under",
title: `${period.label} Over/Under`,
note: "whole-point pushes are refund-normalized; half-point lines cannot push",
groups
};
}
var WINNING_MARGIN_BANDS_FULL = [
[1, 2],
[3, 6],
[7, 9],
[10, 13],
[14, 16],
[17, 20],
[21, Infinity]
];
var WINNING_MARGIN_BANDS_HALF = [
[1, 5],
[6, 10],
[11, 15],
[16, 20],
[21, 25],
[26, Infinity]
];
var WINNING_MARGIN_BANDS_QUARTER = [
[1, 4],
[5, 8],
[9, Infinity]
];
function sumPmf(pmf, offset, predicate) {
let probability = 0;
for (let index = 0; index < pmf.length; index++) {
if (predicate(index - offset)) probability += pmf[index];
}
return probability;
}
function winningMarginMarket(marginals, periodId, period) {
const bands = periodId === "full" ? WINNING_MARGIN_BANDS_FULL : periodId === "p12" ? WINNING_MARGIN_BANDS_HALF : WINNING_MARGIN_BANDS_QUARTER;
const withDraw = periodId !== "full";
const bandLabel = ([lo, hi]) => hi === Infinity ? `${lo}+` : `${lo}\u2013${hi}`;
const outcomes = [];
for (const band of bands) {
outcomes.push({
label: `Home ${bandLabel(band)}`,
p: sumPmf(
marginals.marginPmf,
marginals.marginOffset,
(margin) => margin >= band[0] && margin <= band[1]
)
});
}
if (withDraw) {
outcomes.push({
label: "Draw",
p: sumPmf(marginals.marginPmf, marginals.marginOffset, (margin) => margin === 0)
});
}
for (const band of bands) {
outcomes.push({
label: `Away ${bandLabel(band)}`,
p: sumPmf(
marginals.marginPmf,
marginals.marginOffset,
(margin) => -margin >= band[0] && -margin <= band[1]
)
});
}
return {
key: "winning-margin",
title: `${period.label} Winning Margin (${outcomes.length} Way)`,
note: withDraw ? "the draw outcome is the period tie" : "no draw outcome after full-time OT",
groups: [{ method: "shin", outcomes }]
};
}
function oddEvenMarket(marginals, period) {
const odd = sumPmf(marginals.totalPmf, 0, (total) => total % 2 !== 0);
return {
key: "odd-even",
title: `${period.label} Odd/Even`,
note: "directly read from the selected period total marginal",
groups: [
{
method: "shin",
outcomes: [
{ label: "Odd", p: odd },
{ label: "Even", p: 1 - odd }
]
}
]
};
}
function lastDigitMarket(marginals) {
const outcomes = Array.from({ length: 10 }, (_, digit) => ({
label: String(digit),
p: sumPmf(marginals.totalPmf, 0, (total) => total % 10 === digit)
}));
return {
key: "last-digit",
title: "Full Time Last Digit Score",
note: "last digit of the integer full-time total; not either team\u2019s score digit",
groups: [{ method: "shin", outcomes }]
};
}
function buildBasketMarkets(marginals, options) {
const period = BASKET_PERIODS[options.periodId];
const all = [
handicapMarket(marginals, options.hcpCenter, period),
totalMarket(marginals, options.totalCenter, period)
];
if (options.periodId === "full" || options.periodId === "p12" || options.periodId === "q1")
all.push(winningMarginMarket(marginals, options.periodId, period));
all.push(oddEvenMarket(marginals, period));
if (options.periodId === "full") all.push(lastDigitMarket(marginals));
if (!options.include) return all;
const include = new Set(options.include);
return all.filter((market) => include.has(market.key));
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BASKET_PERIODS,
basketPeriod,
buildBasketMarkets,
priceBasket,
quoteBasket
});
if (require.main === module) {
const demo = priceBasket({
handicapAnchors: [
{ line: -6.5, prices: [0.95, 0.97] },
{ line: -9.5, prices: [-0.72, 0.64] }
],
totalAnchors: [
{ line: 224.5, prices: [0.95, 0.97] },
{ line: 228.5, prices: [-0.76, 0.68] }
]
});
if (!demo.ok) throw new Error(demo.reason);
console.log("basketball four-anchor Full Time solve");
console.table(demo.metrics);
for (const periodId of ["full", "p12", "q1", "q2", "q3", "q4"]) {
const view = basketPeriod(demo.metrics, periodId);
if (!view) throw new Error(`could not build ${periodId}`);
const hcpCenter = periodId === "full" ? -8.5 : -view.metrics.muMargin;
const totalCenter = periodId === "full" ? 226.5 : view.metrics.muTotal;
console.log(
view.definition.label,
buildBasketMarkets(view.marginals, { hcpCenter, totalCenter, periodId }).map(
(market) => market.key
)
);
}
}