58 lines
2.0 KiB
JavaScript
58 lines
2.0 KiB
JavaScript
/**
|
|
* Steam detection — flags lines that move 1+ points in <2 hours.
|
|
*
|
|
* Inputs: a stream of { prop_key, book, line, odds, recorded_at } samples.
|
|
* The orchestrator persists samples to `line_history` and calls check() with
|
|
* the rolling window for tonight's slate.
|
|
*/
|
|
|
|
const TWO_HOURS_MS = 2 * 60 * 60_000;
|
|
const STEAM_THRESHOLD = 1;
|
|
|
|
/**
|
|
* @param {Array<{prop_key:string, book:string, line:number, odds:number|null, recorded_at:string|number}>} samples
|
|
* @returns {Array<{prop_key:string, book:string, from_line:number, to_line:number, delta:number, duration_ms:number, started_at:string, ended_at:string}>}
|
|
*/
|
|
function check(samples) {
|
|
if (!Array.isArray(samples) || samples.length === 0) return [];
|
|
|
|
// Group samples by prop_key + book and sort chronologically.
|
|
const buckets = new Map();
|
|
for (const s of samples) {
|
|
const k = `${s.prop_key}|${s.book}`;
|
|
if (!buckets.has(k)) buckets.set(k, []);
|
|
buckets.get(k).push({ ...s, t: new Date(s.recorded_at).getTime() });
|
|
}
|
|
|
|
const flags = [];
|
|
for (const [key, rows] of buckets.entries()) {
|
|
rows.sort((a, b) => a.t - b.t);
|
|
for (let i = 0; i < rows.length; i++) {
|
|
// Walk forward in time and stop as soon as the gap > window.
|
|
const start = rows[i];
|
|
for (let j = i + 1; j < rows.length; j++) {
|
|
const end = rows[j];
|
|
if (end.t - start.t > TWO_HOURS_MS) break;
|
|
const delta = end.line - start.line;
|
|
if (Math.abs(delta) >= STEAM_THRESHOLD) {
|
|
const [propKey, book] = key.split('|');
|
|
flags.push({
|
|
prop_key: propKey,
|
|
book,
|
|
from_line: start.line,
|
|
to_line: end.line,
|
|
delta,
|
|
duration_ms: end.t - start.t,
|
|
started_at: new Date(start.t).toISOString(),
|
|
ended_at: new Date(end.t).toISOString(),
|
|
});
|
|
break; // one flag per starting sample
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return flags;
|
|
}
|
|
|
|
module.exports = { check, TWO_HOURS_MS, STEAM_THRESHOLD };
|