Files
vyndr/src/services/altLineScanner.js
T

111 lines
3.4 KiB
JavaScript

/**
* 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,
};