/** * Normal CDF using rational approximation (Abramowitz & Stegun). */ function normalCDF(x, mean = 0, stddev = 1) { if (stddev <= 0) return x >= mean ? 1 : 0; const z = (x - mean) / stddev; const t = 1 / (1 + 0.2316419 * Math.abs(z)); const d = 0.3989422804014327; // 1/sqrt(2*pi) const p = d * Math.exp(-z * z / 2) * (t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.8212560 + t * 1.3302744))))); return z > 0 ? 1 - p : p; } /** * Calculate model probability for a prop line using normal distribution. * @param {number} mean - Projected mean * @param {number} stddev - Standard deviation * @param {number} line - The prop line * @param {string} direction - 'over' or 'under' * @returns {number} Probability 0-1 */ function calculateModelProbability(mean, stddev, line, direction) { if (stddev <= 0) { if (direction === 'over') return mean > line ? 1 : 0; return mean < line ? 1 : 0; } const cdf = normalCDF(line, mean, stddev); return direction === 'over' ? 1 - cdf : cdf; } /** * Convert American odds to implied probability. * @param {number} odds - American odds (e.g. -110, +150) * @returns {number} Implied probability 0-1 */ function americanToImplied(odds) { if (odds < 0) return Math.abs(odds) / (Math.abs(odds) + 100); return 100 / (odds + 100); } /** * Compare model probability to book implied probability. * * EDGE IS RETIRED AS A DECISION (2026-08-01). `value_detected: edge > 0` used to * declare that a line had value. It cannot: measured on n=200 settled MLB rows, * corr(edge, outcome) = -0.010 under the incumbent ruler and -0.022 under the * consensus ruler, while corr(p_win, outcome) = +0.26. A quantity that does not * predict the outcome must not decide anything the user sees. * * `edge` is STILL COMPUTED AND RETURNED — losing the record would be worse than * mis-using it, and it stays in the ledger as a diagnostic. What is gone is the * verdict derived from it. `value_detected` is now null with an explicit reason, * so a caller that reads it gets an honest absence instead of a false boolean. * * @returns {object} { model_prob, book_implied, edge, value_detected, value_basis } */ function compareToBookImplied(modelProb, bookOdds) { const bookImplied = americanToImplied(bookOdds); const edge = modelProb - bookImplied; return { model_prob: Math.round(modelProb * 1000) / 1000, book_implied: Math.round(bookImplied * 1000) / 1000, // DIAGNOSTIC ONLY — never a ranking, gate or quality signal. edge: Math.round(edge * 1000) / 1000, value_detected: null, value_basis: 'retired:edge_does_not_predict', }; } /** * Rank the rungs of an alt-line ladder by the model-vs-price gap. * * ⚠️ THIS MODULE HAS NO CALLERS (verified 2026-08-01) — it is unwired, like * mlbGrader.js. Left in place, made honest, not deleted. * * EDGE IS NO LONGER A VERDICT HERE. This used to `filter(e => e.value_detected)` * and call the survivor `optimal_line`. Both were quality claims that edge * cannot support (n=200 settled MLB: corr(edge, outcome) = -0.010 / -0.022). * * A HONEST NOTE ON WHY THIS ONE IS DIFFERENT. Ranking props AGAINST EACH OTHER * must not use edge — p_win is the measured predictor. But choosing between * RUNGS OF THE SAME PROP is inherently price-relative: every rung has a * different price, and ranking rungs by model probability alone would always * pick the lowest line (P(over 0.5) > P(over 2.5) by construction). So the gap * is kept as the ordering key here — and labelled as an UNVALIDATED price * diagnostic, because we have no evidence it predicts rung outcomes either. * * @returns {object|null} { ranked_lines, ranking_basis, top_by_price_gap, ... } */ function scanAltLines(prop, oddsData) { if (!prop || !oddsData || oddsData.length === 0) return null; const { projected_mean, projected_stddev } = prop; const direction = prop.direction || 'over'; const evaluated = oddsData.map(alt => { const modelProb = calculateModelProbability(projected_mean, projected_stddev, alt.line, direction); const comparison = compareToBookImplied(modelProb, alt.odds); return { line: alt.line, odds: alt.odds, book: alt.book, model_probability: comparison.model_prob, book_implied: comparison.book_implied, edge: comparison.edge, }; }); if (evaluated.length === 0) return null; // No value FILTER: a negative gap is a real observation about a rung, not a // reason to hide it. The whole ladder is returned, ranked, and labelled. const ranked = [...evaluated].sort((a, b) => b.edge - a.edge); const top = ranked[0]; return { ranking_basis: 'price_gap_diagnostic_unvalidated', top_by_price_gap: top.line, odds: top.odds, book: top.book, model_probability: top.model_probability, book_implied: top.book_implied, edge: top.edge, ranked_lines: ranked, }; } module.exports = { scanAltLines, calculateModelProbability, compareToBookImplied, normalCDF, americanToImplied, };