| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- 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 = 3 * 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 getOddsHistoryGames = async ({ page = 1, pageSize = 50, status = -1, keyword } = {}) => {
- page = Math.max(1, +page || 1);
- pageSize = Math.min(50, Math.max(1, +pageSize || 50));
- status = +status;
- const now = Date.now();
- const query = { eventId: { $gt: 0 } };
- if (status === 1) {
- query.endTime = { $gte: now };
- }
- else if (status === 2) {
- query.endTime = { $lt: now };
- }
- if (keyword) {
- const pattern = new RegExp(String(keyword).trim(), 'i');
- query.$or = [
- { leagueName: pattern },
- { teamHomeName: pattern },
- { teamAwayName: pattern },
- ];
- if (/^\d+$/.test(String(keyword).trim())) {
- query.$or.push({ eventId: +keyword });
- }
- }
- const [histories, total] = await Promise.all([
- OddsHistory.find(query)
- .select('eventId leagueName teamHomeName teamAwayName startTime endTime markets updatedAt')
- .sort({ startTime: -1 })
- .skip((page - 1) * pageSize)
- .limit(pageSize)
- .lean({ flattenMaps: true }),
- OddsHistory.countDocuments(query),
- ]);
- const list = histories.map(history => ({
- eventId: history.eventId,
- leagueName: history.leagueName,
- teamHomeName: history.teamHomeName,
- teamAwayName: history.teamAwayName,
- startTime: history.startTime,
- endTime: history.endTime,
- marketCount: Object.values(history.markets ?? {}).filter(points => points?.length).length,
- updatedAt: history.updatedAt,
- }));
- return { list, page, pageSize, total };
- }
- 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,
- getOddsHistoryGames,
- isInsideTrackWindow,
- recordGameOdds,
- startCleanup,
- };
|