| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- import mongoose from 'mongoose';
- const { Schema } = mongoose;
- const TRACKED_IOR_KEYS = [
- 'ior_ot_1',
- 'ior_ot_2',
- 'ior_ot_3',
- 'ior_ot_4',
- ];
- const TRACKED_IOR_SET = new Set(TRACKED_IOR_KEYS);
- const TRACKED_PLATFORMS = ['pc', 'ob'];
- const TRACKED_PLATFORM_SET = new Set(TRACKED_PLATFORMS);
- const DEFAULT_PLATFORM = 'pc';
- 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 = 5 * 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 normalizePlatform = (platform) => {
- return TRACKED_PLATFORM_SET.has(platform) ? platform : DEFAULT_PLATFORM;
- }
- const getMarketKey = (platform, ior) => {
- return `${normalizePlatform(platform)}:${ior}`;
- }
- const parseMarketKey = (key) => {
- if (isTrackedKey(key)) {
- return { ior: key, platform: DEFAULT_PLATFORM };
- }
- const [platform, ior] = String(key).split(/[:.]/);
- if (TRACKED_PLATFORM_SET.has(platform) && isTrackedKey(ior)) {
- return { ior, platform };
- }
- return null;
- }
- const getMarketEntries = (markets = {}) => {
- if (markets instanceof Map) {
- return Array.from(markets.entries());
- }
- return Object.entries(markets);
- }
- const normalizeMarkets = (markets = {}) => {
- const normalized = {};
- getMarketEntries(markets).forEach(([key, points]) => {
- const parsed = parseMarketKey(key);
- if (!parsed || !points?.length) {
- return;
- }
- const marketKey = getMarketKey(parsed.platform, parsed.ior);
- normalized[marketKey] = [
- ...(normalized[marketKey] ?? []),
- ...points,
- ].sort((a, b) => a.time - b.time);
- });
- return normalized;
- }
- const migrateLegacyMarkets = (markets) => {
- getMarketEntries(markets).forEach(([key, points]) => {
- const parsed = parseMarketKey(key);
- if (!parsed || !points?.length) {
- return;
- }
- const marketKey = getMarketKey(parsed.platform, parsed.ior);
- if (key === marketKey) {
- return;
- }
- if (!markets.has(marketKey)) {
- markets.set(marketKey, points);
- }
- else {
- markets.set(marketKey, [
- ...markets.get(marketKey),
- ...points,
- ].sort((a, b) => a.time - b.time));
- }
- markets.delete(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, platform = DEFAULT_PLATFORM }) => {
- const eventId = +game?.eventId;
- const startTime = +game?.timestamp;
- const time = Date.now();
- platform = normalizePlatform(platform);
- 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();
- migrateLegacyMarkets(markets);
- let changed = false;
- const currentKeys = new Set(trackedEntries.map(([key]) => getMarketKey(platform, key)));
- trackedEntries.forEach(([key, point]) => {
- if (appendChangedPoint(markets, getMarketKey(platform, key), point, time)) {
- changed = true;
- }
- });
- TRACKED_IOR_KEYS.forEach(key => {
- const marketKey = getMarketKey(platform, key);
- if (currentKeys.has(marketKey) || !markets.has(marketKey)) {
- return;
- }
- if (appendChangedPoint(markets, marketKey, { 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;
- }
- const markets = normalizeMarkets(history.markets);
- return {
- ...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.keys(normalizeMarkets(history.markets)).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);
- }
- export {
- getGameOddsHistory,
- getOddsHistoryGames,
- recordGameOdds,
- startCleanup,
- };
- export default {
- getGameOddsHistory,
- getOddsHistoryGames,
- recordGameOdds,
- startCleanup,
- };
|