Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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.
|
||||
* @param {number} modelProb - Model-calculated probability
|
||||
* @param {number} bookOdds - American odds from the book
|
||||
* @returns {object} { model_prob, book_implied, edge, value_detected }
|
||||
*/
|
||||
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,
|
||||
edge: Math.round(edge * 1000) / 1000,
|
||||
value_detected: edge > 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan alternate lines for A-grade props to find optimal value.
|
||||
* @param {object} prop - { player, stat, projected_mean, projected_stddev, grade }
|
||||
* @param {Array} oddsData - Array of { line, odds, book } from alt markets
|
||||
* @returns {object|null} Best alt line with edge, or null
|
||||
*/
|
||||
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,
|
||||
value_detected: comparison.value_detected,
|
||||
};
|
||||
});
|
||||
|
||||
const withValue = evaluated.filter(e => e.value_detected);
|
||||
if (withValue.length === 0) return null;
|
||||
|
||||
withValue.sort((a, b) => b.edge - a.edge);
|
||||
const optimal = withValue[0];
|
||||
|
||||
return {
|
||||
optimal_line: optimal.line,
|
||||
odds: optimal.odds,
|
||||
book: optimal.book,
|
||||
model_probability: optimal.model_probability,
|
||||
book_implied: optimal.book_implied,
|
||||
edge: optimal.edge,
|
||||
all_value_lines: withValue,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scanAltLines,
|
||||
calculateModelProbability,
|
||||
compareToBookImplied,
|
||||
normalCDF,
|
||||
americanToImplied,
|
||||
};
|
||||
Reference in New Issue
Block a user