Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Line shopping — for each unique prop (game/player/stat), find the best
|
||||
* line per side across books.
|
||||
*
|
||||
* Best Over = lowest line + best odds at that line.
|
||||
* Best Under = highest line + best odds at that line.
|
||||
*
|
||||
* We also flag "outlier" books — a book that's 1+ points off the median.
|
||||
*/
|
||||
|
||||
function propKey(p) {
|
||||
return `${p.game_id}|${p.player_id ?? p.player_name}|${p.stat_type}`;
|
||||
}
|
||||
|
||||
function americanToImplied(odds) {
|
||||
if (typeof odds !== 'number' || !Number.isFinite(odds)) return null;
|
||||
return odds > 0 ? 100 / (odds + 100) : -odds / (-odds + 100);
|
||||
}
|
||||
|
||||
function median(values) {
|
||||
if (!values.length) return null;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
||||
}
|
||||
|
||||
function process(props) {
|
||||
const grouped = new Map();
|
||||
for (const p of props || []) {
|
||||
const key = propKey(p);
|
||||
if (!grouped.has(key)) grouped.set(key, []);
|
||||
grouped.get(key).push(p);
|
||||
}
|
||||
|
||||
const out = [];
|
||||
for (const [key, rows] of grouped.entries()) {
|
||||
if (rows.length === 1) {
|
||||
out.push({ ...rows[0], best_over: rows[0], best_under: rows[0], line_outliers: [] });
|
||||
continue;
|
||||
}
|
||||
const lines = rows.map((r) => r.line).filter((n) => typeof n === 'number');
|
||||
const med = median(lines);
|
||||
|
||||
const overs = rows.filter((r) => r.odds_over != null);
|
||||
const unders = rows.filter((r) => r.odds_under != null);
|
||||
|
||||
// Best Over = lowest line, then best (highest implied prob) odds at that line.
|
||||
let bestOver = null;
|
||||
for (const r of overs) {
|
||||
if (!bestOver) { bestOver = r; continue; }
|
||||
if (r.line < bestOver.line) bestOver = r;
|
||||
else if (r.line === bestOver.line) {
|
||||
const a = americanToImplied(r.odds_over);
|
||||
const b = americanToImplied(bestOver.odds_over);
|
||||
if (a != null && b != null && a < b) bestOver = r;
|
||||
}
|
||||
}
|
||||
let bestUnder = null;
|
||||
for (const r of unders) {
|
||||
if (!bestUnder) { bestUnder = r; continue; }
|
||||
if (r.line > bestUnder.line) bestUnder = r;
|
||||
else if (r.line === bestUnder.line) {
|
||||
const a = americanToImplied(r.odds_under);
|
||||
const b = americanToImplied(bestUnder.odds_under);
|
||||
if (a != null && b != null && a < b) bestUnder = r;
|
||||
}
|
||||
}
|
||||
|
||||
const outliers = (med != null)
|
||||
? rows.filter((r) => Math.abs(r.line - med) >= 1).map((r) => ({ book: r.book, line: r.line, delta: r.line - med }))
|
||||
: [];
|
||||
|
||||
out.push({
|
||||
key,
|
||||
median_line: med,
|
||||
books: rows,
|
||||
best_over: bestOver,
|
||||
best_under: bestUnder,
|
||||
line_outliers: outliers,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = { process, americanToImplied };
|
||||
Reference in New Issue
Block a user