const crypto = require('crypto'); const IOR_KEYS_MAP = require('./iorKeys'); const { getSetting } = require('./settings'); const { eventSolutions } = require('./eventSolutions'); const cartesianOdds = (selection) => { const [a, b, c] = selection; return a.flatMap(itemA => b.flatMap(itemB => c.map(itemC => [itemA, itemB, itemC]))); } const getOptimalSelections = (odds, rules) => { const results = []; rules.forEach((rule, ruleIndex) => { let validOptions = []; for (let i = 0; i < 3; i++) { const innerIndex = i; const selection = []; let isValid = true; for (let j = 0; j < 3; j++) { const key = rule[j]; let item = odds[key]; if (key == '-') { item = { no: { v: 1 } }; } if (!item) { isValid = false; break; } if (j === innerIndex) { if (!('ps' in item)) { isValid = false; break; } selection.push([{ k: key, p: 'ps', v: item.ps.v, r: item.ps.r, s: item.ps.s, o: item }]); } else { const { ps, ...itemRest } = item; const candidates = Object.keys(itemRest); if (candidates.length === 0) { isValid = false; break; } selection.push(candidates.map(k => ({ k: key, p: k, v: item[k].v, r: item[k].r, s: item[k].s, q: item[k].q, o: item }))); } } if (isValid) { const cartesian = cartesianOdds(selection); cartesian.forEach(item => { validOptions.push(item); }); } } validOptions.forEach(iors => { results.push({ rule, iors, ruleIndex }); }); }); return results; } /** * 精确浮点数字 * @param {number} number * @param {number} x * @returns {number} */ const fixFloat = (number, x=2) => { return parseFloat(number.toFixed(x)); } /** * 盘口排序 */ const priority = { ps: 1, ob: 2, hg: 3, pc: 4 }; const sortCpr = (cpr) => { const temp = [...cpr]; temp.sort((a, b) => priority[a.p] - priority[b.p]); return temp; } /** * 获取平台类型 */ const getPlatformKey = (cpr) => { const platforms = sortCpr(cpr).map(item => item.p); return [...new Set(platforms)].join('_'); } /** * 添加返佣 */ const attachRebate = (ior) => { const { obRebateRatio, obRebateType, hgRebateRatio, hgRebateLower, hgRebateType, pcRebateRatio, pcRebateType } = getSetting(); const { p, k } = ior; let rebate = 0, rebateType = 0; if (p == 'ps') { rebate = 0; rebateType = -1; } else if (p == 'pc') { rebate = pcRebateRatio; rebateType = pcRebateType; } else if (p == 'ob') { rebate = obRebateRatio; rebateType = obRebateType; } else if (p == 'hg') { rebate = hgRebateRatio; rebateType = hgRebateType; if (k.startsWith('ior_m')) { rebate = hgRebateLower; } } return { ...ior, b: rebate, t: rebateType }; } /** * 提取盘口数据 */ const extractOdds = ({ evtime, events, sptime, special }) => { const { expireTimeEvents, expireTimeSpecial } = getSetting(); const nowTime = Date.now(); const expireTimeEv = nowTime - expireTimeEvents; const expireTimeSP = nowTime - expireTimeSpecial; const extractData = { odds: null, evExpire: false, spExpire: false, removeCount: 0, } let odds = {}; if (evtime > expireTimeEv) { odds = { ...odds, ...events }; } else if (events) { extractData.evExpire = true; } if (sptime > expireTimeSP) { odds = { ...odds, ...special }; } else if (special) { extractData.spExpire = true; } Object.keys(odds).forEach(ior => { if (!odds[ior]?.v || odds[ior].v <= 0) { delete odds[ior]; extractData.removeCount++; } }); extractData.odds = odds; return extractData; } /** * 筛选有效盘口 */ const getPassableEvents = (relations, eventsLogsMap) => { return relations.map(({ id, rel, mk }) => { const eventsMap = {}; const oddsMap = {}; Object.keys(rel).forEach(platform => { const { leagueName, teamHomeName, teamAwayName, timestamp, stage, score, retime, evtime, events, sptime, special } = rel[platform] ?? {}; if (!events && !special) { return; } if (platform == 'ps') { eventsMap.info = { leagueName, teamHomeName, teamAwayName, id, timestamp, stage, score, retime }; } const { odds, evExpire, spExpire, removeCount } = extractOdds({ evtime, events, sptime, special }); /** 日志 盘口过期 */ if (eventsLogsMap && (evExpire || spExpire)) { eventsLogsMap.expireEvents?.push({ mk, platform, info: rel[platform], evExpire, spExpire, evtime, sptime, }); } /** 日志 盘口移除 */ if (eventsLogsMap && removeCount) { eventsLogsMap.removeEvents?.push({ mk, platform, info: rel[platform], removeCount, }); } /** 日志 End */ Object.keys(odds).forEach(ior => { if (!oddsMap[ior]) { oddsMap[ior] = {}; } oddsMap[ior][platform] = odds[ior]; }); }); eventsMap.odds = oddsMap; return eventsMap; }) .filter(item => item?.info); } /** * 盘口组合计算 */ const eventsCombination = (passableEvents, innerBase, innerRebate) => { const { innerDefaultAmount, innerRebateRatio } = getSetting(); const solutions = []; passableEvents.forEach(events => { const { odds, info } = events; Object.keys(IOR_KEYS_MAP).forEach(iorGroup => { const rules = IOR_KEYS_MAP[iorGroup]; const optimalSelections = getOptimalSelections(odds, rules); optimalSelections.forEach(selection => { const { rule, iors, ruleIndex } = selection; const [, , , crossType] = rule; const oddsSideA = attachRebate(iors[0]); const oddsSideB = attachRebate(iors[1]); const oddsSideC = attachRebate(iors[2]); const innerIndex = iors.findIndex(item => item.p == 'ps'); if (!oddsSideA || !oddsSideB || !oddsSideC) { return; } const cpr = [ oddsSideA, oddsSideB, oddsSideC ]; const betInfo = { cross_type: crossType, inner_index: innerIndex, inner_base: innerBase ?? innerDefaultAmount, inner_rebate: innerRebate ?? fixFloat(innerRebateRatio / 100, 3), odds_side_a: fixFloat(oddsSideA.v - 1), odds_side_b: fixFloat(oddsSideB.v - 1), odds_side_c: fixFloat(oddsSideC.v - 1), rebate_side_a: parseFloat((oddsSideA.b / 100).toFixed(4)), rebate_type_side_a: oddsSideA.t, rebate_side_b: parseFloat((oddsSideB.b / 100).toFixed(4)), rebate_type_side_b: oddsSideB.t, rebate_side_c: parseFloat((oddsSideC.b / 100).toFixed(4)), rebate_type_side_c: oddsSideC.t, }; const sol = eventSolutions(betInfo, true); if (cpr[2].k == '-') { cpr.pop(); } if (!isNaN(sol?.win_average)) { const id = info.id; const keys = cpr.map(item => `${item.k}-${item.p}`).join('##'); const sid = crypto.createHash('sha1').update(`${id}-${keys}`).digest('hex'); const crpGroup = `${id}_${cpr.find(item => item.p == 'ps').k}`; const hasLower = cpr.some(item => item.q === 0); const platformKey = getPlatformKey(cpr); const timestamp = Date.now(); solutions.push({sid, sol, cpr, cross: platformKey, info, group: crpGroup, lower: hasLower, rule: `${iorGroup}:${ruleIndex}`, timestamp}); } }); }); }); return solutions.sort((a, b) => { return b.sol.win_average - a.sol.win_average; }); } module.exports = { eventsCombination, getPassableEvents };