130 lines
4.0 KiB
JavaScript
130 lines
4.0 KiB
JavaScript
/**
|
|
* UnifiedOddsProvider — orchestrator.
|
|
*
|
|
* Fan-out to every adapter via Promise.allSettled, normalize, attach
|
|
* cross-source signals, run processing engines, return a structured
|
|
* `sources` array describing what contributed.
|
|
*
|
|
* Never throws. Adapter failures are surfaced per-source so the API caller
|
|
* can see exactly which upstreams contributed to a given refresh.
|
|
*/
|
|
|
|
const espn = require('./adapters/ESPNAdapter');
|
|
const pinnacle = require('./adapters/PinnacleAdapter');
|
|
const draftkings = require('./adapters/DraftKingsAdapter');
|
|
const fanduel = require('./adapters/FanDuelAdapter');
|
|
const betmgm = require('./adapters/BetMGMAdapter');
|
|
const caesars = require('./adapters/CaesarsAdapter');
|
|
const prizepicks = require('./adapters/PrizePicksAdapter');
|
|
const covers = require('./adapters/CoversAdapter');
|
|
const rotowire = require('./adapters/RotowireAdapter');
|
|
|
|
const lineShopping = require('./processing/LineShoppingEngine');
|
|
const middles = require('./processing/MiddlesDetector');
|
|
const ev = require('./processing/EVCalculator');
|
|
|
|
const { shouldCollect, isActiveSport } = require('../config/sports');
|
|
const rateLimiter = require('./rateLimiter');
|
|
const breaker = require('./circuitBreaker');
|
|
|
|
const ADAPTERS = [espn, pinnacle, draftkings, fanduel, betmgm, caesars, prizepicks, covers, rotowire];
|
|
|
|
function settleToResult(name, settled) {
|
|
if (settled.status === 'fulfilled') {
|
|
const value = settled.value;
|
|
const count = Array.isArray(value) ? value.length : (value?.projections?.length ?? 0);
|
|
return { source: name, ok: true, count };
|
|
}
|
|
const err = settled.reason;
|
|
return {
|
|
source: name,
|
|
ok: false,
|
|
error: err?.code === 'NOT_IMPLEMENTED' ? 'not_implemented'
|
|
: err?.code === 'BREAKER_OPEN' ? 'breaker_open'
|
|
: err?.code === 'RATE_LIMIT_TIMEOUT' ? 'rate_limited'
|
|
: (err?.message || 'unknown'),
|
|
};
|
|
}
|
|
|
|
async function fullRefresh(sport, { gradedProps = [] } = {}) {
|
|
if (!shouldCollect(sport)) {
|
|
return {
|
|
sport,
|
|
collected: false,
|
|
reason: 'sport not in collection set',
|
|
sources: [],
|
|
data: { games: [], props: [], shopped: [], middles: [] },
|
|
refreshed_at: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
const results = await Promise.allSettled([
|
|
espn.getGames(sport),
|
|
pinnacle.getGames(sport),
|
|
draftkings.getPlayerProps(sport),
|
|
fanduel.getPlayerProps(sport),
|
|
betmgm.getPlayerProps(sport),
|
|
caesars.getPlayerProps(sport),
|
|
prizepicks.getPlayerProps(sport),
|
|
covers.getConsensus?.(sport) ?? Promise.resolve([]),
|
|
rotowire.getProjections(sport),
|
|
]);
|
|
|
|
const sources = [
|
|
settleToResult('espn', results[0]),
|
|
settleToResult('pinnacle', results[1]),
|
|
settleToResult('draftkings', results[2]),
|
|
settleToResult('fanduel', results[3]),
|
|
settleToResult('betmgm', results[4]),
|
|
settleToResult('caesars', results[5]),
|
|
settleToResult('prizepicks', results[6]),
|
|
settleToResult('covers', results[7]),
|
|
settleToResult('rotowire', results[8]),
|
|
];
|
|
|
|
const games = results[0].status === 'fulfilled' ? results[0].value : [];
|
|
|
|
// Merge every adapter's player-prop payload (those that returned arrays).
|
|
const props = [];
|
|
for (const idx of [2, 3, 4, 5, 6]) {
|
|
if (results[idx].status === 'fulfilled' && Array.isArray(results[idx].value)) {
|
|
props.push(...results[idx].value);
|
|
}
|
|
}
|
|
|
|
const shopped = lineShopping.process(props);
|
|
const middlesFound = middles.detect(shopped);
|
|
|
|
// EV labels for any already-graded props the caller fed in.
|
|
const evRows = (gradedProps || []).map((g) => ({
|
|
key: g.key,
|
|
grade: g.grade,
|
|
odds: g.odds,
|
|
ev: ev.calculate({ grade: g.grade, odds: g.odds }),
|
|
}));
|
|
|
|
return {
|
|
sport,
|
|
active: isActiveSport(sport),
|
|
sources,
|
|
data: {
|
|
games,
|
|
props,
|
|
shopped,
|
|
middles: middlesFound,
|
|
ev: evRows,
|
|
},
|
|
refreshed_at: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
function status() {
|
|
return {
|
|
rate_limiters: rateLimiter.snapshot(),
|
|
breakers: breaker.snapshot(),
|
|
adapters: ADAPTERS.map((a) => a.name),
|
|
};
|
|
}
|
|
|
|
module.exports = { fullRefresh, status };
|