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