Skip to main content

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):

  1. strips all four bookmaker margins with the two-way Power method;
  2. back-solves Full Time Gaussian margin D and total T marginals;
  3. refines their four metrics against integer settlement reads so every anchor, whole-point push, and half-point line is reproduced;
  4. projects Half Time and Q1–Q4 with equal-duration Gaussian scaling; and
  5. 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 assumptionReplacement
σD = 12.5 league prioridentified from the second Handicap line
σT = 1.1·√μTidentified from the second Total line
Moneyline partial poolingremoved
ρDT = 0 and a team-score jointnot constructed
FIBA/NBA overtime fractionfinal 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.

Handicap anchor 1 · Full Time incl. OT
Total anchor 1 · Full Time incl. OT
Handicap anchor 2 · same snapshot
Total anchor 2 · same snapshot
period Full TimeμD 6.137σD 12.214μT 224.622σT 18.560share 1.00
Score grid
Full Time marginals
Margin D = Home − Away
-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-10+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.00.10.10.10.10.10.20.20.20.30.30.40.50.60.70.80.91.01.11.31.41.61.71.92.12.22.42.52.72.80.03.13.23.33.33.33.43.43.33.33.23.13.02.92.72.62.42.32.11.91.81.61.41.31.21.00.90.80.70.60.50.40.40.30.20.20.20.10.10.10.10.10.0
Full Time includes OT, so the final draw is conditioned away
Total T = Home + Away
173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
0.00.10.10.10.10.10.10.10.10.20.20.20.20.20.30.30.30.40.40.50.50.60.60.70.70.80.80.91.01.01.11.21.21.31.41.41.51.61.61.71.81.81.91.92.02.02.12.12.12.12.12.12.12.12.12.12.12.12.02.01.91.91.81.81.71.71.61.51.51.41.31.21.21.11.01.00.90.80.80.70.70.60.60.50.50.40.40.30.30.30.30.20.20.20.20.10.10.10.10.10.10.10.10.00.0
fitted directly to Full Time markets including OT
Markets
Full Time Handicap
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
Full Time Over/Under
Points224.5225.0225.5226.0226.5227.0227.5228.0228.5229.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
3 more in scope
The Handicap and Over/Under boards above always show. Pick another in-scope market here to price it from the selected period distributions.
1 · Strip all four bookmaker margins (Power method)

Convert each Malay quote to decimal dd and priced implied probability q=1/dq=1/d. For each two-way book, solve one Power exponent xx so q11/x+q21/x=1q_1^{1/x}+q_2^{1/x}=1; the two fair legs are then pi=qi1/xp_i=q_i^{1/x}. The first fair leg is the hard target used below.

Handicap -6.5 → P(home covers)
HomeAway
Malay quote+0.95+0.97
Decimal dd1.9500001.970000
Fair p=q1/xp = q^{1/x}0.5026280.497372
Fair decimal 1/p1/p1.9895442.010566
0.5128211/x+0.5076141/x=1    x=0.9708160.512821^{1/x} + 0.507614^{1/x} = 1 \;\Rightarrow\; x = 0.970816
Handicap -9.5 → P(home covers)
HomeAway
Malay quote-0.72+0.64
Decimal dd2.3888891.640000
Fair p=q1/xp = q^{1/x}0.4031420.596858
Fair decimal 1/p1/p2.4805131.675442
0.4186051/x+0.6097561/x=1    x=0.9585710.418605^{1/x} + 0.609756^{1/x} = 1 \;\Rightarrow\; x = 0.958571
Total 224.5 → P(over)
OverUnder
Malay quote+0.95+0.97
Decimal dd1.9500001.970000
Fair p=q1/xp = q^{1/x}0.5026280.497372
Fair decimal 1/p1/p1.9895442.010566
0.5128211/x+0.5076141/x=1    x=0.9708160.512821^{1/x} + 0.507614^{1/x} = 1 \;\Rightarrow\; x = 0.970816
Total 228.5 → P(over)
OverUnder
Malay quote-0.76+0.68
Decimal dd2.3157891.680000
Fair p=q1/xp = q^{1/x}0.4172500.582750
Fair decimal 1/p1/p2.3966471.716001
0.4318181/x+0.5952381/x=1    x=0.9607360.431818^{1/x} + 0.595238^{1/x} = 1 \;\Rightarrow\; x = 0.960736
2 · Back-solve the four continuous Gaussian metrics

Transform each fair target to a standard-normal score zi=Φ1(pi)z_i=\Phi^{-1}(p_i). The two Handicap equations identify the margin pair (μD,σD)(\mu_D,\sigma_D); the two Total equations independently identify (μT,σT)(\mu_T,\sigma_T).

AnchorLineFair target p*z = Φ⁻¹(p*)
Handicap 1-6.50.5026280.006587
Handicap 2-9.50.403142-0.245222
Total 1224.50.5026280.006587
Total 2228.50.417250-0.208935
σD(0)=12z1z2=11.913825,μD(0)=σD(0)z11=6.578473\sigma_D^{(0)}=\frac{\ell_1-\ell_2}{z_1-z_2}=11.913825,\qquad \mu_D^{(0)}=\sigma_D^{(0)}z_1-\ell_1=6.578473
σT(0)=q2q1z1z2=18.559650,μT(0)=q1+σT(0)z1=224.622247\sigma_T^{(0)}=\frac{q_2-q_1}{z_1-z_2}=18.559650,\qquad \mu_T^{(0)}=q_1+\sigma_T^{(0)}z_1=224.622247

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 (μD,logσD)(\mu_D,\log\sigma_D) and (μT,logσT)(\mu_T,\log\sigma_T) until the exact settlement reader matches both targets on each marginal. For a whole-point line, fair probability conditions away the push: p=W/(1R)p=W/(1-R). Half-point lines have R=0R=0.

(μD,σD,μT,σT)=(6.137476, 12.213529, 224.622247, 18.559650)(\mu_D,\sigma_D,\mu_T,\sigma_T)=(6.137476,\ 12.213529,\ 224.622247,\ 18.559650)
AnchorLineTarget p*Model pWR|Δ|
Handicap 1-6.50.5026280.5026280.5026280.0000006.13e-8
Handicap 2-9.50.4031420.4031420.4031420.0000002.14e-8
Total 1224.50.5026280.5026280.5026280.0000006.20e-12
Total 2228.50.4172500.4172500.4172500.0000007.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 s=1.0000s=1.0000. Under the equal-duration Gaussian projection, means scale by ss and standard deviations by s\sqrt{s}.

(μD,s,σD,s,μT,s,σT,s)=(sμD,sσD,sμT,sσT)=(6.137476, 12.213529, 224.622247, 18.559650)(\mu_{D,s},\sigma_{D,s},\mu_{T,s},\sigma_{T,s})=(s\mu_D,\sqrt{s}\sigma_D,s\mu_T,\sqrt{s}\sigma_T)=(6.137476,\ 12.213529,\ 224.622247,\ 18.559650)

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 WW and RR from the selected marginals and compute p=W/(1R)p=W/(1-R). 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 outputLineOutcomeFair pFair decimalDisplayed odds
Full Time Handicap-8.0Home0.4508382.2181-0.86
Full Time Handicap-8.0Away0.5491621.8210+0.78
Full Time Over/Under226.5Over0.4597062.1753-0.89
Full Time Over/Under226.5Under0.5402941.8508+0.81
Full Time Winning Margin (14 Way)Home 1–20.06252615.993315
Full Time Winning Margin (14 Way)Home 3–60.1327427.53347.20
Full Time Winning Margin (14 Way)Home 7–90.09948510.05179.60
Full Time Winning Margin (14 Way)Home 10–130.1217278.21517.90
Full Time Winning Margin (14 Way)Home 14–160.07745012.911612
Full Time Winning Margin (14 Way)Home 17–200.08060812.405811
Full Time Winning Margin (14 Way)Home 21+0.1233588.10657.80
Full Time Winning Margin (14 Way)Away 1–20.05528118.089417
Full Time Winning Margin (14 Way)Away 3–60.09196410.873810
Full Time Winning Margin (14 Way)Away 7–90.05167719.351118
Full Time Winning Margin (14 Way)Away 10–130.04765020.986419
Full Time Winning Margin (14 Way)Away 14–160.02268044.091339
Full Time Winning Margin (14 Way)Away 17–200.01782856.092049
Full Time Winning Margin (14 Way)Away 21+0.01502566.556558
Full Time Odd/EvenOdd0.5000002.0000+0.96
Full Time Odd/EvenEven0.5000002.0000+0.96
Full Time Last Digit Score00.10000010.00009.50
Full Time Last Digit Score10.10000010.00009.50
Full Time Last Digit Score20.10000010.00009.50
Full Time Last Digit Score30.10000010.00009.50
Full Time Last Digit Score40.10000010.00009.50
Full Time Last Digit Score50.10000010.00009.50
Full Time Last Digit Score60.10000010.00009.50
Full Time Last Digit Score70.10000010.00009.50
Full Time Last Digit Score80.10000010.00009.50
Full Time Last Digit Score90.10000010.00009.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
)
);
}
}