const Logs = require('../libs/logs'); const IOR_KEYS_MAP = require('./iorKeys'); const SETTING = { innerDefaultAmount: 10000, minProfitAmount: 0, innerRebateRatio: 0, obRebateRatio: 0, hgRebateRatio: 0, } /** * 筛选最优赔率 */ function getOptimalSelections(odds, rules) { const results = []; const { obRebateRatio, hgRebateRatio } = SETTING; const obRebate = 1 + obRebateRatio / 100; const hgRebate = 1 + hgRebateRatio / 100; rules.forEach((rule, index) => { 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]; const item = odds[key]; if (!item) { isValid = false; break; } if (j === innerIndex) { if (!('ps' in item)) { isValid = false; break; } selection.push({ k: key, p: 'ps', v: item.ps, o: item, }); } else { const candidates = ['ob', 'hg'].filter((k) => k in item); if (candidates.length === 0) { isValid = false; break; } const best = candidates.reduce((a, b) => item[a]*obRebate > item[b]*hgRebate ? a : b); selection.push({ k: key, p: best, v: item[best], o: item, }); } } if (isValid) { validOptions.push(selection); } } if (validOptions.length > 0) { const iors = validOptions.reduce((a, b) => { const sumA = a.reduce((sum, x) => sum + x.v, 0); const sumB = b.reduce((sum, x) => sum + x.v, 0); return sumA > sumB ? a : b; }); results.push({ rule, iors, index }); } }); return results; } /** * 精确浮点数字 * @param {number} number * @param {number} x * @returns {number} */ const fixFloat = (number, x=2) => { return parseFloat(number.toFixed(x)); } /** * 计算盈利 */ const triangleProfitCalc = (goldsInfo, oddsOption) => { const { innerRebateRatio } = SETTING; const { gold_side_a: x, gold_side_b: y, gold_side_c: z, odds_side_a: a, odds_side_b: b, odds_side_c: c } = goldsInfo; const { crossType, innerIndex, rebateA: A = 0, rebateB: B = 0, rebateC: C = 0 } = oddsOption; /** * crossType: * la: 全输 * wa: 全赢 * lh: 半输 * wh: 半赢 * dr: 和局 * la_wh_wa, la_dr_wa, la_lh_wa, lh_dr_wa, lh_lh_wa, la_la_wa */ let inner_rebate = 0; if (innerIndex == 0) { inner_rebate = x * innerRebateRatio; } else if (innerIndex == 1) { inner_rebate = y * innerRebateRatio; } else if (innerIndex == 2) { inner_rebate = z * innerRebateRatio; } let win_side_a = 0, win_side_b = 0, win_side_c = 0; win_side_a = a*x - y - z + a*A*x + B*y + C*z; win_side_b = b*y - x - z + b*B*y + A*x + C*z; switch (crossType) { case 'la_wh_wa': // 全输 半赢 全赢 win_side_c = c*z - x + b*y/2 + c*C*z + A*x + b*B*y/2; break; case 'la_dr_wa': // 全输 和局 全赢 win_side_c = c*z - x + c*C*z + A*x; break; case 'la_lh_wa': // 全输 半输 全赢 win_side_c = c*z - x - y/2 + c*C*z + A*x + B*y/2; break; case 'lh_dr_wa': // 半输 和局 全赢 win_side_c = c*z - x/2 + c*C*z + A*x/2; break; case 'lh_lh_wa': // 半输 半输 全赢 win_side_c = c*z - x/2 - y/2 + c*C*z + A*x/2 + B*y/2; break; case 'la_la_wa': // 全输 全输 全赢 win_side_c = c*z - x - y + c*C*z + A*x + B*y; break; } win_side_a = fixFloat(win_side_a + inner_rebate); win_side_b = fixFloat(win_side_b + inner_rebate); win_side_c = fixFloat(win_side_c + inner_rebate); const win_average = fixFloat((win_side_a + win_side_b + win_side_c) / 3); return { win_side_a, win_side_b, win_side_c, win_average } } const triangleGoldCalc = (oddsInfo, oddsOption) => { const { innerDefaultAmount } = SETTING; const { odds_side_a: a, odds_side_b: b, odds_side_c: c } = oddsInfo; if (!a || !b || !c) { return; } const { crossType, innerIndex, rebateA: A = 0, rebateB: B = 0, rebateC: C = 0 } = oddsOption; let x = innerDefaultAmount; let y = (a + a*A + 1 - A) * x / (b + b*B + 1 - B); let z; switch (crossType) { case 'la_wh_wa': // 全输 半赢 全赢 z = (b + b*B) * y / 2 / (c + c*C + 1 - C); break; case 'la_dr_wa': // 全输 和局 全赢 z = (b + b*B) * y / (c + c*C + 1 - C); break; case 'la_lh_wa': // 全输 半输 全赢 z = (2*b + 2*b*B + 1 - B) * y / 2 / (c + c*C + 1 - C); break; case 'lh_dr_wa': // 半输 和局 全赢 z = ((b + b*B) * y - (1 - A) * x / 2) / (c + c*C + 1 - C); break; case 'lh_lh_wa': // 半输 半输 全赢 z = ((2*b + 2*b*B + 1 - B) * y / 2 + (1 - A) * x / 2) / (c + c*C + 1 - C); break; case 'la_la_wa': // 全输 全输 全赢 z = (b + b*B + 1 - B) * y / (c + c*C + 1 - C); break; default: z = 0; } if (innerIndex == 1) { const scale = innerDefaultAmount / y; x = x * scale; y = innerDefaultAmount; z = z * scale; } else if (innerIndex == 2) { const scale = innerDefaultAmount / z; x = x * scale; y = y * scale; z = innerDefaultAmount; } return { gold_side_a: x, gold_side_b: fixFloat(y), gold_side_c: fixFloat(z), odds_side_a: a, odds_side_b: b, odds_side_c: c, }; } const eventSolutions = (oddsInfo, oddsOption) => { const { innerDefaultAmount } = SETTING; const goldsInfo = triangleGoldCalc(oddsInfo, oddsOption); if (!goldsInfo) { return; } const profitInfo = triangleProfitCalc(goldsInfo, oddsOption); const { odds_side_a, odds_side_b, odds_side_c } = goldsInfo; const { win_average } = profitInfo; return { odds_side_a, odds_side_b, odds_side_c, win_average, cross_type: oddsOption.crossType, inner_index: oddsOption.innerIndex, inner_base: innerDefaultAmount, } } /** * 将数组中的指定索引的元素移动到最前面 */ const moveToFront = (arr, index) => { if (index < 0 || index >= arr.length) return arr; // index 越界处理 const item = arr.splice(index, 1)[0]; // 删除该项并获取 arr.unshift(item); // 插入到最前面 return arr; } /** * 添加返佣 */ const attachRebate = (ior) => { const { innerRebateRatio, obRebateRatio, hgRebateRatio } = SETTING; const { p } = ior; let rebate = 0; if (p == 'ps') { rebate = innerRebateRatio; } else if (p == 'ob') { rebate = obRebateRatio; } else if (p == 'hg') { rebate = hgRebateRatio; } return { ...ior, r: rebate }; } const eventsCombination = (passableEvents, setting) => { Object.keys(setting).forEach(key => { if (key in SETTING && SETTING[key] !== setting[key]) { SETTING[key] = setting[key]; Logs.out(`setting ${key} changed to ${setting[key]}`); } }); 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, index } = 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 oddsInfo = { odds_side_a: fixFloat(oddsSideA.v - 1), odds_side_b: fixFloat(oddsSideB.v - 1), odds_side_c: fixFloat(oddsSideC.v - 1), }; const oddsOption = { crossType, innerIndex, rebateA: parseFloat((oddsSideA.r / 100).toFixed(4)), rebateB: parseFloat((oddsSideB.r / 100).toFixed(4)), rebateC: parseFloat((oddsSideC.r / 100).toFixed(4)), }; const sol = eventSolutions(oddsInfo, oddsOption); if (sol?.win_average > SETTING.minProfitAmount) { const id = info.id; const sortedCpr = moveToFront([...cpr], innerIndex); const crpGroup = `${id}_${sortedCpr[0].k}`; const keys = sortedCpr.map(item => `${item.k}`).join('_'); const sid = `${id}_${keys}`; const timestamp = Date.now(); if (!solutions[crpGroup]) { solutions[crpGroup] = []; } solutions[crpGroup].push({sid, sol, cpr, info, rule: `${iorGroup}:${index}`, timestamp}); } }); }); }); return Object.values(solutions).map(item => { return item.sort((a, b) => { return b.sol.win_average - a.sol.win_average; }) .slice(0, 2); }) .sort((a, b) => { return b[0].sol.win_average - a[0].sol.win_average; }); } module.exports = { eventsCombination };