Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
const SHARP_BOOKS = ['pinnacle', 'circa', 'bookmaker'];
|
||||
const SQUARE_BOOKS = ['draftkings', 'fanduel', 'betmgm', 'caesars', 'fanatics', 'bet365'];
|
||||
|
||||
/**
|
||||
* Detect discrepancy between sharp and square book consensus lines.
|
||||
* @param {Array<{book: string, line: number}>} propLines
|
||||
* @returns {object} { discrepancy, gap, sharp_consensus, square_consensus }
|
||||
*/
|
||||
function detectDiscrepancy(propLines) {
|
||||
if (!propLines || propLines.length === 0) {
|
||||
return { discrepancy: false, gap: 0, sharp_consensus: null, square_consensus: null };
|
||||
}
|
||||
|
||||
const sharpLines = propLines.filter(p => SHARP_BOOKS.includes(p.book.toLowerCase()));
|
||||
const squareLines = propLines.filter(p => SQUARE_BOOKS.includes(p.book.toLowerCase()));
|
||||
|
||||
if (sharpLines.length === 0 || squareLines.length === 0) {
|
||||
return { discrepancy: false, gap: 0, sharp_consensus: null, square_consensus: null };
|
||||
}
|
||||
|
||||
const sharpConsensus = sharpLines.reduce((s, p) => s + p.line, 0) / sharpLines.length;
|
||||
const squareConsensus = squareLines.reduce((s, p) => s + p.line, 0) / squareLines.length;
|
||||
const gap = Math.abs(sharpConsensus - squareConsensus);
|
||||
|
||||
return {
|
||||
discrepancy: gap > 0.5,
|
||||
gap: Math.round(gap * 100) / 100,
|
||||
sharp_consensus: Math.round(sharpConsensus * 100) / 100,
|
||||
square_consensus: Math.round(squareConsensus * 100) / 100,
|
||||
sharp_books_used: sharpLines.length,
|
||||
square_books_used: squareLines.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect steam move: 0.5+ movement at 3+ books within 10 minutes.
|
||||
* @param {Array<{book: string, line: number, timestamp: string}>} movements
|
||||
* @returns {object} { steam_move, books_moved, magnitude, window_minutes }
|
||||
*/
|
||||
function detectSteamMove(movements) {
|
||||
if (!movements || movements.length < 3) {
|
||||
return { steam_move: false, books_moved: 0, magnitude: 0, window_minutes: 0 };
|
||||
}
|
||||
|
||||
const sorted = [...movements].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||
const windowMs = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const windowStart = new Date(sorted[i].timestamp).getTime();
|
||||
const windowEnd = windowStart + windowMs;
|
||||
|
||||
const inWindow = sorted.filter(m => {
|
||||
const t = new Date(m.timestamp).getTime();
|
||||
return t >= windowStart && t <= windowEnd;
|
||||
});
|
||||
|
||||
// Group by book, find those with 0.5+ movement
|
||||
const bookMovements = {};
|
||||
for (const m of inWindow) {
|
||||
if (!bookMovements[m.book]) bookMovements[m.book] = [];
|
||||
bookMovements[m.book].push(m.line);
|
||||
}
|
||||
|
||||
const significantMoves = Object.entries(bookMovements).filter(([_, lines]) => {
|
||||
if (lines.length < 2) return false;
|
||||
const range = Math.max(...lines) - Math.min(...lines);
|
||||
return range >= 0.5;
|
||||
});
|
||||
|
||||
// Also count books that appear with already-moved lines (single entry with magnitude info)
|
||||
const booksWithMovement = inWindow.filter(m => Math.abs(m.line) >= 0.5);
|
||||
const uniqueBooks = new Set(booksWithMovement.map(m => m.book));
|
||||
|
||||
if (uniqueBooks.size >= 3 || significantMoves.length >= 3) {
|
||||
const allMagnitudes = booksWithMovement.map(m => Math.abs(m.line));
|
||||
return {
|
||||
steam_move: true,
|
||||
books_moved: Math.max(uniqueBooks.size, significantMoves.length),
|
||||
magnitude: Math.round((allMagnitudes.reduce((s, v) => s + v, 0) / allMagnitudes.length) * 100) / 100,
|
||||
window_minutes: 10,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { steam_move: false, books_moved: 0, magnitude: 0, window_minutes: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reliability score for a prop type in a sport from historical accuracy.
|
||||
* @param {string} propType
|
||||
* @param {string} sport
|
||||
* @returns {number} Reliability score 0-1
|
||||
*/
|
||||
function getReliabilityScore(propType, sport) {
|
||||
const reliabilityMap = {
|
||||
nba: {
|
||||
points: 0.72,
|
||||
rebounds: 0.65,
|
||||
assists: 0.68,
|
||||
threes: 0.60,
|
||||
steals: 0.45,
|
||||
blocks: 0.42,
|
||||
pts_rebs_asts: 0.70,
|
||||
},
|
||||
mlb: {
|
||||
hits: 0.55,
|
||||
home_runs: 0.40,
|
||||
rbis: 0.48,
|
||||
stolen_bases: 0.52,
|
||||
strikeouts_pitcher: 0.65,
|
||||
earned_runs: 0.58,
|
||||
total_bases: 0.53,
|
||||
},
|
||||
};
|
||||
|
||||
const sportMap = reliabilityMap[sport.toLowerCase()];
|
||||
if (!sportMap) return 0.50; // default
|
||||
return sportMap[propType.toLowerCase()] || 0.50;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SHARP_BOOKS,
|
||||
SQUARE_BOOKS,
|
||||
detectDiscrepancy,
|
||||
detectSteamMove,
|
||||
getReliabilityScore,
|
||||
};
|
||||
Reference in New Issue
Block a user