'use strict'; /** * Book comparison (Session 28). * * Surfaces the same prop across every available sportsbook with the best * line highlighted. Data source is the grouped odds-api prop shape, which * already carries book-by-book lines: * { player, stat_type, lines: [{ book, line, over_odds, under_odds }] } * * "Best line" = highest decimal payout for the selected side. Savings is * the per-$100 payout edge of the best book over the field average — the * concrete dollars a user leaves on the table by not line-shopping. * * Zero credits: it only reads odds already fetched/cached. */ const { __internals } = require('./parlayService'); const { americanToDecimal } = __internals; function average(nums) { const valid = nums.filter((n) => Number.isFinite(n)); if (valid.length === 0) return 0; return valid.reduce((a, b) => a + b, 0) / valid.length; } function oddsForSide(line, side) { if (!line) return null; const raw = side === 'under' ? line.under_odds : line.over_odds; return raw == null ? null : Number(raw); } /** * Compare one grouped prop across its books for a given side. * Returns null when there are no usable book lines. */ function compareProp(prop, side = 'over') { const books = (prop?.lines || prop?.books || []).filter((b) => b && b.book); const priced = books.filter((b) => Number.isFinite(oddsForSide(b, side))); if (priced.length === 0) return null; let best = priced[0]; for (const b of priced) { if (americanToDecimal(oddsForSide(b, side)) > americanToDecimal(oddsForSide(best, side))) best = b; } const bestDecimal = americanToDecimal(oddsForSide(best, side)); const avgDecimal = average(priced.map((b) => americanToDecimal(oddsForSide(b, side)))); const savings = Math.round((bestDecimal - avgDecimal) * 100 * 100) / 100; // per $100, 2dp return { player: prop.player, stat: prop.stat_type || prop.stat, line: best.line ?? prop.line ?? null, side, books: priced.map((b) => ({ book: b.book, line: b.line ?? null, over_odds: b.over_odds ?? null, under_odds: b.under_odds ?? null, isBest: b.book === best.book, })), bestBook: best.book, bestOdds: oddsForSide(best, side), bookCount: priced.length, savings, }; } /** * Best lines across a list of props, sorted by savings desc (where line- * shopping matters most). Drops props with a single book (nothing to * compare). `limit` caps the result. */ function bestLines(props, { side = 'over', limit = 20 } = {}) { if (!Array.isArray(props)) return []; const out = []; for (const prop of props) { const cmp = compareProp(prop, side); if (cmp && cmp.bookCount >= 2) out.push(cmp); } out.sort((a, b) => b.savings - a.savings); return limit > 0 ? out.slice(0, limit) : out; } module.exports = { compareProp, bestLines, __internals: { average, oddsForSide }, };