Methodology

How the Hoops PSM live totals model works

A line-by-line breakdown of the projection engine — rate blending, time-decay regression, momentum windows, capped extrapolation, EV/Kelly staking, scenario grids, and the halftime green-light gate — with real arithmetic for both NBA and college basketball.

Hoops PSM (Points-per-minute / Possessions / Structure / Momentum) is not a black box. Every number on the live dashboard traces back to a small set of transparent formulas. This page walks through those formulas in the exact order the engine evaluates them, with the same variable names used in the code, so you can audit any output by hand. If you can do arithmetic with a calculator, you can reproduce every projection the tool shows you.

1. The two raw projection methods

At any moment in a live game the engine has two independent ways to project the final combined score: a points-per-minute (PPM) method and a points-per-possession (PPP) method. Both start from the same idea — take an observed scoring rate, blend it with a pregame expectation, then multiply by the time or possessions remaining.

1.1 Points-per-minute (PPM) projection

Let t be minutes elapsed, p be total combined points scored so far, and totalMinutes be 48 for NBA or 40 for CBB. The remaining minutes are R = totalMinutes − t. The baseline rate comes straight from the pregame total:

  • baselinePPM = pregameTotal / totalMinutes — e.g. a 226.5 NBA total implies a baseline of 226.5 / 48 = 4.719 points per minute combined.
  • obs = estimateRate(p / t, baselinePPM, t) — the observed rate so far, shrunk toward the baseline and then clamped (see §2 below) so an early hot stretch can’t explode the projection.

1.2 Points-per-possession (PPP) projection

If you supply a possessions-used count, the engine mirrors the same logic on a per-possession basis. possTotal is your estimate of total possessions (both teams) for the full game — a common CBB input is around 140, and NBA around 200.

  • baselinePPP = pregameTotal / possTotal.
  • obs = estimateRate(p / possUsed, baselinePPP, possUsed) — points per possession so far, shrunk with a prior worth 8 possessions and bounded by a league-relative ceiling (1.78 PPP for the NBA, 1.58 for CBB).
  • Remaining possessions: Prem = possTotal − possUsed, and the final projection is p + pppAdj × Prem.

If no possession count is entered, the PPP method simply returns null and the engine falls back to PPM.

2. Small samples — why a hot start doesn’t run away with the projection

Three minutes into a game, a 14-2 run is 5.3 combined points per minute. Extrapolated naively, that’s a 213-point college game. The model has to discount it — but a hard ceiling is the wrong tool for the job, and it’s worth being precise about why.

2.1 Shrinkage does the work

The projection is a weighted average of what’s happened so far and what we expected before tip-off:

projected = w × observed + (1 − w) × baseline, where w = n / (n + k).

n is the sample so far — elapsed minutes for pace, completed possessions for efficiency. k is how much the pregame expectation is worth in the same units (6 minutes for pace, 8 possessions for efficiency). At n = k the two carry equal weight; by the second half the observed data dominates and the prior fades out. Run the 14-2 example through it: three minutes in, w = 0.33, and the projection lands at 167 rather than 213. Nothing was clamped — the estimator simply doesn’t believe three minutes of basketball very much yet.

This is the part that matters, because it works on the whole distribution. A rate sitting at 1.4× expectation in minute five is far more common than a 14-2 run and does far more cumulative damage to a projection — and a ceiling never touches it.

2.2 The clamp is a backstop

After shrinkage, the blended rate is bounded on both sides. The allowed band widens continuously with elapsed time as the sample earns credibility:

band(t) = mMin + (mMax − mMin) × (min(t, 16) / 16)^1.8

Elapsed time (t)Pace band (PPM)Efficiency band (PPP)
0 min2.00×1.25×
4 min2.25×1.30×
8 min2.86×1.84× (ceiling binds)
12 min3.79×1.84× (ceiling binds)
16 min+5.00×1.84× (ceiling binds)

There are no tier boundaries, so the projection never jumps because the clock ticked past a round number. The floor is the reciprocal of the ceiling. A 6-2 game after four minutes is the same small-sample artefact as a 14-2 game, and it gets the same treatment. Capping only the upside would bias the model toward unders in exactly the spots where it knows the least.

2.3 Absolute ceilings

Each channel also carries an absolute ceiling set as a multiple of the league baseline — 2.60× for pace, 1.55× for efficiency. For college that’s 9.4 combined PPM and 1.58 PPP; for the NBA, 12.5 PPM and 1.78 PPP. They’re league-relative because a fixed number tuned for NBA pace is far too loose for a 40-minute college game.

Because the effective ceiling is min(absolute, baseline × band), the two terms trade off: the multiplier governs early and the absolute ceiling governs late. For a league-average matchup the handoff happens around minute 6.5 for pace and minute 8.4 for efficiency. The live model reports which term is active rather than assuming.

Earlier versions of this page described a four-tier step schedule with fixed 12.0 PPM and 1.8 PPP ceilings and no floor. That version capped only the upside, jumped discontinuously at 4:00, 8:00 and 16:00, and applied NBA-scaled ceilings to college games.

3. Time confidence and the regression term

The engine treats "how much do I trust the observed rate" as a function of elapsed time via an exponential decay curve:

confidence = 1 − 0.5^(t / halfLife)

halfLife is a setting (in minutes) that controls how quickly confidence in the observed live rate builds. With a halfLife of 10 minutes, confidence is 50% at t=10, 75% at t=20, and 93.75% at t=40. A shorter halfLife trusts live data faster; a longer one keeps the model anchored to the pregame total longer.

The blend between the observed rate and the baseline rate is controlled by a regression-effective weight:

regEff = clamp(0.65 × regression, 0, 1)

Here regression is a user-set slider (0 to 1) representing your prior belief that the game will revert toward the pregame number. This used to carry an extra 0.55 × (1 − confidence) term, but §2’s shrinkage already pulls small samples toward the baseline; keeping both applied the same correction twice and over-anchored early projections. Time confidence still drives the momentum weight below.

The blended per-minute (or per-possession) rate is then:

ppmBlend = (1 − regEff) × obs + regEff × baseline

4. Momentum — weighting the last few minutes

Beyond overall pace, the engine also tracks a short recency window using scoring markers you log during the game (timestamp + cumulative combined points). momWindow is the lookback length in minutes (a common default is 6–8). The recent rate is:

ppmRecent = (pNow − p₀) / (tNow − t₀), where p₀ is the interpolated score at tNow − momWindow.

This recent rate goes through the same shrink-then-clamp estimator, but on a widened schedule (band up to 5.8× and a 3.05× league-baseline ceiling, with a looser floor), since short windows are naturally noisier and a legitimate hot stretch can run faster than a full-game pace without being an outlier. The sample size used for shrinkage is the window length, not the full elapsed clock.

The momentum weight actually applied scales with both your momWeight slider and current confidence:

momEff = clamp(momWeight × (0.35 + 0.65 × confidence), 0, 1)

This means momentum has a floor influence of 35% of your slider setting even very early in the game, rising to 100% of the slider value as confidence approaches 1. The final adjusted rate folds momentum into the regression-blended rate:

ppmAdj = (1 − momEff) × ppmBlend + momEff × ppmRecent

The full PPM projection is then simply proj = pNow + ppmAdj × R, remaining minutes times the fully blended, momentum-adjusted, capped rate.

5. The BLEND source and blend weight

The projSource setting picks which method drives the displayed projection: "PPM", "POSS", or "BLEND". In BLEND mode the engine mixes the two independent projections using blendW (0 to 1, the weight on the PPM method):

chosen = blendW × ppmProjection + (1 − blendW) × pppProjection

Confidence is blended the same way. A blendW of 0.5 gives PPM and PPP equal say; 0.7 leans heavily on the per-minute clock-based method (useful when possession counts are rough estimates); 0.3 leans on possessions (useful in slow, foul-heavy CBB games where pace swings matter more than raw minutes). If possessions data is missing, the engine silently falls back to pure PPM regardless of blendW.

Why two methods at all?
Minutes and possessions diverge whenever pace changes mid-game. A team that starts fouling or a game that goes to a slow, half-court grind will show a falling PPP rate well before PPM "notices," because PPM only cares about the clock. Blending the two gives you a cross-check: when PPM and PPP disagree sharply, that disagreement itself is informative — it usually means pace, not shooting variance, is driving the score.

6. From projection to a betting signal

6.1 Margin and lean

Once you have a chosen total projection, compare it to the live market total (liveLine): margin = chosen − liveLine. The engine tags a lean using a 1.0-point no-man's-land band:

  • margin > 1.0 → Over
  • margin < −1.0 → Under
  • otherwise → No bet

6.2 Signal rating (SR)

Signal rating condenses margin size and confidence into a single 0–100 score:

SR = clamp((|margin| / 10) × (0.35 + 0.65 × confidence) × 100, 0, 100)

A 10-point margin at full confidence produces an SR of 100; a 5-point margin at 50% confidence produces roughly (5/10) × (0.35 + 0.325) × 100 ≈ 33.75. SR is a convenient way to rank multiple live games at once — it rewards both edge size and how much you can trust that edge given the current point in the game.

6.3 Sigma, win probability, and normal CDF

The engine treats the final combined score as roughly normal around the projection with a standard deviation you set as sigma (typical NBA full-game total sigma is 10–13 points; CBB full-game is often 12–16 given lower possession counts and higher variance per possession). Win probability for an Over/Under bet at line L is:

z = (chosen − L) / sigma, then P(Over) = normalCdf(z), P(Under) = 1 − normalCdf(z).

normalCdf is computed with a standard Zelen & Severo polynomial approximation of the cumulative normal distribution — accurate to about 7.5×10⁻⁸, more than sufficient for betting purposes.

6.4 Odds conversion, EV, and Kelly stake

American odds convert to decimal odds with americanToDecimal(a) = 1 + a/100 for positive odds, or 1 + 100/|a| for negative odds. From decimal odds d, define b = d − 1 (the net profit per 1 unit staked). Given your model probability p:

  • Expected value: EV = p×b − (1−p). EV is expressed in units per 1 unit staked — an EV of 0.05 means a 5% expected return on stake.
  • Kelly fraction: f* = max(0, (b×p − (1−p)) / b). Full Kelly is aggressive and assumes your probability estimate is exact; in practice you stake a fraction of it (commonly 1/4 to 1/2 Kelly) and cap the maximum bet size regardless of what Kelly recommends, since live-model probabilities carry real estimation error.

A practical staking rule many users apply on top of the raw math: stake = min(baseBet × fractionalKellyMultiplier, maxBetCap), where baseBet is your unit size setting and the cap prevents a single mis-estimated edge from ballooning stake size.

7. The scenario grid — stress-testing your inputs

Rather than trust one static regression/momentum setting, the engine runs three named scenarios through the exact same projection math, each nudging the regression and momentum weight inputs before recomputing:

ScenarioregAddmomAddInterpretation
Conservative+0.20−0.20Trusts pregame expectation more, discounts the recent hot/cold stretch
Base00Your exact configured settings
Aggressive−0.15+0.25Trusts the live rate and recent momentum more heavily

Each row's regAdd and momAdd are added to your base regression/momentum settings before being clamped back to [0, 1] and run through the entire pipeline in §3–§5. If all three scenarios agree on direction (Over or Under) and land on the same side of the live line, that agreement across very different assumptions is a much stronger signal than any single-scenario margin. If Conservative says Under but Aggressive says Over, your edge is fragile and highly dependent on how much you trust the recent run of scoring.

Reading scenario disagreement
When Conservative and Aggressive scenarios land on opposite sides of the live total, treat that as a "no consensus" situation. Either wait for more data (elapsed time increases confidence and narrows the scenario spread automatically) or reduce your stake size significantly relative to what the Base scenario's Kelly number suggests.

8. Worked example — NBA, mid third quarter

Inputs:

  • League: NBA, totalMinutes = 48, pregameTotal = 228.5, possTotal = 200
  • Elapsed t = 30 minutes, combined points pNow = 148
  • halfLife = 10, regression = 0.35, momWeight = 0.4, momWindow = 6
  • Marker 6 minutes ago (t=24): p = 120 (so 28 points scored in the last 6 minutes)
  • Live total line = 224.5, sigma = 11, book odds on Under = −110

Step 1 — baseline and observed rate:

baselinePPM = 228.5 / 48 = 4.760 ppm. Raw observed rate = 148 / 30 = 4.933 ppm. Cap check at t=30 (≥16 min bucket, 5.0× multiplier): cap = min(12, 4.760×5) = 12, so obs stays 4.933 (uncapped, well under ceiling).

Step 2 — confidence and regEff:

confidence = 1 − 0.5^(30/10) = 1 − 0.125 = 0.875. regEff = clamp(0.65×0.35 + 0.55×(1−0.875), 0, 1) = clamp(0.2275 + 0.06875, 0, 1) = 0.2963.

Step 3 — blended rate:

ppmBlend = (1 − 0.2963)×4.933 + 0.2963×4.760 = 0.7037×4.933 + 0.2963×4.760 = 3.4715 + 1.4106 = 4.882 ppm.

Step 4 — momentum:

ppmRecent = (148 − 120) / (30 − 24) = 28/6 = 4.667 ppm (well under the widened momentum band). momEff = clamp(0.4×(0.35 + 0.65×0.875), 0, 1) = 0.4×0.90875 = 0.3635. ppmAdj = (1 − 0.3635)×4.882 + 0.3635×4.667 = 0.6365×4.882 + 0.3635×4.667 = 3.107 + 1.696 = 4.803 ppm.

Step 5 — project to final:

Remaining minutes R = 48 − 30 = 18. Projection = 148 + 4.803×18 = 148 + 86.45 = 234.45.

Step 6 — margin, SR, probability, EV/Kelly:

margin = 234.45 − 224.5 = +9.95 → lean Over. SR = clamp((9.95/10)×(0.35 + 0.65×0.875)×100) = 0.995×0.90875×100 ≈ 90.4 — a very strong signal rating. For a symmetric check on the Under side (or for whichever side you actually hold), z = (234.45 − 224.5)/11 = 0.905, normalCdf(0.905) ≈ 0.8171 → P(Over) ≈ 81.7%, P(Under) ≈ 18.3%. If instead you were examining an Over bet at −110 (decimal 1.909), b = 0.909: EV = 0.817×0.909 − 0.183 = 0.7427 − 0.183 = +0.560 (56% EV — extremely high, which should itself prompt a reality check on your inputs, not blind confidence). Kelly f* = (0.909×0.817 − 0.183)/0.909 ≈ 0.617, i.e. full Kelly recommends 61.7% of bankroll — a number this large means you should heavily fraction it (e.g. 1/8 Kelly ≈ 7.7% of bankroll) and apply your stake cap.

9. Worked example — CBB, first half under 5:00

Inputs:

  • League: CBB, totalMinutes = 40, pregameTotal = 142.5, possTotal = 138
  • Elapsed t = 15 minutes, combined points pNow = 58, possessions used = 52
  • halfLife = 8, regression = 0.5, momWeight = 0.3, momWindow = 5
  • Marker at t=10: p = 39 (19 points in the last 5 minutes)
  • Live total line = 138.5, sigma = 14, projSource = BLEND with blendW = 0.5

PPM leg:

baselinePPM = 142.5/40 = 3.5625. obs = 58/15 = 3.867 (uncapped). confidence = 1−0.5^(15/8)=1−0.5^1.875 ≈ 1 − 0.2724 = 0.7276. regEff = clamp(0.65×0.5 + 0.55×(1−0.7276)) = 0.325 + 0.1498 = 0.4748. ppmBlend = 0.5252×3.867 + 0.4748×3.5625 = 2.0313 + 1.6917 = 3.723. ppmRecent = (58−39)/(15−10) = 19/5 = 3.80. momEff = 0.3×(0.35+0.65×0.7276) = 0.3×0.8229 = 0.2469. ppmAdj = 0.7531×3.723 + 0.2469×3.80 = 2.804 + 0.938 = 3.742. PPM projection = 58 + 3.742×(40−15) = 58 + 93.55 = 151.55.

PPP leg:

baselinePPP = 142.5/138 = 1.0326. obs = 58/52 = 1.1154 (under the 1.8 cap). regEff is the same 0.4748 (regression/confidence don’t depend on the method). pppAdj = 0.5252×1.1154 + 0.4748×1.0326 = 0.5858 + 0.4903 = 1.0761. Remaining possessions Prem = 138 − 52 = 86. PPP projection = 58 + 1.0761×86 = 58 + 92.54 = 150.54.

Blend (blendW = 0.5):

chosen = 0.5×151.55 + 0.5×150.54 = 151.05. Confidence blends the same way to 0.7276 (identical for both legs here). margin = 151.05 − 138.5 = +12.55 → strong Over lean. This large a margin this early (t=15 of 40, confidence only 73%) is exactly the situation where you should also check the scenario grid before trusting it — run Conservative (regAdd +0.20 → reg=0.7, momAdd −0.20 → momWeight=0.1) and Aggressive (reg=0.35, momWeight=0.55) and confirm both still lean Over before sizing a real bet.

10. Recommended default settings by league

SettingNBA defaultCBB defaultWhy
projSourceBLENDBLENDCross-checks pace vs. clock
blendW0.60.45CBB pace swings (fouling, stalling) matter more than raw clock
halfLife10 min7 minShorter games need faster confidence buildup
regression0.350.45CBB samples are smaller and noisier per team
momWeight0.350.3Keep recency real but bounded
momWindow6 min5 minScaled to shorter CBB halves
possTotal~200~138League-average pace anchors
sigma10–1312–16CBB totals vary more per possession

11. The halftime green-light gate

At halftime the engine switches to a dedicated structural gate designed specifically for Under bets, since halftime is the single cleanest decision point in a game: a full sample of one half plus a known market total to react to.

11.1 Structure-adjusted mean

Starting from the market total, the gate computes halftime pace htPace = (htPoints / elapsed) × fullGame (fullGame = 48 or 40), then adjusts the mean:

  • mean = marketTotal + clamp((htPace − marketTotal) × 0.15, −3, 3) — pace pulls the mean toward itself, capped to ±3 points of influence.
  • Forced-scoring risk: +1.0 if "possible", +2.5 if "active" (e.g. a team trailing big and forced to press/foul, inflating late scoring).
  • Variance spent: −0.8 if "med", −2.0 if "high" (blowout garbage-time, foul-outs, etc. that historically suppress the second half).

11.2 Pace label

paceDelta = htPace − marketTotal is labeled Slow (≤ −6), Slightly Slow (−6 to −2), Neutral (−2 to +2), Fast (+2 to +6), or Very Fast (> +6).

11.3 The four gate checks

  1. Pace OK: htPace ≤ marketTotal + 0.5 — the half wasn't played at a pace that already outruns the full-game total.
  2. Cushion OK: cushion = mean − clientUnder (your specific Under number) must be ≥ your configured cushGate (a minimum buffer, commonly 3–5 points).
  3. SR separation OK: Under signal rating must exceed Over signal rating by more than 12 points: srUnder + 12 < srOver is false is required (i.e. Under must dominate by >12 SR points) for the green light; note the code checks srUnder + 12 < srOver for the light itself — meaning Under's own SR must still beat Over's by that 12-point margin structurally.
  4. Forced-scoring risk veto: forcedRisk must be "none". Any possible or active forced-scoring risk automatically vetoes the green light regardless of every other check, producing "NO PLAY (FORCED SCORING RISK)".

Only when all four checks pass does the gate return "HT UNDER GREEN LIGHT". If SR favors Over by more than 12 (i.e. srOver + 12 < srUnder is false the other direction), it instead flags "CAUTION: OVER STRUCTURE". Otherwise it defaults to "NO PLAY (WAIT)" — the gate is deliberately conservative and biased toward inaction.

11.4 Bands and the late-game binary zone

The gate also reports a five-point normal band around the structure-adjusted mean: −2σ, −1σ, mean, +1σ, +2σ using your sigma setting, giving you a visual sense of how far the market total sits from the projected distribution. Past 36 minutes elapsed (NBA) it flags a "Binary zone" when forcedOK is true and the Over/Under SR gap is under 18 — signaling that late-game variance (fouling, garbage time) makes the outcome close to a coin flip regardless of model lean, and stake size should shrink accordingly.

The gate is an Under-focused tool
Notice the entire structure of §11 is written to validate Under bets specifically (cushion is measured against clientUnder, and the "green light" language is Under-only). This reflects the reality that halftime overs are harder to structurally validate — pace can accelerate for many reasons in the second half — whereas a well-rested favorite already up big with a slow pace and no forced-scoring risk is a genuinely identifiable Under structure.

12. When NOT to bet

  • Margin inside the ±1.0 no-bet band. The engine explicitly returns "No bet" here — don't override it because you have a hunch.
  • Scenario disagreement. If Conservative and Aggressive scenarios point different directions, treat the edge as unresolved.
  • Confidence below ~0.4 (roughly the first third of a halfLife-scaled window) combined with a large margin — a big edge this early is more likely noise than signal; wait for confidence to build before staking meaningfully.
  • Forced-scoring risk flagged as "possible" or "active." This is a hard veto in the halftime gate for a reason — desperate trailing teams foul, press, and shoot threes at an inflated and unpredictable rate.
  • EV that looks too good to be true (e.g. >25–30%). Extreme EV almost always means an input error — a stale line, wrong pregame total, or a possession count typo — not a genuine market inefficiency. Re-check inputs before trusting it.
  • Binary zone late in the fourth quarter/second half with a thin SR gap — the model itself is telling you the outcome is close to a coin flip.
  • No live line available or stale odds. Never compute EV/Kelly against a line that hasn't refreshed in the last few minutes of a fast-moving live market.

13. Logging results and improving your inputs over time

The model is only as good as its settings, and the settings are only as good as your record of how they performed. For every live bet, log at minimum:

  1. Game, league, elapsed time and score at bet placement
  2. Settings used (regression, momWeight, halfLife, blendW, possTotal, sigma)
  3. Projection, live line, margin, SR, and which scenario(s) you relied on
  4. Odds taken, decimal odds, EV%, Kelly% recommended, and stake actually placed
  5. Final score and whether the bet won, lost, or pushed

Periodically (every 25–50 logged bets is a reasonable batch size) compute your realized hit rate by SR bucket (e.g. 0–40, 40–70, 70–100) and by confidence bucket. If your 70–100 SR bucket isn't meaningfully outperforming your 0–40 bucket, your sigma or regression settings likely need recalibration — SR should be monotonically predictive of win rate if the underlying probability model is well-tuned. Track closing-line value too: compare your bet's line to the final live line at game end. Consistently beating the closing number is a much stronger long-run signal of edge than short-term win/loss record alone, given the variance inherent in single-game Over/Under outcomes.

Responsible gambling
This page describes a statistical modeling tool for educational purposes — it does not guarantee winning outcomes, and no staking formula (including Kelly) eliminates variance or the risk of loss. Bet only with money you can afford to lose, set strict bankroll limits before you start, avoid chasing losses, and treat any single-game EV or Kelly output as an estimate with real uncertainty, not a certainty. If gambling stops being enjoyable or starts affecting your finances, relationships, or well-being, contact a resource such as the National Council on Problem Gambling (1-800-522-4700) or your local equivalent.

Frequently asked questions

Why does the model ignore a hot shooting stretch?
Because three-point percentage has very little correlation from one stretch of a game to the next. The projection weights observed efficiency by sample size, so early shooting carries only a fraction of the weight until enough possessions have accumulated to justify more.
What is a rate cap and when does it apply?
A rate cap is a backstop that bounds the projected scoring rate above and below a matchup baseline. The allowed band widens continuously as elapsed time grows. In practice the cap rarely binds, because shrinkage handles most of the correction before the cap is reached.
Why are the college and NBA settings different?
College basketball is 40 minutes with a lower possession count and a lower efficiency baseline. Ceilings and priors are set as multiples of each league's own baseline rather than as fixed numbers, so an NBA-tuned parameter is never applied to a college game.