const axios = require('axios'); const Logs = require('../libs/logs'); const Setting = require('./Setting'); const calcTotalProfit = require('../triangle/totalProfitCalc'); const childOptions = process.env.NODE_ENV == 'development' ? { execArgv: ['--inspect=9228'] } : {}; const { fork } = require('child_process'); const events_child = fork('./triangle/eventsMatch.js', [], childOptions); const PS_IOR_KEYS = [ ['0', 'ior_mh', 'ior_mn', 'ior_mc'], // ['0', 'ior_rh_05', 'ior_mn', 'ior_rc_05'], ['-1', 'ior_rh_15', 'ior_wmh_1', 'ior_rac_05'], ['-2', 'ior_rh_25', 'ior_wmh_2', 'ior_rac_15'], ['+1', 'ior_rah_05', 'ior_wmc_1', 'ior_rc_15'], ['+2', 'ior_rah_15', 'ior_wmc_2', 'ior_rc_25'], ]; const BASE_URL = 'https://api.isthe.me/api/p'; const IS_DEV = process.env.NODE_ENV == 'development'; const GAMES = { Leagues: {}, List: {}, Baselist: {}, Relations: {}, Solutions: {}, }; const Request = { callbacks: {}, count: 0, } /** * 精确浮点数字 * @param {number} number * @param {number} x * @returns {number} */ const fixFloat = (number, x=2) => { return parseFloat(number.toFixed(x)); } /** * 获取市场类型 */ const getMarketType = (mk) => { return mk == 0 ? 'early' : 'today'; } /** * 同步联赛列表 */ const syncLeaguesList = ({ mk, leagues }) => { if (IS_DEV) { return Logs.out('syncLeaguesList', { mk, leagues }); } axios.post(`${BASE_URL}/syncLeague`, { mk, leagues }) .then(res => { Logs.out('syncLeaguesList', res.data); }) .catch(err => { Logs.out('syncLeaguesList', err.message); }); } /** * 更新联赛列表 */ const updateLeaguesList = ({ mk, leagues }) => { const leaguesList = GAMES.Leagues; if (JSON.stringify(leaguesList[mk]) != JSON.stringify(leagues)) { leaguesList[mk] = leagues; syncLeaguesList({ mk, leagues }); return leagues.length; } return 0; } /** * 获取筛选过的联赛 */ const getFilteredLeagues = async (mk) => { return axios.get(`${BASE_URL}/getLeagueTast?mk=${mk}`) .then(res => { if (res.data.code == 0) { return res.data.data; } return Promise.reject(new Error(res.data.message)); }); } /** * 同步比赛列表到服务器 */ const syncGamesList = ({ platform, mk, games }) => { if (IS_DEV) { return Logs.out('syncGamesList', { platform, mk, games }); } axios.post(`${BASE_URL}/syncGames`, { platform, mk, games }) .then(res => { Logs.out('syncGamesList', { platform, mk, count: games.length }, res.data); }) .catch(err => { Logs.out('syncGamesList', { platform, mk }, err.message); }); } /** * 同步基准比赛列表 */ const syncBaseList = ({ marketType, games }) => { const baseList = GAMES.Baselist; if (!baseList[marketType]) { baseList[marketType] = games; } const newMap = new Map(games.map(item => [item.eventId, item])); // 删除不存在的项 for (let i = baseList[marketType].length - 1; i >= 0; i--) { if (!newMap.has(baseList[marketType][i].eventId)) { baseList[marketType].splice(i, 1); } } // 添加或更新 const oldIds = new Set(baseList[marketType].map(item => item.eventId)); games.forEach(game => { if (!oldIds.has(game.eventId)) { // 添加新项 baseList[marketType].push(game); } }); } /** * 更新比赛列表 */ const updateGamesList = (({ platform, mk, games } = {}) => { return new Promise((resolve, reject) => { if (!platform || !games) { return reject(new Error('PLATFORM_GAMES_INVALID')); } const marketType = getMarketType(mk); syncGamesList({ platform, mk, games }); if (platform == 'ps') { syncBaseList({ marketType, games }); } resolve(); }); }); /** * 提交盘口数据 */ const submitOdds = ({ platform, mk, games }) => { if (IS_DEV) { return Logs.out('syncOdds', { platform, mk, games }); } axios.post(`${BASE_URL}/syncOdds`, { platform, mk, games}) .then(res => { Logs.out('syncOdds', { platform, mk, count: games.length }, res.data); }) .catch(err => { Logs.out('syncOdds', { platform, mk }, err.message); }); } /** * 同步基准盘口 */ const syncBaseEvents = ({ mk, games, outrights }) => { const marketType = getMarketType(mk); const baseList = GAMES.Baselist; if (!baseList[marketType]) { return; } const baseMap = new Map(baseList[marketType].map(item => [item.eventId, item])); games?.forEach(game => { const { eventId, evtime, events } = game; const baseGame = baseMap.get(eventId); if (baseGame) { baseGame.evtime = evtime; baseGame.events = events; } }); outrights?.forEach(outright => { const { parentId, sptime, special } = outright; const baseGame = baseMap.get(parentId); if (baseGame) { baseGame.sptime = sptime; baseGame.special = special; } }); if (games?.length) { const gamesList = baseList[marketType]?.map(game => { const { evtime, events, sptime, special, ...gameInfo } = game; const expireTime = Date.now() - 15000; let odds = {}; if (evtime > expireTime) { odds = { ...odds, ...events }; } if (sptime > expireTime) { odds = { ...odds, ...special }; } const matches = PS_IOR_KEYS.map(([label, ...keys]) => { const match = keys.map(key => ({ key, value: odds[key] ?? 0 })); return { label, match }; }).filter(item => item.match.every(entry => entry.value !== 0)); return { ...gameInfo, matches, uptime: Math.min(evtime ?? 0, sptime ?? 0) }; }); if (gamesList.filter(item => item.uptime > 0).length) { submitOdds({ platform: 'ps', mk, games: gamesList }); } const relatedGames = Object.values(GAMES.Relations).map(item => item.rel?.['ps'] ?? {}); if (!relatedGames.length) { return 0; } let update = 0; const relatedMap = new Map(relatedGames.map(item => [item.eventId, item])); gamesList?.forEach(game => { const { eventId, matches, uptime } = game; const relatedGame = relatedMap.get(eventId); if (relatedGame) { const events = {}; matches.forEach(({ label, match }) => { match.forEach(({ key, value }) => { events[key] = value; }); }); relatedGame.evtime = uptime; relatedGame.events = events; update ++; } }); return update; } } const updateGamesEvents = ({ platform, mk, games, outrights }) => { return new Promise((resolve, reject) => { if (!platform || (!games && !outrights)) { return reject(new Error('PLATFORM_GAMES_INVALID')); } if (platform == 'ps') { const update = syncBaseEvents({ mk, games, outrights }); return resolve({ update }); } const relatedGames = Object.values(GAMES.Relations).map(item => item.rel?.[platform] ?? {}); if (!relatedGames.length) { return resolve({ update: 0 }); } const updateCount = { update: 0 }; const relatedMap = new Map(relatedGames.map(item => [item.eventId, item])); games?.forEach(game => { const { eventId, evtime, events } = game; const relatedGame = relatedMap.get(eventId); if (relatedGame) { relatedGame.evtime = evtime; relatedGame.events = events; updateCount.update ++; } }); outrights?.forEach(outright => { const { parentId, sptime, special } = outright; const relatedGame = relatedMap.get(parentId); if (relatedGame) { relatedGame.sptime = sptime; relatedGame.special = special; updateCount.update ++; } }); resolve(updateCount); }); } /** * 获取比赛盘口 */ const getGamesEvents = ({ platform, relIds = [] } = {}) => { if (!relIds.length) { return null; } const idSet = new Set(relIds); const relations = { ...GAMES.Relations }; Object.keys(relations).forEach(id => { if (idSet.size && !idSet.has(+id)) { delete relations[id]; } }); if (platform) { return Object.values(relations).map(rel => rel[platform] ?? {}); } const gamesEvents = {}; Object.values(relations).forEach(({ rel }) => { Object.keys(rel).forEach(platform => { const game = rel[platform] ?? {}; const { eventId, events, special } = game; if (!gamesEvents[platform]) { gamesEvents[platform] = {}; } gamesEvents[platform][eventId] = { ...events, ...special }; }); }); return gamesEvents; } /** * 获取关联比赛 */ const fetchGamesRelation = async (mk='') => { return axios.get(`${BASE_URL}/getGameTast?mk=${mk}`) .then(res => { if (res.data.code == 0) { const now = Date.now(); const gamesRelation = res.data.data?.filter(item => { const timestamp = new Date(item.timestamp).getTime(); item.timestamp = timestamp; return timestamp > now; }).map(item => { const { id, mk, league_name, event_id: ps_event_id, league_id: ps_league_id, team_home_name: ps_team_home_name, team_away_name: ps_team_away_name, ob_event_id, ob_league_id, ob_team_home_name, ob_team_away_name, hg_event_id, hg_league_id, hg_team_home_name, hg_team_away_name, timestamp, } = item; const rel = { ps: { eventId: +ps_event_id, leagueId: +ps_league_id, leagueName: league_name, teamHomeName: ps_team_home_name, teamAwayName: ps_team_away_name, timestamp }, ob: ob_event_id ? { eventId: +ob_event_id, leagueId: +ob_league_id, leagueName: league_name, teamHomeName: ob_team_home_name, teamAwayName: ob_team_away_name, timestamp } : null, hg: hg_event_id ? { eventId: +hg_event_id, leagueId: +hg_league_id, leagueName: league_name, teamHomeName: hg_team_home_name, teamAwayName: hg_team_away_name, timestamp } : null }; return { id: ps_event_id, mk, rel }; }) ?? []; return gamesRelation; } return Promise.reject(new Error(res.data.message)); }); } const getGamesRelation = ({ mk, listEvents } = {}) => { const relations = Object.values(GAMES.Relations).filter(item => { if (typeof(mk) == 'undefined') { return true; } return item.mk == mk; }); if (listEvents) { return relations; } const gamesRelation = relations.map(item => { const { rel, ...relationInfo } = item; const tempRel = { ...rel }; Object.keys(tempRel).forEach(platform => { const { events, evtime, sptime, special, ...gameInfo } = tempRel[platform]; tempRel[platform] = gameInfo; }); return { ...relationInfo, rel: tempRel }; }); return gamesRelation; } /** * 定时更新关联比赛列表 */ const updateGamesRelation = () => { fetchGamesRelation() .then(res => { const gamesRelation = res.flat(); const updateCount = { add: 0, delete: 0 }; gamesRelation.forEach(item => { const { id } = item; if (!GAMES.Relations[id]) { GAMES.Relations[id] = item; updateCount.add ++; } }); const relations = new Set(gamesRelation.map(item => +item.id)); Object.keys(GAMES.Relations).forEach(id => { if (!relations.has(+id)) { delete GAMES.Relations[id]; updateCount.delete ++; } }); Logs.out('updateGamesRelation', updateCount); }) .catch(err => { Logs.out('updateGamesRelation', err.message); }) .finally(() => { setTimeout(updateGamesRelation, 60000); }); } updateGamesRelation(); /** * 同步比赛结果 */ const syncGamesResult = async (result) => { if (IS_DEV) { return Logs.out('updateGamesResult', result); } axios.post(`${BASE_URL}/syncMatchResult`, result) .then(res => { Logs.out('syncMatchResult', res.data); }) .catch(err => { Logs.out('syncMatchResult', err.message); }); } /** * 更新比赛结果 */ const updateGamesResult = (result) => { syncGamesResult(result); return Promise.resolve(); } /** * 同步中单方案 */ const syncSolutions = (solutions) => { if (IS_DEV) { return Logs.out('syncSolutions', solutions); } axios.post(`${BASE_URL}/syncDsOpportunity`, solutions) .then(res => { Logs.out('syncSolutions', res.data); }) .catch(err => { Logs.out('syncSolutions', err.message); }); } /** * 更新中单方案 */ const getCprKey = (cpr) => { const { k, p, v } = cpr; return `${k}_${p}_${v}`; } const compareCpr = (cpr1, cpr2) => { const key1 = getCprKey(cpr1); const key2 = getCprKey(cpr2); return key1 === key2; } const updateSolutions = (solutions) => { if (solutions?.length) { const solutionsHistory = GAMES.Solutions; const updateIds = { add: [], update: [] } solutions.forEach(item => { const { sid, cpr, sol: { win_average } } = item; if (!solutionsHistory[sid]) { solutionsHistory[sid] = item; updateIds.add.push(sid); return; } const historySolution = solutionsHistory[sid]; if (historySolution.sol.win_average !== win_average || !compareCpr(historySolution.cpr, cpr)) { solutionsHistory[sid] = item; updateIds.update.push(sid); return; } const { timestamp } = item; solutionsHistory[sid].timestamp = timestamp; }); if (updateIds.add.length || updateIds.update.length) { const solutionUpdate = {}; Object.keys(updateIds).forEach(key => { solutionUpdate[key] = updateIds[key].map(sid => solutionsHistory[sid]); }); syncSolutions(solutionUpdate); // Logs.outDev('solutions history update', solutionUpdate); } } } /** * 获取中单方案 */ const getSolutions = async () => { const solutionsList = Object.values(GAMES.Solutions); const relIds = solutionsList.map(item => item.info.id); const gamesEvents = getGamesEvents({ relIds }); const gamesRelation = getGamesRelation(); const relationsMap = new Map(gamesRelation.map(item => [item.id, item.rel])); const solutions = solutionsList.sort((a, b) => b.sol.win_average - a.sol.win_average).map(item => { const { info: { id } } = item; const relation = relationsMap.get(id); return { ...item, info: { id, ...relation } } }); return { solutions, gamesEvents }; } /** * 清理中单方案 */ const solutionsCleanup = () => { const solutionsHistory = GAMES.Solutions; const updateIds = { remove: [] } Object.keys(solutionsHistory).forEach(sid => { const { timestamp } = solutionsHistory[sid]; const nowTime = Date.now(); if (nowTime - timestamp > 1000*60) { delete solutionsHistory[sid]; updateIds.remove.push(sid); return; } const solution = solutionsHistory[sid]; const eventTime = solution.info.timestamp; if (nowTime > eventTime) { delete solutionsHistory[sid]; updateIds.remove.push(sid); } }); if (updateIds.remove.length) { syncSolutions(updateIds); } } /** * 定时清理中单方案 */ setInterval(() => { solutionsCleanup(); }, 1000*30); /** * 获取综合利润 */ const getTotalProfit = async (sid1, sid2, gold_side_inner) => { const preSolution = GAMES.Solutions[sid1]; const subSolution = GAMES.Solutions[sid2]; // const relId1 = preSolution?.info?.id; // const relId2 = subSolution?.info?.id; // const relIds = [relId1, relId2]; // const gamesEvents = getGamesEvents({ relIds }); // const gamesRelations = getGamesRelation(); // const relationsMap = new Map(gamesRelations.map(item => [item.id, item.rel])); // const preRelation = relationsMap.get(relId1); // const subRelation = relationsMap.get(relId2); // preSolution.info = { id: relId1, ...preRelation }; // subSolution.info = { id: relId2, ...subRelation }; const sol1 = preSolution?.sol; const sol2 = subSolution?.sol; if (!sol1 || !sol2) { return Promise.reject(new Error('SOLUTION_ID_INVALID')); } if (!gold_side_inner) { return Promise.reject(new Error('GOLD_SIDE_INNER_INVALID')); } const { innerRebateRatio: rebate_side_inner } = await getSetting(); const profit = calcTotalProfit(sol1, sol2, gold_side_inner, rebate_side_inner); return { profit, solutions: [preSolution, subSolution] }; } /** * 获取后台设置 */ const getSetting = async () => { return Setting.get(); } /** * 从子进程获取数据 */ const getDataFromChild = (type, callback) => { const id = ++Request.count; Request.callbacks[id] = callback; events_child.send({ method: 'get', id, type }); } /** * 处理子进程消息 */ events_child.on('message', async (message) => { const { callbacks } = Request; const { method, id, type, data } = message; if (method == 'get' && id) { let responseData = null; if (type == 'getGamesRelation') { responseData = getGamesRelation({ listEvents: true }); } else if (type == 'getSetting') { responseData = await getSetting(); } // else if (type == 'getSolutionHistory') { // responseData = getSolutionHistory(); // } events_child.send({ type: 'response', id, data: responseData }); } else if (method == 'post') { if (type == 'updateSolutions') { updateSolutions(data); } } else if (method == 'response' && id && callbacks[id]) { callbacks[id](data); delete callbacks[id]; } }); module.exports = { updateLeaguesList, getFilteredLeagues, updateGamesList, updateGamesEvents, getGamesRelation, updateGamesResult, getSolutions, getTotalProfit, }