trangleCalc.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. const Logs = require('../libs/logs');
  2. const IOR_KEYS_MAP = require('./iorKeys');
  3. const SETTING = {
  4. innerDefaultAmount: 10000,
  5. minProfitAmount: 0,
  6. innerRebateRatio: 0,
  7. obRebateRatio: 0,
  8. hgRebateRatio: 0,
  9. }
  10. /**
  11. * 筛选最优赔率
  12. */
  13. function getOptimalSelections(odds, rules) {
  14. const results = [];
  15. const { obRebateRatio, hgRebateRatio } = SETTING;
  16. const obRebate = 1 + obRebateRatio / 100;
  17. const hgRebate = 1 + hgRebateRatio / 100;
  18. rules.forEach((rule, index) => {
  19. let validOptions = [];
  20. for (let i = 0; i < 3; i++) {
  21. const innerIndex = i;
  22. const selection = [];
  23. let isValid = true;
  24. for (let j = 0; j < 3; j++) {
  25. const key = rule[j];
  26. const item = odds[key];
  27. if (!item) {
  28. isValid = false;
  29. break;
  30. }
  31. if (j === innerIndex) {
  32. if (!('ps' in item)) {
  33. isValid = false;
  34. break;
  35. }
  36. selection.push({
  37. k: key,
  38. p: 'ps',
  39. v: item.ps,
  40. o: item,
  41. });
  42. }
  43. else {
  44. const candidates = ['ob', 'hg'].filter((k) => k in item);
  45. if (candidates.length === 0) {
  46. isValid = false;
  47. break;
  48. }
  49. const best = candidates.reduce((a, b) => item[a]*obRebate > item[b]*hgRebate ? a : b);
  50. selection.push({
  51. k: key,
  52. p: best,
  53. v: item[best],
  54. o: item,
  55. });
  56. }
  57. }
  58. if (isValid) {
  59. validOptions.push(selection);
  60. }
  61. }
  62. if (validOptions.length > 0) {
  63. const iors = validOptions.reduce((a, b) => {
  64. const sumA = a.reduce((sum, x) => sum + x.v, 0);
  65. const sumB = b.reduce((sum, x) => sum + x.v, 0);
  66. return sumA > sumB ? a : b;
  67. });
  68. results.push({ rule, iors, index });
  69. }
  70. });
  71. return results;
  72. }
  73. /**
  74. * 精确浮点数字
  75. * @param {number} number
  76. * @param {number} x
  77. * @returns {number}
  78. */
  79. const fixFloat = (number, x=2) => {
  80. return parseFloat(number.toFixed(x));
  81. }
  82. /**
  83. * 计算盈利
  84. */
  85. const triangleProfitCalc = (goldsInfo, oddsOption) => {
  86. const { innerRebateRatio } = SETTING;
  87. const {
  88. gold_side_a: x,
  89. gold_side_b: y,
  90. gold_side_c: z,
  91. odds_side_a: a,
  92. odds_side_b: b,
  93. odds_side_c: c
  94. } = goldsInfo;
  95. const { crossType, innerIndex, rebateA: A = 0, rebateB: B = 0, rebateC: C = 0 } = oddsOption;
  96. /**
  97. * crossType:
  98. * la: 全输
  99. * wa: 全赢
  100. * lh: 半输
  101. * wh: 半赢
  102. * dr: 和局
  103. * la_wh_wa, la_dr_wa, la_lh_wa, lh_dr_wa, lh_lh_wa, la_la_wa
  104. */
  105. let inner_rebate = 0;
  106. if (innerIndex == 0) {
  107. inner_rebate = x * innerRebateRatio;
  108. }
  109. else if (innerIndex == 1) {
  110. inner_rebate = y * innerRebateRatio;
  111. }
  112. else if (innerIndex == 2) {
  113. inner_rebate = z * innerRebateRatio;
  114. }
  115. let win_side_a = 0, win_side_b = 0, win_side_c = 0;
  116. win_side_a = a*x - y - z + a*A*x + B*y + C*z;
  117. win_side_b = b*y - x - z + b*B*y + A*x + C*z;
  118. switch (crossType) {
  119. case 'la_wh_wa': // 全输 半赢 全赢
  120. win_side_c = c*z - x + b*y/2 + c*C*z + A*x + b*B*y/2;
  121. break;
  122. case 'la_dr_wa': // 全输 和局 全赢
  123. win_side_c = c*z - x + c*C*z + A*x;
  124. break;
  125. case 'la_lh_wa': // 全输 半输 全赢
  126. win_side_c = c*z - x - y/2 + c*C*z + A*x + B*y/2;
  127. break;
  128. case 'lh_dr_wa': // 半输 和局 全赢
  129. win_side_c = c*z - x/2 + c*C*z + A*x/2;
  130. break;
  131. case 'lh_lh_wa': // 半输 半输 全赢
  132. win_side_c = c*z - x/2 - y/2 + c*C*z + A*x/2 + B*y/2;
  133. break;
  134. case 'la_la_wa': // 全输 全输 全赢
  135. win_side_c = c*z - x - y + c*C*z + A*x + B*y;
  136. break;
  137. }
  138. win_side_a = fixFloat(win_side_a + inner_rebate);
  139. win_side_b = fixFloat(win_side_b + inner_rebate);
  140. win_side_c = fixFloat(win_side_c + inner_rebate);
  141. const win_average = fixFloat((win_side_a + win_side_b + win_side_c) / 3);
  142. return { win_side_a, win_side_b, win_side_c, win_average }
  143. }
  144. const triangleGoldCalc = (oddsInfo, oddsOption) => {
  145. const { innerDefaultAmount } = SETTING;
  146. const { odds_side_a: a, odds_side_b: b, odds_side_c: c } = oddsInfo;
  147. if (!a || !b || !c) {
  148. return;
  149. }
  150. const { crossType, innerIndex, rebateA: A = 0, rebateB: B = 0, rebateC: C = 0 } = oddsOption;
  151. let x = innerDefaultAmount;
  152. let y = (a + a*A + 1 - A) * x / (b + b*B + 1 - B);
  153. let z;
  154. switch (crossType) {
  155. case 'la_wh_wa': // 全输 半赢 全赢
  156. z = (b + b*B) * y / 2 / (c + c*C + 1 - C);
  157. break;
  158. case 'la_dr_wa': // 全输 和局 全赢
  159. z = (b + b*B) * y / (c + c*C + 1 - C);
  160. break;
  161. case 'la_lh_wa': // 全输 半输 全赢
  162. z = (2*b + 2*b*B + 1 - B) * y / 2 / (c + c*C + 1 - C);
  163. break;
  164. case 'lh_dr_wa': // 半输 和局 全赢
  165. z = ((b + b*B) * y - (1 - A) * x / 2) / (c + c*C + 1 - C);
  166. break;
  167. case 'lh_lh_wa': // 半输 半输 全赢
  168. z = ((2*b + 2*b*B + 1 - B) * y / 2 + (1 - A) * x / 2) / (c + c*C + 1 - C);
  169. break;
  170. case 'la_la_wa': // 全输 全输 全赢
  171. z = (b + b*B + 1 - B) * y / (c + c*C + 1 - C);
  172. break;
  173. default:
  174. z = 0;
  175. }
  176. if (innerIndex == 1) {
  177. const scale = innerDefaultAmount / y;
  178. x = x * scale;
  179. y = innerDefaultAmount;
  180. z = z * scale;
  181. }
  182. else if (innerIndex == 2) {
  183. const scale = innerDefaultAmount / z;
  184. x = x * scale;
  185. y = y * scale;
  186. z = innerDefaultAmount;
  187. }
  188. return {
  189. gold_side_a: x,
  190. gold_side_b: fixFloat(y),
  191. gold_side_c: fixFloat(z),
  192. odds_side_a: a,
  193. odds_side_b: b,
  194. odds_side_c: c,
  195. };
  196. }
  197. const eventSolutions = (oddsInfo, oddsOption) => {
  198. const { innerDefaultAmount } = SETTING;
  199. const goldsInfo = triangleGoldCalc(oddsInfo, oddsOption);
  200. if (!goldsInfo) {
  201. return;
  202. }
  203. const profitInfo = triangleProfitCalc(goldsInfo, oddsOption);
  204. const { odds_side_a, odds_side_b, odds_side_c } = goldsInfo;
  205. const { win_average } = profitInfo;
  206. return {
  207. odds_side_a,
  208. odds_side_b,
  209. odds_side_c,
  210. win_average,
  211. cross_type: oddsOption.crossType,
  212. inner_index: oddsOption.innerIndex,
  213. inner_base: innerDefaultAmount,
  214. }
  215. }
  216. /**
  217. * 将数组中的指定索引的元素移动到最前面
  218. */
  219. const moveToFront = (arr, index) => {
  220. if (index < 0 || index >= arr.length) return arr; // index 越界处理
  221. const item = arr.splice(index, 1)[0]; // 删除该项并获取
  222. arr.unshift(item); // 插入到最前面
  223. return arr;
  224. }
  225. /**
  226. * 添加返佣
  227. */
  228. const attachRebate = (ior) => {
  229. const { innerRebateRatio, obRebateRatio, hgRebateRatio } = SETTING;
  230. const { p } = ior;
  231. let rebate = 0;
  232. if (p == 'ps') {
  233. rebate = innerRebateRatio;
  234. }
  235. else if (p == 'ob') {
  236. rebate = obRebateRatio;
  237. }
  238. else if (p == 'hg') {
  239. rebate = hgRebateRatio;
  240. }
  241. return { ...ior, r: rebate };
  242. }
  243. const eventsCombination = (passableEvents, setting) => {
  244. Object.keys(setting).forEach(key => {
  245. if (key in SETTING && SETTING[key] !== setting[key]) {
  246. SETTING[key] = setting[key];
  247. Logs.out(`setting ${key} changed to ${setting[key]}`);
  248. }
  249. });
  250. const solutions = {};
  251. passableEvents.forEach(events => {
  252. const { odds, info } = events;
  253. Object.keys(IOR_KEYS_MAP).forEach(iorGroup => {
  254. const rules = IOR_KEYS_MAP[iorGroup];
  255. const optimalSelections = getOptimalSelections(odds, rules);
  256. optimalSelections.forEach(selection => {
  257. const { rule, iors, index } = selection;
  258. const [, , , crossType] = rule;
  259. const oddsSideA = attachRebate(iors[0]);
  260. const oddsSideB = attachRebate(iors[1]);
  261. const oddsSideC = attachRebate(iors[2]);
  262. const innerIndex = iors.findIndex(item => item.p == 'ps');
  263. if (!oddsSideA || !oddsSideB || !oddsSideC) {
  264. return;
  265. }
  266. const cpr = [ oddsSideA, oddsSideB, oddsSideC ];
  267. const oddsInfo = {
  268. odds_side_a: fixFloat(oddsSideA.v - 1),
  269. odds_side_b: fixFloat(oddsSideB.v - 1),
  270. odds_side_c: fixFloat(oddsSideC.v - 1),
  271. };
  272. const oddsOption = {
  273. crossType, innerIndex,
  274. rebateA: parseFloat((oddsSideA.r / 100).toFixed(4)),
  275. rebateB: parseFloat((oddsSideB.r / 100).toFixed(4)),
  276. rebateC: parseFloat((oddsSideC.r / 100).toFixed(4)),
  277. };
  278. const sol = eventSolutions(oddsInfo, oddsOption);
  279. if (sol?.win_average > SETTING.minProfitAmount) {
  280. const id = info.id;
  281. const sortedCpr = moveToFront([...cpr], innerIndex);
  282. const crpGroup = `${id}_${sortedCpr[0].k}`;
  283. const keys = sortedCpr.map(item => `${item.k}`).join('_');
  284. const sid = `${id}_${keys}`;
  285. const timestamp = Date.now();
  286. if (!solutions[crpGroup]) {
  287. solutions[crpGroup] = [];
  288. }
  289. solutions[crpGroup].push({sid, sol, cpr, info, rule: `${iorGroup}:${index}`, timestamp});
  290. }
  291. });
  292. });
  293. });
  294. return Object.values(solutions).map(item => {
  295. return item.sort((a, b) => {
  296. return b.sol.win_average - a.sol.win_average;
  297. })
  298. .slice(0, 2);
  299. })
  300. .sort((a, b) => {
  301. return b[0].sol.win_average - a[0].sol.win_average;
  302. });
  303. }
  304. module.exports = { eventsCombination };