Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user