|
|
@@ -1,855 +1,73 @@
|
|
|
-import { pinnacleGet, getPsteryRelations, updateBaseEvents, notifyException } from "./libs/pinnacleClient.js";
|
|
|
-import { Logs } from "./libs/logs.js";
|
|
|
-import { getData, setData } from "./libs/cache.js";
|
|
|
+import 'dotenv/config';
|
|
|
|
|
|
+import express from 'express';
|
|
|
|
|
|
-const gamesMapCacheFile = 'data/gamesCache.json';
|
|
|
-const globalDataCacheFile = 'data/globalDataCache.json';
|
|
|
-const TP = 'ps_9_9_1';
|
|
|
-const IS_DEV = process.env.NODE_ENV == 'development';
|
|
|
+import Logs from "./libs/logs.js";
|
|
|
+import requireInternalToken from './middleware/requireInternalToken.js';
|
|
|
+import tradingRoutes from './routes/trading.js';
|
|
|
|
|
|
-const GLOBAL_DATA = {
|
|
|
- filtedLeagues: [],
|
|
|
- filtedGames: [],
|
|
|
- gamesMap: {},
|
|
|
- straightFixturesVersion: 0,
|
|
|
- straightFixturesCount: 0,
|
|
|
- specialFixturesVersion: 0,
|
|
|
- specialFixturesCount: 0,
|
|
|
- straightOddsVersion: 0,
|
|
|
- // straightOddsCount: 0,
|
|
|
- specialsOddsVersion: 0,
|
|
|
- // specialsOddsCount: 0,
|
|
|
- requestErrorCount: 0,
|
|
|
- loopActive: false,
|
|
|
- loopResultTime: 0,
|
|
|
-};
|
|
|
+const app = express();
|
|
|
|
|
|
-const resetVersionsCount = () => {
|
|
|
- GLOBAL_DATA.straightFixturesVersion = 0;
|
|
|
- GLOBAL_DATA.specialFixturesVersion = 0;
|
|
|
- GLOBAL_DATA.straightOddsVersion = 0;
|
|
|
- GLOBAL_DATA.specialsOddsVersion = 0;
|
|
|
+app.use((req, res, next) => {
|
|
|
+ const origin = req.headers.origin;
|
|
|
+ res.header('Access-Control-Allow-Origin', origin || '*');
|
|
|
+ res.header('Access-Control-Allow-Credentials', 'true');
|
|
|
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Internal-Token');
|
|
|
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
|
+ res.header('Vary', 'Origin');
|
|
|
|
|
|
- GLOBAL_DATA.straightFixturesCount = 0;
|
|
|
- GLOBAL_DATA.specialFixturesCount = 0;
|
|
|
-}
|
|
|
-
|
|
|
-const incrementVersionsCount = () => {
|
|
|
- GLOBAL_DATA.straightFixturesCount += 1;
|
|
|
- GLOBAL_DATA.specialFixturesCount += 1;
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-/**
|
|
|
- * 获取指定时区当前日期或时间
|
|
|
- * @param {number} offsetHours - 时区相对 UTC 的偏移(例如:-4 表示 GMT-4)
|
|
|
- * @param {boolean} [withTime=false] - 是否返回完整时间(默认只返回日期)
|
|
|
- * @returns {string} 格式化的日期或时间字符串
|
|
|
- */
|
|
|
-const getDateInTimezone = (offsetHours, withTime = false) => {
|
|
|
- const nowUTC = new Date();
|
|
|
- const targetTime = new Date(nowUTC.getTime() + offsetHours * 60 * 60 * 1000);
|
|
|
-
|
|
|
- const year = targetTime.getUTCFullYear();
|
|
|
- const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0');
|
|
|
- const day = String(targetTime.getUTCDate()).padStart(2, '0');
|
|
|
-
|
|
|
- if (!withTime) {
|
|
|
- return `${year}-${month}-${day}`;
|
|
|
+ if (req.method === 'OPTIONS') {
|
|
|
+ return res.sendStatus(200);
|
|
|
}
|
|
|
+ next();
|
|
|
+});
|
|
|
|
|
|
- const hours = String(targetTime.getUTCHours()).padStart(2, '0');
|
|
|
- const minutes = String(targetTime.getUTCMinutes()).padStart(2, '0');
|
|
|
- const seconds = String(targetTime.getUTCSeconds()).padStart(2, '0');
|
|
|
-
|
|
|
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const isArrayEqualUnordered = (arr1, arr2) => {
|
|
|
- if (arr1.length !== arr2.length) return false;
|
|
|
- const s1 = [...arr1].sort();
|
|
|
- const s2 = [...arr2].sort();
|
|
|
- return s1.every((v, i) => v === s2[i]);
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const getFiltedGames = () => {
|
|
|
- return new Promise(resolve => {
|
|
|
- const updateFiltedGames = () => {
|
|
|
- getPsteryRelations()
|
|
|
- .then(res => {
|
|
|
- if (res.statusCode !== 200) {
|
|
|
- throw new Error(res.message);
|
|
|
- }
|
|
|
- // Logs.outDev('update filted games', res.data);
|
|
|
- const games = res.data.map(game => {
|
|
|
- const { eventId, leagueId } = game?.rel?.ps ?? {};
|
|
|
- return {
|
|
|
- eventId,
|
|
|
- leagueId,
|
|
|
- };
|
|
|
- });
|
|
|
-
|
|
|
- const newFiltedLeagues = [...new Set(games.map(game => game.leagueId).filter(leagueId => leagueId))];
|
|
|
-
|
|
|
- if (!isArrayEqualUnordered(newFiltedLeagues, GLOBAL_DATA.filtedLeagues)) {
|
|
|
- Logs.outDev('filted leagues changed', newFiltedLeagues);
|
|
|
- resetVersionsCount();
|
|
|
- GLOBAL_DATA.filtedLeagues = newFiltedLeagues;
|
|
|
- }
|
|
|
-
|
|
|
- GLOBAL_DATA.filtedGames = games.map(game => game.eventId).filter(eventId => eventId);
|
|
|
+app.use(express.json({ limit: '10mb' }));
|
|
|
|
|
|
- resolve();
|
|
|
- setTimeout(updateFiltedGames, 1000 * 60);
|
|
|
- })
|
|
|
- .catch(err => {
|
|
|
- Logs.err('failed to update filted games:', err.message);
|
|
|
- setTimeout(updateFiltedGames, 1000 * 5);
|
|
|
- });
|
|
|
+app.use((req, res, next) => {
|
|
|
+ res.badRequest = (data, msg) => {
|
|
|
+ if (!msg && typeof data === 'string') {
|
|
|
+ msg = data;
|
|
|
+ data = undefined;
|
|
|
}
|
|
|
- updateFiltedGames();
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const getStraightFixtures = async () => {
|
|
|
- if (!GLOBAL_DATA.filtedLeagues.length) {
|
|
|
- return Promise.reject(new Error('no filted leagues'));
|
|
|
- }
|
|
|
- const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
|
|
|
- let since = GLOBAL_DATA.straightFixturesVersion;
|
|
|
- if (GLOBAL_DATA.straightFixturesCount >= 12) {
|
|
|
- since = 0;
|
|
|
- GLOBAL_DATA.straightFixturesCount = 0;
|
|
|
- }
|
|
|
- if (since == 0) {
|
|
|
- Logs.outDev('full update straight fixtures');
|
|
|
+ return res.status(400).json({ statusCode: 400, code: -1, message: msg ?? 'Bad Request', data });
|
|
|
}
|
|
|
- return pinnacleGet('/v3/fixtures', { sportId: 29, leagueIds, since })
|
|
|
- .then(data => {
|
|
|
- const { league, last } = data;
|
|
|
- if (!last) {
|
|
|
- return {};
|
|
|
+ res.serverError = (data, msg) => {
|
|
|
+ if (!msg && typeof data === 'string') {
|
|
|
+ msg = data;
|
|
|
+ data = undefined;
|
|
|
}
|
|
|
- GLOBAL_DATA.straightFixturesVersion = last;
|
|
|
- const games = league?.map(league => {
|
|
|
- const { id: leagueId, events } = league;
|
|
|
- return events?.map(event => {
|
|
|
- const { starts } = event;
|
|
|
- const timestamp = new Date(starts).getTime();
|
|
|
- return { leagueId, ...event, timestamp };
|
|
|
- });
|
|
|
- })
|
|
|
- .flat() ?? [];
|
|
|
- const update = since == 0 ? 'full' : 'increment';
|
|
|
- return { games, update };
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const updateStraightFixtures = async () => {
|
|
|
- return getStraightFixtures()
|
|
|
- .then(data => {
|
|
|
- const { games, update } = data;
|
|
|
- const { gamesMap } = GLOBAL_DATA;
|
|
|
- if (games?.length) {
|
|
|
- games.forEach(game => {
|
|
|
- const { id } = game;
|
|
|
- if (!gamesMap[id]) {
|
|
|
- gamesMap[id] = game;
|
|
|
- }
|
|
|
- else {
|
|
|
- Object.assign(gamesMap[id], game);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- if (update && update == 'full') {
|
|
|
- const gamesSet = new Set(games.map(game => game.id));
|
|
|
- Object.keys(gamesMap)?.forEach(key => {
|
|
|
- if (!gamesSet.has(+key)) {
|
|
|
- delete gamesMap[key];
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const getStraightOdds = async () => {
|
|
|
- if (!GLOBAL_DATA.filtedLeagues.length) {
|
|
|
- return Promise.reject(new Error('no filted leagues'));
|
|
|
- }
|
|
|
- const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
|
|
|
- const since = GLOBAL_DATA.straightOddsVersion;
|
|
|
- // if (GLOBAL_DATA.straightOddsCount >= 27) {
|
|
|
- // since = 0;
|
|
|
- // GLOBAL_DATA.straightOddsCount = 3;
|
|
|
- // }
|
|
|
- if (since == 0) {
|
|
|
- Logs.outDev('full update straight odds');
|
|
|
+ return res.status(500).json({ statusCode: 500, code: -1, message: msg ?? 'Internal Server Error', data });
|
|
|
}
|
|
|
- return pinnacleGet('/v4/odds', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
|
|
|
- .then(data => {
|
|
|
- const { leagues, last } = data;
|
|
|
- if (!last) {
|
|
|
- return [];
|
|
|
- }
|
|
|
- GLOBAL_DATA.straightOddsVersion = last;
|
|
|
- const games = leagues?.flatMap(league => league.events);
|
|
|
- // return games;
|
|
|
- return games?.map(item => {
|
|
|
- const { periods, ...rest } = item;
|
|
|
- const straight = periods?.find(period => period.number == 0);
|
|
|
- const straight1st = periods?.find(period => period.number == 1);
|
|
|
- const gameInfo = rest;
|
|
|
- if (straight || straight1st) {
|
|
|
- gameInfo.periods = {};
|
|
|
- }
|
|
|
- if (straight) {
|
|
|
- gameInfo.periods.straight = straight;
|
|
|
- }
|
|
|
- if (straight1st) {
|
|
|
- gameInfo.periods.straight1st = straight1st;
|
|
|
- }
|
|
|
- return gameInfo;
|
|
|
- }) ?? [];
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const updateStraightOdds = async () => {
|
|
|
- return getStraightOdds()
|
|
|
- .then(games => {
|
|
|
- if (games?.length) {
|
|
|
- const { gamesMap={} } = GLOBAL_DATA;
|
|
|
- games.forEach(game => {
|
|
|
- const { id, periods, ...rest } = game;
|
|
|
- const localGame = gamesMap[id];
|
|
|
- if (localGame) {
|
|
|
- Object.assign(localGame, rest);
|
|
|
- if (!localGame.periods && periods) {
|
|
|
- localGame.periods = periods;
|
|
|
- }
|
|
|
- else if (periods) {
|
|
|
- Object.assign(localGame.periods, periods);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
+ res.unauthorized = (data, msg) => {
|
|
|
+ if (!msg && typeof data === 'string') {
|
|
|
+ msg = data;
|
|
|
+ data = undefined;
|
|
|
}
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const getSpecialFixtures = async () => {
|
|
|
- if (!GLOBAL_DATA.filtedLeagues.length) {
|
|
|
- return Promise.reject(new Error('no filted leagues'));
|
|
|
- }
|
|
|
- const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
|
|
|
- let since = GLOBAL_DATA.specialFixturesVersion;
|
|
|
- if (GLOBAL_DATA.specialFixturesCount >= 18) {
|
|
|
- since = 0;
|
|
|
- GLOBAL_DATA.specialFixturesCount = 6;
|
|
|
- }
|
|
|
- if (since == 0) {
|
|
|
- Logs.outDev('full update special fixtures');
|
|
|
+ return res.status(401).json({ statusCode: 401, code: -1, message: msg ?? 'Unauthorized', data });
|
|
|
}
|
|
|
- return pinnacleGet('/v2/fixtures/special', { sportId: 29, leagueIds, since })
|
|
|
- .then(data => {
|
|
|
- const { leagues, last } = data;
|
|
|
- if (!last) {
|
|
|
- return { specials: [], update: 'increment' };
|
|
|
- }
|
|
|
- GLOBAL_DATA.specialFixturesVersion = last;
|
|
|
- const specials = leagues?.map(league => {
|
|
|
- const { specials } = league;
|
|
|
- return specials?.filter(special => special.event)
|
|
|
- .map(special => {
|
|
|
- const { event: { id: eventId, periodNumber }, ...rest } = special ?? { event: {} };
|
|
|
- return { eventId, periodNumber, ...rest };
|
|
|
- }) ?? [];
|
|
|
- })
|
|
|
- .flat()
|
|
|
- .filter(special => {
|
|
|
- if (special.name.includes('Winning Margin') || special.name.includes('Exact Total Goals')) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- return false;
|
|
|
- }) ?? [];
|
|
|
- const update = since == 0 ? 'full' : 'increment';
|
|
|
- return { specials, update };
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const mergeContestants = (localContestants=[], remoteContestants=[]) => {
|
|
|
- const localContestantsMap = new Map(localContestants.map(contestant => [contestant.id, contestant]));
|
|
|
- remoteContestants.forEach(contestant => {
|
|
|
- const localContestant = localContestantsMap.get(contestant.id);
|
|
|
- if (localContestant) {
|
|
|
- Object.assign(localContestant, contestant);
|
|
|
- }
|
|
|
- else {
|
|
|
- localContestants.push(contestant);
|
|
|
+ res.sendSuccess = (data, msg) => {
|
|
|
+ const response = { statusCode: 200, code: 0, message: msg ?? 'OK' }
|
|
|
+ if (data !== undefined) {
|
|
|
+ response.data = data;
|
|
|
}
|
|
|
- });
|
|
|
- const remoteContestantsMap = new Map(remoteContestants.map(contestant => [contestant.id, contestant]));
|
|
|
- for (let i = localContestants.length - 1; i >= 0; i--) {
|
|
|
- if (!remoteContestantsMap.has(localContestants[i].id)) {
|
|
|
- localContestants.splice(i, 1);
|
|
|
- }
|
|
|
- }
|
|
|
- return localContestants;
|
|
|
-}
|
|
|
-
|
|
|
-const mergeSpecials = (localSpecials, remoteSpecials, specialName) => {
|
|
|
- if (localSpecials[specialName] && remoteSpecials[specialName]) {
|
|
|
- const { contestants: specialContestants, ...specialRest } = remoteSpecials[specialName];
|
|
|
- Object.assign(localSpecials[specialName], specialRest);
|
|
|
- mergeContestants(localSpecials[specialName].contestants, specialContestants);
|
|
|
+ return res.status(200).json(response);
|
|
|
}
|
|
|
- else if (remoteSpecials[specialName]) {
|
|
|
- localSpecials[specialName] = remoteSpecials[specialName];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const updateSpecialFixtures = async () => {
|
|
|
- return getSpecialFixtures()
|
|
|
- .then(data => {
|
|
|
- const { specials, update } = data;
|
|
|
- if (specials?.length) {
|
|
|
- const { gamesMap={} } = GLOBAL_DATA;
|
|
|
- const gamesSpecialsMap = {};
|
|
|
- specials.forEach(special => {
|
|
|
- const { eventId } = special;
|
|
|
- if (!gamesSpecialsMap[eventId]) {
|
|
|
- gamesSpecialsMap[eventId] = {};
|
|
|
- }
|
|
|
- if (special.name == 'Winning Margin') {
|
|
|
- gamesSpecialsMap[eventId].winningMargin = special;
|
|
|
- }
|
|
|
- else if (special.name == 'Exact Total Goals') {
|
|
|
- gamesSpecialsMap[eventId].exactTotalGoals = special;
|
|
|
- }
|
|
|
- else if (special.name == 'Winning Margin 1st Half') {
|
|
|
- gamesSpecialsMap[eventId].winningMargin1st = special;
|
|
|
- }
|
|
|
- else if (special.name == 'Exact Total Goals 1st Half') {
|
|
|
- gamesSpecialsMap[eventId].exactTotalGoals1st = special;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- Object.keys(gamesSpecialsMap).forEach(eventId => {
|
|
|
- if (!gamesMap[eventId]) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!gamesMap[eventId].specials) {
|
|
|
- gamesMap[eventId].specials = {};
|
|
|
- }
|
|
|
-
|
|
|
- const localSpecials = gamesMap[eventId].specials;
|
|
|
- const remoteSpecials = gamesSpecialsMap[eventId];
|
|
|
-
|
|
|
- mergeSpecials(localSpecials, remoteSpecials, 'winningMargin');
|
|
|
- mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals');
|
|
|
- mergeSpecials(localSpecials, remoteSpecials, 'winningMargin1st');
|
|
|
- mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals1st');
|
|
|
-
|
|
|
- });
|
|
|
+ res.sendError = (err) => {
|
|
|
+ if (err.cause === 400 || err.status === 400) {
|
|
|
+ return res.badRequest(err.data, err.message);
|
|
|
}
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const getSpecialsOdds = async () => {
|
|
|
- if (!GLOBAL_DATA.filtedLeagues.length) {
|
|
|
- return Promise.reject(new Error('no filted leagues'));
|
|
|
+ return res.serverError(err.data, err.message);
|
|
|
}
|
|
|
- const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
|
|
|
- const since = GLOBAL_DATA.specialsOddsVersion;
|
|
|
- // if (GLOBAL_DATA.specialsOddsCount >= 33) {
|
|
|
- // since = 0;
|
|
|
- // GLOBAL_DATA.specialsOddsCount = 9;
|
|
|
- // }
|
|
|
- if (since == 0) {
|
|
|
- Logs.outDev('full update specials odds');
|
|
|
- }
|
|
|
- return pinnacleGet('/v2/odds/special', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
|
|
|
- .then(data => {
|
|
|
- const { leagues, last } = data;
|
|
|
- if (!last) {
|
|
|
- return [];
|
|
|
- }
|
|
|
- GLOBAL_DATA.specialsOddsVersion = last;
|
|
|
- return leagues?.flatMap(league => league.specials);
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const updateSpecialsOdds = async () => {
|
|
|
- return getSpecialsOdds()
|
|
|
- .then(specials => {
|
|
|
- if (specials.length) {
|
|
|
- const { gamesMap={} } = GLOBAL_DATA;
|
|
|
- const contestants = Object.values(gamesMap)
|
|
|
- .filter(game => game.specials)
|
|
|
- .map(game => {
|
|
|
- const { specials } = game;
|
|
|
- const contestants = Object.values(specials).map(special => {
|
|
|
- const { contestants } = special;
|
|
|
- return contestants.map(contestant => [contestant.id, contestant]);
|
|
|
- });
|
|
|
- return contestants;
|
|
|
- }).flat(2);
|
|
|
- const contestantsMap = new Map(contestants);
|
|
|
- const lines = specials?.flatMap(special => special.contestantLines) ?? [];
|
|
|
- lines.forEach(line => {
|
|
|
- const { id, handicap, lineId, max, price } = line;
|
|
|
- const contestant = contestantsMap.get(id);
|
|
|
- if (!contestant) {
|
|
|
- return;
|
|
|
- }
|
|
|
- contestant.handicap = handicap;
|
|
|
- contestant.lineId = lineId;
|
|
|
- contestant.max = max;
|
|
|
- contestant.price = price;
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const getInRunning = async () => {
|
|
|
- return pinnacleGet('/v2/inrunning')
|
|
|
- .then(data => {
|
|
|
- const sportId = 29;
|
|
|
- const leagues = data.sports?.find(sport => sport.id == sportId)?.leagues ?? [];
|
|
|
- return leagues.filter(league => {
|
|
|
- const { id } = league;
|
|
|
- const filtedLeaguesSet = new Set(GLOBAL_DATA.filtedLeagues);
|
|
|
- return filtedLeaguesSet.has(id);
|
|
|
- }).flatMap(league => league.events);
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const updateInRunning = async () => {
|
|
|
- return getInRunning()
|
|
|
- .then(games => {
|
|
|
- if (!games.length) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const { gamesMap={} } = GLOBAL_DATA;
|
|
|
- games.forEach(game => {
|
|
|
- const { id, state, elapsed } = game;
|
|
|
- const localGame = gamesMap[id];
|
|
|
- if (localGame) {
|
|
|
- Object.assign(localGame, { state, elapsed });
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const ratioAccept = (ratio) => {
|
|
|
- if (ratio > 0) {
|
|
|
- return 'a';
|
|
|
- }
|
|
|
- return '';
|
|
|
-}
|
|
|
-const ratioString = (ratio) => {
|
|
|
- ratio = Math.abs(ratio);
|
|
|
- ratio = ratio.toString();
|
|
|
- ratio = ratio.replace(/\./, '');
|
|
|
- return ratio;
|
|
|
-}
|
|
|
-
|
|
|
-const parseSpreads = (spreads, wm) => {
|
|
|
- // 让分盘
|
|
|
- if (!spreads?.length) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- const events = {};
|
|
|
- spreads.forEach(spread => {
|
|
|
- const { hdp, home, away } = spread;
|
|
|
-
|
|
|
- // if (!(hdp % 1) || !!(hdp % 0.5)) {
|
|
|
- // // 整数或不能被0.5整除的让分盘不处理
|
|
|
- // return;
|
|
|
- // }
|
|
|
-
|
|
|
- const ratio_ro = hdp;
|
|
|
- const ratio_r = ratio_ro - wm;
|
|
|
- events[`ior_r${ratioAccept(ratio_r)}h_${ratioString(ratio_r)}`] = {
|
|
|
- v: home,
|
|
|
- r: wm != 0 ? `ior_r${ratioAccept(ratio_ro)}h_${ratioString(ratio_ro)}` : undefined
|
|
|
- };
|
|
|
- events[`ior_r${ratioAccept(-ratio_r)}c_${ratioString(ratio_r)}`] = {
|
|
|
- v: away,
|
|
|
- r: wm != 0 ? `ior_r${ratioAccept(-ratio_ro)}c_${ratioString(ratio_ro)}` : undefined
|
|
|
- };
|
|
|
- });
|
|
|
- return events;
|
|
|
-}
|
|
|
-
|
|
|
-const parseMoneyline = (moneyline) => {
|
|
|
- // 胜平负
|
|
|
- if (!moneyline) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- const { home, away, draw } = moneyline;
|
|
|
- return {
|
|
|
- 'ior_mh': { v: home },
|
|
|
- 'ior_mc': { v: away },
|
|
|
- 'ior_mn': { v: draw },
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const parseTotals = (totals) => {
|
|
|
- // 大小球盘
|
|
|
- if (!totals?.length) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- const events = {};
|
|
|
-
|
|
|
- totals.forEach(total => {
|
|
|
- const { points, over, under } = total;
|
|
|
- events[`ior_ouc_${ratioString(points)}`] = { v: over };
|
|
|
- events[`ior_ouh_${ratioString(points)}`] = { v: under };
|
|
|
- });
|
|
|
- return events;
|
|
|
-}
|
|
|
-
|
|
|
-const parseStraight = (straight, wm) => {
|
|
|
- if (!straight) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- const { cutoff='', status=0, spreads=[], moneyline={}, totals=[] } = straight;
|
|
|
- const cutoffTime = new Date(cutoff).getTime();
|
|
|
- const nowTime = Date.now();
|
|
|
-
|
|
|
- if (status != 1 || cutoffTime < nowTime) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- const events = {};
|
|
|
- Object.assign(events, parseSpreads(spreads, wm));
|
|
|
- Object.assign(events, parseMoneyline(moneyline));
|
|
|
- Object.assign(events, parseTotals(totals));
|
|
|
-
|
|
|
- return events;
|
|
|
-}
|
|
|
-
|
|
|
-const parseWinningMargin = (winningMargin, home, away) => {
|
|
|
- if (!winningMargin) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- const { cutoff='', status='', contestants=[] } = winningMargin;
|
|
|
- const cutoffTime = new Date(cutoff).getTime();
|
|
|
- const nowTime = Date.now();
|
|
|
-
|
|
|
- if (status != 'O' || cutoffTime < nowTime || !contestants?.length) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- const events = {};
|
|
|
- contestants.forEach(contestant => {
|
|
|
- const { name, price } = contestant;
|
|
|
- const nr = name.match(/\d+$/)?.[0];
|
|
|
- if (!nr) {
|
|
|
- return;
|
|
|
- }
|
|
|
- let side;
|
|
|
- if (name.startsWith(home)) {
|
|
|
- side = 'h';
|
|
|
- }
|
|
|
- else if (name.startsWith(away)) {
|
|
|
- side = 'c';
|
|
|
- }
|
|
|
- else {
|
|
|
- return;
|
|
|
- }
|
|
|
- events[`ior_wm${side}_${nr}`] = { v: price };
|
|
|
- });
|
|
|
- return events;
|
|
|
-}
|
|
|
-
|
|
|
-const parseExactTotalGoals = (exactTotalGoals) => {
|
|
|
- if (!exactTotalGoals) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- const { cutoff='', status='', contestants=[] } = exactTotalGoals;
|
|
|
- const cutoffTime = new Date(cutoff).getTime();
|
|
|
- const nowTime = Date.now();
|
|
|
-
|
|
|
- if (status != 'O' || cutoffTime < nowTime || !contestants?.length) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- const events = {};
|
|
|
- contestants.forEach(contestant => {
|
|
|
- const { name, price } = contestant;
|
|
|
- if (+name >= 1 && +name <= 7) {
|
|
|
- events[`ior_ot_${name}`] = { v: price };
|
|
|
- }
|
|
|
- });
|
|
|
- return events;
|
|
|
-}
|
|
|
-
|
|
|
-const parseRbState = (state) => {
|
|
|
- let stage = null;
|
|
|
- if (state == 1) {
|
|
|
- stage = '1H';
|
|
|
- }
|
|
|
- else if (state == 2) {
|
|
|
- stage = 'HT';
|
|
|
- }
|
|
|
- else if (state == 3) {
|
|
|
- stage = '2H';
|
|
|
- }
|
|
|
- else if (state >= 4) {
|
|
|
- stage = 'ET';
|
|
|
- }
|
|
|
- return stage;
|
|
|
-}
|
|
|
-
|
|
|
-const parseGame = (game) => {
|
|
|
- const { eventId=0, originId=0, periods={}, specials={}, home, away, marketType, state, elapsed, homeScore=0, awayScore=0 } = game;
|
|
|
- const { straight, straight1st } = periods;
|
|
|
- const { winningMargin={}, exactTotalGoals={}, winningMargin1st={}, exactTotalGoals1st={} } = specials;
|
|
|
- const filtedGamesSet = new Set(GLOBAL_DATA.filtedGames);
|
|
|
- const wm = homeScore - awayScore;
|
|
|
- const score = `${homeScore}-${awayScore}`;
|
|
|
- const events = parseStraight(straight, wm) ?? {};
|
|
|
- const stage = parseRbState(state);
|
|
|
- const retime = elapsed ? `${elapsed}'` : '';
|
|
|
- const evtime = Date.now();
|
|
|
- Object.assign(events, parseWinningMargin(winningMargin, home, away));
|
|
|
- Object.assign(events, parseExactTotalGoals(exactTotalGoals));
|
|
|
- const gameInfos = [];
|
|
|
- gameInfos.push({ eventId, originId, events, evtime, stage, retime, score, wm, marketType });
|
|
|
- const halfEventId = eventId * -1;
|
|
|
- if (filtedGamesSet.has(halfEventId)) {
|
|
|
- const events = parseStraight(straight1st, wm) ?? {};
|
|
|
- Object.assign(events, parseWinningMargin(winningMargin1st, home, away));
|
|
|
- Object.assign(events, parseExactTotalGoals(exactTotalGoals1st));
|
|
|
- gameInfos.push({ eventId: halfEventId, originId, events, evtime, stage, retime, score, wm, marketType });
|
|
|
- }
|
|
|
- return gameInfos;
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const getGames = () => {
|
|
|
- const { filtedGames, gamesMap={} } = GLOBAL_DATA;
|
|
|
- const filtedGamesSet = new Set(filtedGames);
|
|
|
- const nowTime = Date.now();
|
|
|
- const gamesData = {};
|
|
|
- Object.values(gamesMap).forEach(game => {
|
|
|
- const { id, liveStatus, parentId, resultingUnit, timestamp } = game;
|
|
|
-
|
|
|
- if (resultingUnit !== 'Regular') {
|
|
|
- return false; // 非常规赛事不处理
|
|
|
- }
|
|
|
-
|
|
|
- const gmtMinus4Date = getDateInTimezone(-4);
|
|
|
- const todayEndTime = new Date(`${gmtMinus4Date} 23:59:59 GMT-4`).getTime();
|
|
|
- const tomorrowEndTime = todayEndTime + 24 * 60 * 60 * 1000;
|
|
|
- if (liveStatus == 1 && timestamp < nowTime) {
|
|
|
- game.marketType = 2; // 滚球赛事
|
|
|
- }
|
|
|
- else if (liveStatus != 1 && timestamp > nowTime && timestamp <= todayEndTime) {
|
|
|
- game.marketType = 1; // 今日赛事
|
|
|
- }
|
|
|
- else if (liveStatus != 1 && timestamp > todayEndTime && timestamp <= tomorrowEndTime) {
|
|
|
- game.marketType = 0; // 明日早盘赛事
|
|
|
- }
|
|
|
- else {
|
|
|
- game.marketType = -1; // 非近期赛事
|
|
|
- }
|
|
|
-
|
|
|
- if (game.marketType < 0) {
|
|
|
- return false; // 非近期赛事不处理
|
|
|
- }
|
|
|
-
|
|
|
- let actived = false;
|
|
|
- if (liveStatus != 1 && filtedGamesSet.has(id)) {
|
|
|
- actived = true; // 在赛前列表中
|
|
|
- game.eventId = id;
|
|
|
- game.originId = 0;
|
|
|
- }
|
|
|
- else if (liveStatus == 1 && filtedGamesSet.has(parentId)) {
|
|
|
- actived = true; // 在滚球列表中
|
|
|
- game.eventId = parentId;
|
|
|
- game.originId = id;
|
|
|
- }
|
|
|
-
|
|
|
- if (actived) {
|
|
|
- parseGame(game)?.forEach(gameInfo => {
|
|
|
- const { marketType, ...rest } = gameInfo;
|
|
|
- if (!gamesData[marketType]) {
|
|
|
- gamesData[marketType] = [];
|
|
|
- }
|
|
|
- gamesData[marketType].push(rest);
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
- return gamesData;
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const pinnacleDataLoop = () => {
|
|
|
- updateStraightFixtures()
|
|
|
- .then(() => {
|
|
|
- return Promise.all([
|
|
|
- updateStraightOdds(),
|
|
|
- updateSpecialFixtures(),
|
|
|
- updateInRunning(),
|
|
|
- ]);
|
|
|
- })
|
|
|
- .then(() => {
|
|
|
- return updateSpecialsOdds();
|
|
|
- })
|
|
|
- .then(() => {
|
|
|
- if (!GLOBAL_DATA.loopActive) {
|
|
|
- GLOBAL_DATA.loopActive = true;
|
|
|
- Logs.out('loop active');
|
|
|
- notifyException('Pinnacle API startup.');
|
|
|
- }
|
|
|
-
|
|
|
- if (GLOBAL_DATA.requestErrorCount > 0) {
|
|
|
- GLOBAL_DATA.requestErrorCount = 0;
|
|
|
- Logs.out('request error count reset');
|
|
|
- }
|
|
|
-
|
|
|
- const nowTime = Date.now();
|
|
|
- const loopDuration = nowTime - GLOBAL_DATA.loopResultTime;
|
|
|
- GLOBAL_DATA.loopResultTime = nowTime;
|
|
|
- if (loopDuration > 15000) {
|
|
|
- Logs.out('loop duration is too long', loopDuration);
|
|
|
- }
|
|
|
- else {
|
|
|
- Logs.outDev('loop duration', loopDuration);
|
|
|
- }
|
|
|
-
|
|
|
- const timestamp = Date.now();
|
|
|
- const games = getGames();
|
|
|
- const data = { games, timestamp, tp: TP };
|
|
|
-
|
|
|
- updateBaseEvents(data);
|
|
|
-
|
|
|
- if (IS_DEV) {
|
|
|
- setData(gamesMapCacheFile, GLOBAL_DATA.gamesMap)
|
|
|
- .then(() => {
|
|
|
- Logs.outDev('games map saved');
|
|
|
- })
|
|
|
- .catch(err => {
|
|
|
- Logs.err('failed to save games map', err.message);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- })
|
|
|
- .catch(err => {
|
|
|
- Logs.err(err.message, err.source);
|
|
|
- GLOBAL_DATA.requestErrorCount++;
|
|
|
- if (GLOBAL_DATA.loopActive && GLOBAL_DATA.requestErrorCount > 5) {
|
|
|
- const exceptionMessage = 'request errors have reached the limit';
|
|
|
- Logs.out(exceptionMessage);
|
|
|
- GLOBAL_DATA.loopActive = false;
|
|
|
-
|
|
|
- Logs.out('loop inactive');
|
|
|
- const exceptionList = ['Pinnacle API paused']
|
|
|
- if (exceptionMessage) {
|
|
|
- exceptionList.push(exceptionMessage);
|
|
|
- }
|
|
|
- if (err.source?.data?.message) {
|
|
|
- exceptionList.push(err.source.data.message);
|
|
|
- }
|
|
|
- else if (err.message) {
|
|
|
- exceptionList.push(err.message);
|
|
|
- }
|
|
|
- if (err.source?.username) {
|
|
|
- exceptionList.push(err.source.username);
|
|
|
- }
|
|
|
- notifyException(exceptionList.join('. '));
|
|
|
- }
|
|
|
- })
|
|
|
- .finally(() => {
|
|
|
- const { loopActive } = GLOBAL_DATA;
|
|
|
- let loopDelay = 1000 * 5;
|
|
|
- if (!loopActive) {
|
|
|
- loopDelay = 1000 * 60;
|
|
|
- resetVersionsCount();
|
|
|
- }
|
|
|
- else {
|
|
|
- incrementVersionsCount();
|
|
|
- }
|
|
|
- setTimeout(pinnacleDataLoop, loopDelay);
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-/**
|
|
|
- * 缓存GLOBAL_DATA数据到文件
|
|
|
- */
|
|
|
-const saveGlobalDataToCache = async () => {
|
|
|
- return setData(globalDataCacheFile, GLOBAL_DATA);
|
|
|
-}
|
|
|
-
|
|
|
-const loadGlobalDataFromCache = async () => {
|
|
|
- return getData(globalDataCacheFile)
|
|
|
- .then(data => {
|
|
|
- if (!data) {
|
|
|
- return;
|
|
|
- }
|
|
|
- Object.keys(GLOBAL_DATA)?.forEach(key => {
|
|
|
- if (key in data) {
|
|
|
- GLOBAL_DATA[key] = data[key];
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 监听进程退出事件,保存GLOBAL_DATA数据
|
|
|
-const saveExit = (code) => {
|
|
|
- saveGlobalDataToCache()
|
|
|
- .then(() => {
|
|
|
- Logs.out('global data saved');
|
|
|
- })
|
|
|
- .catch(err => {
|
|
|
- Logs.err('failed to save global data', err.message);
|
|
|
- })
|
|
|
- .finally(() => {
|
|
|
- process.exit(code);
|
|
|
- });
|
|
|
-}
|
|
|
-process.on('SIGINT', () => {
|
|
|
- saveExit(0);
|
|
|
-});
|
|
|
-process.on('SIGTERM', () => {
|
|
|
- saveExit(0);
|
|
|
-});
|
|
|
-process.on('SIGUSR2', () => {
|
|
|
- saveExit(0);
|
|
|
+ next();
|
|
|
});
|
|
|
|
|
|
+app.get('/health', (req, res) => {
|
|
|
+ res.sendSuccess({ service: 'polymarket', status: 'ok' });
|
|
|
+});
|
|
|
|
|
|
-(() => {
|
|
|
- loadGlobalDataFromCache()
|
|
|
- .then(() => {
|
|
|
- Logs.out('global data loaded');
|
|
|
- })
|
|
|
- .catch(err => {
|
|
|
- Logs.err('failed to load global data', err.message);
|
|
|
- })
|
|
|
- .finally(() => {
|
|
|
- GLOBAL_DATA.loopResultTime = Date.now();
|
|
|
- GLOBAL_DATA.loopActive = true;
|
|
|
- return getFiltedGames();
|
|
|
- })
|
|
|
- .then(pinnacleDataLoop);
|
|
|
-})();
|
|
|
-
|
|
|
+app.use('/api/trading', requireInternalToken, tradingRoutes);
|
|
|
|
|
|
+// 启动服务
|
|
|
+const PORT = process.env.PORT || 9056;
|
|
|
+app.listen(PORT, () => Logs.out(`Pinnacle service running on port ${PORT}`));
|