Wave 2A: offseason data feeds — news wire + quota-disciplined futures
FREE ESPN news wire + championship-winner futures for the never-dark
offseason hub. Both graceful/empty, never fabricate a market value.
- newsService (mirrors injuryService): per-sport ESPN /news FEEDS, pure
parseNews → { sport, items:[{id,headline,description,published,type,
athlete?{name,key},team?,href}] }; athlete/team from categories[] only
(absent when not present). Cache 15m, injectable, offline-tested.
- oddsNormalizer.normalizeOutrights: NEW branch — outrights outcomes are
{name,price} with no point, so normalizeProps drops them; keeps them with
best-price-across-allowed-books per selection. + americanToDecimal.
- oddsService.FUTURES_KEYS: separate map (mlb/nba/wnba championship winner),
OUT of the daily SPORT_KEYS/snapshot budget.
- futuresService: getFutures(sport,deps) → { sport, updated_at, markets:
[{key,title,selections:[{name,price,prevPrice?,move?}]}] }. One outrights
call per 12h TTL (quota-disciplined), FUTURES_ENABLED gate. Price-move
(shortening/drifting/flat) mirrors computeLineDeltas SHAPE on odds not
line; prev prices persisted inside the futures:{sport} value (no new key).
linkNewsToMoves pure causal-tie helper.
- Routes /api/news/:sport + /api/futures/:sport (registered) + Next proxies.
- Tests: newsService, futuresService, oddsNormalizerOutrights (fail→pass,
no network). Full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,69 @@ function normalizeProps(eventsWithOdds) {
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* american → decimal payout (for "best price" selection across books).
|
||||
* +150 → 2.5, -200 → 1.5. Returns null for a non-integer/absent price.
|
||||
*/
|
||||
function americanToDecimal(price) {
|
||||
if (price == null || typeof price !== 'number' || !Number.isFinite(price) || price === 0) return null;
|
||||
return price > 0 ? 1 + price / 100 : 1 + 100 / Math.abs(price);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wave 2A — outrights/futures normalizer. NEW branch: outrights outcomes are
|
||||
* `{ name, price }` with NO `point` and NO Over/Under grouping, so
|
||||
* `normalizeProps` (which requires `outcome.point` + pairs Over/Under) would
|
||||
* DROP every one of them. This keeps them.
|
||||
*
|
||||
* Input: odds-api `/sports/{key}/odds?markets=outrights` events. Each event is
|
||||
* a single futures market (e.g. "MLB World Series Winner"). Output: one market
|
||||
* per event with the BEST available american price per selection across the
|
||||
* ALLOWED books (best = highest decimal payout — honest "best price you can
|
||||
* get"), plus the book that posted it for provenance.
|
||||
*
|
||||
* [{ key: sport_key, title: sport_title, selections:
|
||||
* [{ name, price (american int), book }] }]
|
||||
*
|
||||
* Never fabricates: an event with no outrights market / no priced outcomes
|
||||
* yields no selections; a book not on the ALLOWED list is skipped.
|
||||
*/
|
||||
function normalizeOutrights(events) {
|
||||
const markets = [];
|
||||
for (const event of Array.isArray(events) ? events : []) {
|
||||
const key = event.sport_key || event.id;
|
||||
if (!key) continue;
|
||||
const title = event.sport_title || key;
|
||||
|
||||
// best[name] = { price, book, dec }
|
||||
const best = {};
|
||||
for (const bookmaker of Array.isArray(event.bookmakers) ? event.bookmakers : []) {
|
||||
if (!ALLOWED_BOOKS.has(bookmaker.key)) continue;
|
||||
for (const market of Array.isArray(bookmaker.markets) ? bookmaker.markets : []) {
|
||||
if (market.key !== 'outrights') continue;
|
||||
for (const outcome of market.outcomes || []) {
|
||||
if (!outcome || !outcome.name || outcome.price == null) continue;
|
||||
const dec = americanToDecimal(outcome.price);
|
||||
if (dec == null) continue;
|
||||
const cur = best[outcome.name];
|
||||
if (!cur || dec > cur.dec) {
|
||||
best[outcome.name] = { price: outcome.price, book: bookmaker.key, dec };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selections = Object.keys(best).map((name) => ({
|
||||
name,
|
||||
price: best[name].price,
|
||||
book: best[name].book,
|
||||
}));
|
||||
if (selections.length === 0) continue;
|
||||
markets.push({ key: String(key), title: String(title), selections });
|
||||
}
|
||||
return markets;
|
||||
}
|
||||
|
||||
function extractSpreads(eventsWithOdds) {
|
||||
const spreads = [];
|
||||
|
||||
@@ -197,4 +260,4 @@ function extractSpreads(eventsWithOdds) {
|
||||
return spreads;
|
||||
}
|
||||
|
||||
module.exports = { normalizeProps, extractSpreads, MARKET_MAP, ALLOWED_BOOKS };
|
||||
module.exports = { normalizeProps, normalizeOutrights, americanToDecimal, extractSpreads, MARKET_MAP, ALLOWED_BOOKS };
|
||||
|
||||
Reference in New Issue
Block a user