Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* ParlayAPI historical pull.
|
||||
*
|
||||
* BULK operation — runs for hours, not seconds. Free tier is 1,000 credits
|
||||
* per month, so we walk one (sport, date) at a time with a checkpoint in
|
||||
* Redis so an interrupted run can resume right where it stopped.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/pull-parlayapi-history.js nba 2025 # one sport / season
|
||||
* node scripts/pull-parlayapi-history.js all # all active sports
|
||||
* node scripts/pull-parlayapi-history.js --resume # continue from checkpoint
|
||||
* node scripts/pull-parlayapi-history.js --dry-run # don't insert
|
||||
* node scripts/pull-parlayapi-history.js --yes # skip confirmation
|
||||
*
|
||||
* Pace: 2 requests / minute. Set PARLAYAPI_PULL_RATE in env to override.
|
||||
*/
|
||||
|
||||
if (require.main !== module) {
|
||||
throw new Error('Run directly: node scripts/pull-parlayapi-history.js');
|
||||
}
|
||||
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const { getSupabaseServiceClient } = require('../src/utils/supabase');
|
||||
const { getRedisClient } = require('../src/utils/redis');
|
||||
const parlayApi = require('../src/services/adapters/parlayApiAdapter');
|
||||
const { getActiveSports } = require('../src/config/sports');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const resume = args.includes('--resume');
|
||||
const skipConfirm = args.includes('--yes');
|
||||
const positional = args.filter((a) => !a.startsWith('--'));
|
||||
const sportArg = positional[0];
|
||||
const seasonArg = positional[1];
|
||||
|
||||
const RATE_MS = Number(process.env.PARLAYAPI_PULL_RATE_MS) || 30_000; // 2/min by default
|
||||
const SEASON_DEFAULT = new Date().getFullYear();
|
||||
|
||||
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
||||
|
||||
async function confirm(question) {
|
||||
if (skipConfirm) return true;
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const answer = await new Promise((r) => rl.question(question, r));
|
||||
rl.close();
|
||||
return /^y(es)?$/i.test(answer.trim());
|
||||
}
|
||||
|
||||
function* dateRange(season) {
|
||||
// Walk a season's worth of days. ParlayAPI's date arg is YYYY-MM-DD.
|
||||
// For each season we cover Aug 1 → Jul 31 of the following calendar
|
||||
// year — that umbrella catches every sport's annual span.
|
||||
const start = new Date(Date.UTC(season, 7, 1));
|
||||
const end = new Date(Date.UTC(season + 1, 6, 31));
|
||||
const cursor = new Date(start);
|
||||
while (cursor <= end) {
|
||||
yield cursor.toISOString().slice(0, 10);
|
||||
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkpointKey(sport, season) {
|
||||
return `parlayapi:checkpoint:${sport}:${season}`;
|
||||
}
|
||||
|
||||
async function readCheckpoint(sport, season) {
|
||||
const redis = getRedisClient();
|
||||
return redis.get(await checkpointKey(sport, season));
|
||||
}
|
||||
|
||||
async function writeCheckpoint(sport, season, date) {
|
||||
const redis = getRedisClient();
|
||||
await redis.set(await checkpointKey(sport, season), date, 'EX', 30 * 24 * 60 * 60);
|
||||
}
|
||||
|
||||
async function processSportSeason(sport, season) {
|
||||
console.log(`[parlayapi-pull] ${sport} ${season} starting (rate=${RATE_MS}ms)`);
|
||||
let totalInserted = 0;
|
||||
let lastSeen = null;
|
||||
if (resume) {
|
||||
lastSeen = await readCheckpoint(sport, season);
|
||||
if (lastSeen) console.log(`[parlayapi-pull] resuming after ${lastSeen}`);
|
||||
}
|
||||
|
||||
for (const date of dateRange(season)) {
|
||||
if (lastSeen && date <= lastSeen) continue;
|
||||
let lines;
|
||||
try {
|
||||
lines = await parlayApi.getClosingLines(sport, date);
|
||||
} catch (err) {
|
||||
console.warn(`[parlayapi-pull] ${date} failed: ${err.message}`);
|
||||
await sleep(RATE_MS);
|
||||
continue;
|
||||
}
|
||||
if (lines.length && !dryRun) {
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const rows = lines.map((l) => ({
|
||||
sport,
|
||||
game_date: date,
|
||||
player_name: l.player ?? l.player_name ?? 'unknown',
|
||||
stat_type: l.stat_type ?? l.market ?? 'unknown',
|
||||
line: Number(l.line ?? l.point ?? 0),
|
||||
closing_line: Number(l.closing_line ?? l.close ?? null) || null,
|
||||
result: l.result ?? l.outcome ?? null,
|
||||
source: 'parlayapi',
|
||||
}));
|
||||
const { error } = await supabase.from('historical_props').insert(rows);
|
||||
if (error) {
|
||||
console.warn(`[parlayapi-pull] insert failed for ${date}: ${error.message}`);
|
||||
} else {
|
||||
totalInserted += rows.length;
|
||||
}
|
||||
}
|
||||
await writeCheckpoint(sport, season, date);
|
||||
if (dryRun) {
|
||||
console.log(`[parlayapi-pull] dry-run ${date} → ${lines.length} lines`);
|
||||
}
|
||||
await sleep(RATE_MS);
|
||||
}
|
||||
console.log(`[parlayapi-pull] ${sport} ${season} done — inserted ${totalInserted} rows`);
|
||||
return totalInserted;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!parlayApi.configured()) {
|
||||
console.error('PARLAYAPI_KEY is not set');
|
||||
process.exit(2);
|
||||
}
|
||||
const sportList = sportArg && sportArg !== 'all'
|
||||
? [sportArg]
|
||||
: getActiveSports().map((s) => s.key);
|
||||
const season = Number(seasonArg) || SEASON_DEFAULT;
|
||||
|
||||
if (!dryRun) {
|
||||
const ok = await confirm(
|
||||
`This will insert ParlayAPI history for ${sportList.join(', ')} ${season} into ${process.env.SUPABASE_URL || '(unknown)'}. Continue? (y/n) `
|
||||
);
|
||||
if (!ok) { console.log('aborted'); process.exit(0); }
|
||||
}
|
||||
|
||||
for (const sport of sportList) {
|
||||
try { await processSportSeason(sport, season); }
|
||||
catch (err) { console.error(`[parlayapi-pull] ${sport} fatal: ${err.message}`); }
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[parlayapi-pull] fatal:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user