| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- const mongoose = require('mongoose');
- const { Schema } = mongoose;
- const TRACKED_IOR_KEYS = [
- 'ior_mn',
- 'ior_wmh_1',
- 'ior_wmh_2',
- 'ior_wmh_3',
- 'ior_wmc_1',
- 'ior_wmc_2',
- 'ior_wmc_3',
- 'ior_ot_1',
- 'ior_ot_2',
- 'ior_ot_3',
- 'ior_ot_4',
- 'ior_ot_5',
- ];
- const TRACKED_IOR_SET = new Set(TRACKED_IOR_KEYS);
- const ODDS_HISTORY_START_OFFSET = 2 * 60 * 60 * 1000;
- const ODDS_HISTORY_END_OFFSET = 3 * 60 * 60 * 1000;
- const ODDS_HISTORY_RETENTION = 2 * 24 * 60 * 60 * 1000;
- const ODDS_HISTORY_CLEANUP_INTERVAL = 60 * 60 * 1000;
- const oddsPointSchema = new Schema({
- time: { type: Number, required: true },
- value: { type: Number, required: true },
- source: { type: Number },
- origin: { type: String },
- }, { _id: false });
- const oddsHistorySchema = new Schema({
- eventId: { type: Number, required: true, unique: true, index: true },
- originId: { type: Number },
- leagueName: { type: String },
- teamHomeName: { type: String },
- teamAwayName: { type: String },
- startTime: { type: Number, required: true, index: true },
- endTime: { type: Number, required: true, index: true },
- markets: {
- type: Map,
- of: [oddsPointSchema],
- default: {},
- },
- }, { timestamps: true });
- const OddsHistory = mongoose.model('OddsHistory', oddsHistorySchema);
- const isTrackedKey = (key) => TRACKED_IOR_SET.has(key);
- const isInsideTrackWindow = (eventTime, recordTime = Date.now()) => {
- if (!eventTime) {
- return false;
- }
- return recordTime >= eventTime - ODDS_HISTORY_START_OFFSET &&
- recordTime <= eventTime + ODDS_HISTORY_END_OFFSET;
- }
- const formatPoint = (event) => {
- const value = event?.v;
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
- return null;
- }
- return {
- value,
- source: event.s,
- origin: event.r,
- };
- }
- const appendChangedPoint = (markets, key, point, time) => {
- const history = markets.get(key) ?? [];
- const last = history[history.length - 1];
- if (last && last.value === point.value && last.source === point.source && last.origin === point.origin) {
- return false;
- }
- history.push({ time, ...point });
- markets.set(key, history);
- return true;
- }
- const recordGameOdds = async ({ game, events }) => {
- const eventId = +game?.eventId;
- const startTime = +game?.timestamp;
- const time = Date.now();
- if (!eventId || !isInsideTrackWindow(startTime, time) || !events) {
- return { updated: false };
- }
- const trackedEntries = Object.entries(events)
- .filter(([key]) => isTrackedKey(key))
- .map(([key, event]) => [key, formatPoint(event)])
- .filter(([, point]) => point);
- const doc = await OddsHistory.findOne({ eventId });
- const markets = doc?.markets ?? new Map();
- let changed = false;
- const currentKeys = new Set(trackedEntries.map(([key]) => key));
- trackedEntries.forEach(([key, point]) => {
- if (appendChangedPoint(markets, key, point, time)) {
- changed = true;
- }
- });
- TRACKED_IOR_KEYS.forEach(key => {
- if (currentKeys.has(key) || !markets.has(key)) {
- return;
- }
- if (appendChangedPoint(markets, key, { value: 0 }, time)) {
- changed = true;
- }
- });
- if (!changed) {
- return { updated: false };
- }
- await OddsHistory.findOneAndUpdate(
- { eventId },
- {
- $set: {
- originId: game.originId,
- leagueName: game.leagueName,
- teamHomeName: game.teamHomeName,
- teamAwayName: game.teamAwayName,
- startTime,
- endTime: startTime + ODDS_HISTORY_END_OFFSET,
- markets,
- },
- },
- { upsert: true, new: true },
- );
- return { updated: true };
- }
- const getGameOddsHistory = async (eventId) => {
- if (!eventId) {
- throw new Error('eventId is required');
- }
- const history = await OddsHistory.findOne({ eventId: +eventId }).lean({ flattenMaps: true });
- if (!history) {
- return null;
- }
- return {
- ...history,
- markets: history.markets ? Object.fromEntries(Object.entries(history.markets)) : {},
- };
- }
- const cleanupExpiredHistory = async () => {
- const expireTime = Date.now() - ODDS_HISTORY_RETENTION;
- return OddsHistory.deleteMany({ startTime: { $lt: expireTime } });
- }
- const startCleanup = (logger = console) => {
- const cleanup = () => {
- cleanupExpiredHistory()
- .then(result => {
- if (result.deletedCount) {
- logger.out?.('odds history cleanup deleted %d records', result.deletedCount);
- }
- })
- .catch(err => {
- logger.out?.('odds history cleanup failed: %s', err.message);
- });
- };
- cleanup();
- return setInterval(cleanup, ODDS_HISTORY_CLEANUP_INTERVAL);
- }
- module.exports = {
- TRACKED_IOR_KEYS,
- cleanupExpiredHistory,
- getGameOddsHistory,
- isInsideTrackWindow,
- recordGameOdds,
- startCleanup,
- };
|