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 isp + 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 min | 2.00× | 1.25× |
| 4 min | 2.25× | 1.30× |
| 8 min | 2.86× | 1.84× (ceiling binds) |
| 12 min | 3.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.
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:
| Scenario | regAdd | momAdd | Interpretation |
|---|---|---|---|
| Conservative | +0.20 | −0.20 | Trusts pregame expectation more, discounts the recent hot/cold stretch |
| Base | 0 | 0 | Your exact configured settings |
| Aggressive | −0.15 | +0.25 | Trusts 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.
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
| Setting | NBA default | CBB default | Why |
|---|---|---|---|
| projSource | BLEND | BLEND | Cross-checks pace vs. clock |
| blendW | 0.6 | 0.45 | CBB pace swings (fouling, stalling) matter more than raw clock |
| halfLife | 10 min | 7 min | Shorter games need faster confidence buildup |
| regression | 0.35 | 0.45 | CBB samples are smaller and noisier per team |
| momWeight | 0.35 | 0.3 | Keep recency real but bounded |
| momWindow | 6 min | 5 min | Scaled to shorter CBB halves |
| possTotal | ~200 | ~138 | League-average pace anchors |
| sigma | 10–13 | 12–16 | CBB 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
- Pace OK: htPace ≤ marketTotal + 0.5 — the half wasn't played at a pace that already outruns the full-game total.
- Cushion OK: cushion = mean − clientUnder (your specific Under number) must be ≥ your configured cushGate (a minimum buffer, commonly 3–5 points).
- SR separation OK: Under signal rating must exceed Over signal rating by more than 12 points:
srUnder + 12 < srOveris false is required (i.e. Under must dominate by >12 SR points) for the green light; note the code checkssrUnder + 12 < srOverfor the light itself — meaning Under's own SR must still beat Over's by that 12-point margin structurally. - 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.
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:
- Game, league, elapsed time and score at bet placement
- Settings used (regression, momWeight, halfLife, blendW, possTotal, sigma)
- Projection, live line, margin, SR, and which scenario(s) you relied on
- Odds taken, decimal odds, EV%, Kelly% recommended, and stake actually placed
- 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.