ソースを参照

新增过关后金额重新计算

flyzto 5 ヶ月 前
コミット
aef7d06660

+ 51 - 9
server/models/GamesPs.js

@@ -1,10 +1,12 @@
 const axios = require('axios');
 const Logs = require('../libs/logs');
 const Setting = require('./Setting');
-const calcTotalProfit = require('../triangle/totalProfitCalc');
+const { eventSolutions } = require('../triangle/eventSolutions');
+const { calcTotalProfit, calcTotalProfitWithFixedFirst } = require('../triangle/totalProfitCalc');
 
 const childOptions = process.env.NODE_ENV == 'development' ? {
-  execArgv: ['--inspect=9228']
+  execArgv: ['--inspect=9228'],
+  stdio: ['pipe', 'pipe', 'pipe', 'ipc']
 } : {};
 const { fork } = require('child_process');
 const events_child = fork('./triangle/eventsMatch.js', [], childOptions);
@@ -613,7 +615,18 @@ setInterval(() => {
 /**
  * 获取综合利润
  */
-const getTotalProfit = async (sid1, sid2, gold_side_inner) => {
+const getTotalProfit = async (sol1, sol2, inner_base, inner_rebate) => {
+  const { innerDefaultAmount, innerRebateRatio } = await getSetting();
+  inner_base = inner_base ? +inner_base : innerDefaultAmount;
+  inner_rebate = inner_rebate ? +inner_rebate : fixFloat(innerRebateRatio / 100, 3);
+  const profit = calcTotalProfit(sol1, sol2, inner_base, inner_rebate);
+  return profit;
+}
+
+/**
+ * 通过 sid 获取综合利润
+ */
+const getTotalProfitWithSid = async (sid1, sid2, inner_base, inner_rebate) => {
   const preSolution = GAMES.Solutions[sid1];
   const subSolution = GAMES.Solutions[sid2];
   const sol1 = preSolution?.sol;
@@ -621,14 +634,27 @@ const getTotalProfit = async (sid1, sid2, gold_side_inner) => {
   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);
+  const profit = await getTotalProfit(sol1, sol2, inner_base, inner_rebate);
   return { profit, solutions: [preSolution, subSolution] };
 }
 
+/**
+ * 通过盘口信息获取综合利润
+ */
+const getTotalProfitWithBetInfo = async (betInfo1, betInfo2, fixed=false, inner_base, inner_rebate) => {
+  const { innerDefaultAmount, innerRebateRatio } = await getSetting();
+  inner_base = inner_base ? +inner_base : innerDefaultAmount;
+  inner_rebate = inner_rebate ? +inner_rebate : fixFloat(innerRebateRatio / 100, 3);
+
+  if (fixed) {
+    return calcTotalProfitWithFixedFirst(betInfo1, betInfo2, inner_base, inner_rebate);
+  }
+
+  const [sol1, sol2] = [betInfo1, betInfo2].map(betinfo => eventSolutions({...betinfo, inner_base, inner_rebate }));
+  return getTotalProfit(sol1, sol2, inner_base, inner_rebate);
+}
+
+
 /**
  * 获取后台设置
  */
@@ -645,6 +671,13 @@ const getDataFromChild = (type, callback) => {
   events_child.send({ method: 'get', id, type });
 }
 
+/**
+ * 向子进程发送数据
+ */
+const postDataToChild = (type, data) => {
+  events_child.send({ method: 'post', type, data });
+}
+
 /**
  * 处理子进程消息
  */
@@ -675,11 +708,20 @@ events_child.on('message', async (message) => {
   }
 });
 
+events_child.stderr.on('data', data => {
+  Logs.out('events_child stderr', data.toString());
+});
+
+Setting.onUpdate(fields => {
+  postDataToChild('updateSetting', fields);
+});
+
 module.exports = {
   updateLeaguesList, getFilteredLeagues,
   updateGamesList, updateGamesEvents,
   getGamesRelation,
   updateGamesResult,
   getSolutions,
-  getTotalProfit,
+  getTotalProfitWithSid,
+  getTotalProfitWithBetInfo,
 }

+ 9 - 1
server/models/Setting.js

@@ -52,12 +52,20 @@ const systemSettingSchema = new Schema({
 });
 
 const Setting = mongoose.model('SystemSetting', systemSettingSchema);
+const CALLBACKS = {
+  update: null,
+};
+
+const onUpdate = (callback) => {
+  CALLBACKS.update = callback;
+}
 
 const get = async () => {
   return await Setting.findById('system');
 }
 
 const update = async (fields) => {
+  CALLBACKS.update?.(fields);
   return await Setting.findByIdAndUpdate(
     'system',
     { $set: fields },
@@ -77,4 +85,4 @@ const init = async (fields = {}) => {
   return setting.save();
 }
 
-module.exports = { get, update, init };
+module.exports = { get, update, init, onUpdate };

+ 14 - 2
server/routes/pstery.js

@@ -78,8 +78,20 @@ router.get('/get_solutions', (req, res) => {
 
 // 获取综合利润方案
 router.post('/calc_total_profit', (req, res) => {
-  const [sid1, sid2, gold_side_inner] = req.body;
-  Games.getTotalProfit(sid1, sid2, gold_side_inner)
+  const [sid1, sid2, inner_base, inner_rebate] = req.body;
+  Games.getTotalProfitWithSid(sid1, sid2, inner_base, inner_rebate)
+  .then(totalProfit => {
+    res.sendSuccess(totalProfit);
+  })
+  .catch(err => {
+    res.badRequest(err.message);
+  });
+});
+
+// 获取自定义综合利润
+router.post('/calc_custom_total_profit', (req, res) => {
+  const [betInfo1, betInfo2, fixed, inner_base, inner_rebate] = req.body;
+  Games.getTotalProfitWithBetInfo(betInfo1, betInfo2, fixed, inner_base, inner_rebate)
   .then(totalProfit => {
     res.sendSuccess(totalProfit);
   })

+ 195 - 0
server/triangle/eventSolutions.js

@@ -0,0 +1,195 @@
+const Logs = require('../libs/logs');
+
+/**
+ * 精确浮点数字
+ * @param {number} number
+ * @param {number} x
+ * @returns {number}
+ */
+const fixFloat = (number, x=2) => {
+  return parseFloat(number.toFixed(x));
+}
+
+/**
+ * 计算盈利
+ */
+const triangleProfitCalc = (betInfo) => {
+  const {
+    cross_type,
+    inner_index,
+    inner_rebate,
+    gold_side_a: x,
+    gold_side_b: y,
+    gold_side_c: z,
+    odds_side_a: a,
+    odds_side_b: b,
+    odds_side_c: c,
+    rebate_side_a: A = 0,
+    rebate_side_b: B = 0,
+    rebate_side_c: C = 0,
+  } = betInfo;
+
+  /**
+   * cross_type:
+   *  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_value = 0;
+  if (inner_index == 0) {
+    inner_rebate_value = x * inner_rebate;
+  }
+  else if (inner_index == 1) {
+    inner_rebate_value = y * inner_rebate;
+  }
+  else if (inner_index == 2) {
+    inner_rebate_value = z * inner_rebate;
+  }
+
+  const k1 = a * (1 + A);
+  const k2 = 1 - A;
+  const k3 = b * (1 + B);
+  const k4 = 1 - B;
+  const k5 = c * (1 + C);
+  const k6 = 1 - C;
+
+  let win_side_a = 0, win_side_b = 0, win_side_c = 0;
+  win_side_a = k1*x - k4*y - k6*z;
+  win_side_b = k3*y - k2*x - k6*z;
+
+  switch (cross_type) {
+    case 'la_wh_wa': // 全输 半赢 全赢
+      win_side_c = k5*z - k2*x + k3*y/2;
+      break;
+    case 'la_dr_wa': // 全输 和局 全赢
+      win_side_c = k5*z - k2*x;
+      break;
+    case 'la_lh_wa': // 全输 半输 全赢
+      win_side_c = k5*z - k2*x - k4*y/2;
+      break;
+    case 'lh_dr_wa': // 半输 和局 全赢
+      win_side_c = k5*z - k2*x/2;
+      break;
+    case 'lh_lh_wa': // 半输 半输 全赢
+      win_side_c = k5*z - k2*x/2 - k4*y/2;
+      break;
+    case 'la_la_wa': // 全输 全输 全赢
+      win_side_c = k5*z - k2*x - k4*y;
+      break;
+  }
+
+  win_side_a = fixFloat(win_side_a + inner_rebate_value);
+  win_side_b = fixFloat(win_side_b + inner_rebate_value);
+  win_side_c = fixFloat(win_side_c + inner_rebate_value);
+  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 = (betInfo) => {
+  const {
+    cross_type, inner_index, inner_base,
+    odds_side_a: a,
+    odds_side_b: b,
+    odds_side_c: c,
+    rebate_side_a: A = 0,
+    rebate_side_b: B = 0,
+    rebate_side_c: C = 0,
+  } = betInfo;
+  if (!a || !b || !c) {
+    return;
+  }
+  const k1 = a * (1 + A);
+  const k2 = 1 - A;
+  const k3 = b * (1 + B);
+  const k4 = 1 - B;
+  const k5 = c * (1 + C);
+  const k6 = 1 - C;
+  let x = inner_base;
+  let y = (k1 + k2) * x / (k3 + k4);
+  let z;
+  switch (cross_type) {
+    case 'la_wh_wa': // 全输 半赢 全赢
+      z = k3 * y / 2 / (k5 + k6);
+      break;
+    case 'la_dr_wa': // 全输 和局 全赢
+      z = k3 * y / (k5 + k6);
+      break;
+    case 'la_lh_wa': // 全输 半输 全赢
+      z = (k3 + k4 / 2) * y / (k5 + k6);
+      break;
+    case 'lh_dr_wa': // 半输 和局 全赢
+      z = (k3 * y - k2 * x / 2) / (k5 + k6);
+      break;
+    case 'lh_lh_wa': // 半输 半输 全赢
+      z = ((k3 + k4/2) * y - k2 * x / 2) / (k5 + k6);
+      break;
+    case 'la_la_wa': // 全输 全输 全赢
+      z = (k3 + k4) * y / (k5 + k6);
+      break;
+    default:
+      z = 0;
+  }
+
+  if (inner_index == 1) {
+    const scale = inner_base / y;
+    x = x * scale;
+    y = inner_base;
+    z = z * scale;
+  }
+  else if (inner_index == 2) {
+    const scale = inner_base / z;
+    x = x * scale;
+    y = y * scale;
+    z = inner_base;
+  }
+
+  return {
+    gold_side_a: fixFloat(x),
+    gold_side_b: fixFloat(y),
+    gold_side_c: fixFloat(z),
+  };
+
+}
+
+const eventSolutions = (betInfo, showGolds=false) => {
+  const goldsInfo = triangleGoldCalc(betInfo);
+  if (!goldsInfo) {
+    return;
+  }
+  const profitInfo = triangleProfitCalc({ ...betInfo, ...goldsInfo }, showGolds);
+
+  const { gold_side_a, gold_side_b, gold_side_c } = goldsInfo;
+  const { win_side_a, win_side_b, win_side_c, win_average } = profitInfo;
+
+  const {
+    cross_type, inner_index, inner_rebate, inner_base,
+    odds_side_a, odds_side_b, odds_side_c,
+    rebate_side_a,rebate_side_b, rebate_side_c,
+  } = betInfo;
+
+
+  // const win_profit = fixFloat(win_average / (gold_side_a + gold_side_b + gold_side_c) * 100);
+
+  let result = {
+    odds_side_a, odds_side_b, odds_side_c,
+    rebate_side_a, rebate_side_b, rebate_side_c,
+    win_average, cross_type,
+    inner_index, inner_base, inner_rebate,
+  }
+
+  if (showGolds) {
+    result = {
+      ...result,
+      gold_side_a, gold_side_b, gold_side_c,
+      // win_side_a, win_side_b, win_side_c,
+    }
+  }
+  return result;
+}
+
+module.exports = { eventSolutions };

+ 15 - 75
server/triangle/eventsMatch.js

@@ -1,23 +1,12 @@
 const Logs = require('../libs/logs');
 const { eventsCombination } = require('./trangleCalc');
+const { getSetting, updateSetting } = require('./settings');
 
 const Request = {
   callbacks: {},
   count: 0,
 }
 
-// const WIN_STEP = 15;
-// const SOL_FREEZ_TIME = 1000 * 30;
-
-const SETTING = {
-  innerDefaultAmount: 10000,
-  minProfitAmount: 0,
-  innerRebateRatio: 0,
-  obRebateRatio: 0,
-  hgRebateRatio: 0,
-  runWorkerEnabled: false
-}
-
 const GLOBAL_DATA = {
   relationLength: 0,
   loopStatus: -1,
@@ -43,6 +32,11 @@ process.on('message', (message) => {
     // }
     process.send({ type: 'response', id, data: responseData });
   }
+  else if (method == 'post') {
+    if (type == 'updateSetting') {
+      updateSetting(data);
+    }
+  }
   else if (type == 'response' && id && callbacks[id]) {
     callbacks[id](data);
     delete callbacks[id];
@@ -59,22 +53,15 @@ const fixFloat = (number, x=2) => {
   return parseFloat(number.toFixed(x));
 }
 
-const getSetting = () => {
-  return new Promise(resolve => {
-    getDataFromParent('getSetting', (setting) => {
-      resolve(setting);
-    });
-  });
-}
-
 const getGamesRelation = () => {
-  const status = +SETTING.runWorkerEnabled;
+  const setting = getSetting();
+  const status = +setting.runWorkerEnabled;
   if (GLOBAL_DATA.loopStatus !== status) {
     GLOBAL_DATA.loopStatus = status;
     Logs.out('loop status changed to', status);
   }
 
-  if (!SETTING.runWorkerEnabled) {
+  if (!setting.runWorkerEnabled) {
     return Promise.resolve([]);
   }
   return new Promise(resolve => {
@@ -84,14 +71,6 @@ const getGamesRelation = () => {
   });
 }
 
-// const getSolutionHistory = () => {
-//   return new Promise(resolve => {
-//     getDataFromParent('getSolutionHistory', (solutions) => {
-//       resolve(solutions);
-//     });
-//   });
-// }
-
 const updateSolutions = (solutions) => {
   postDataToParent('updateSolutions', solutions);
 }
@@ -108,41 +87,9 @@ const extractOdds = ({ evtime, events, sptime, special }) => {
   return odds;
 }
 
-// const getOptimalOdds = (oddsMap) => {
-//   const oddsInfo = {};
-//   Object.keys(oddsMap).forEach(platform => {
-//     const odds = oddsMap[platform];
-//     Object.keys(odds).forEach(ior => {
-//       const oddsValue = odds[ior];
-//       if (!oddsInfo[ior] || oddsInfo[ior]?.v < oddsValue) {
-//         oddsInfo[ior] = {
-//           p: platform,
-//           v: oddsValue
-//         }
-//       }
-//     });
-//   });
-//   return oddsInfo;
-// }
-
 const eventMatch = () => {
   getGamesRelation()
   .then(relations => {
-    // Logs.out('eventMatch', relations);
-    // if (!relations?.length) {
-    //   return;
-    // }
-
-    // const nowTime = Date.now();
-    // relations = relations.filter(relaiton => {
-    //   const expire = Object.values(relaiton.rel).find(event => event.timestamp <= nowTime);
-    //   if (expire) {
-    //     return false;
-    //   }
-    //   return true;
-    // });
-
-    // Logs.out('eventMatch relations', relations);
 
     const relationLength = relations?.length;
     if (!relationLength) {
@@ -179,9 +126,7 @@ const eventMatch = () => {
     })
     .filter(item => item.info);
 
-    // Logs.out('eventMatch passableEvents', passableEvents);
-
-    const solutions = eventsCombination(passableEvents, SETTING);
+    const solutions = eventsCombination(passableEvents);
 
     // Logs.out('eventMatch solutions', solutions);
     if (solutions?.length) {
@@ -196,18 +141,13 @@ const eventMatch = () => {
 };
 
 const syncSetting = () => {
-  getSetting()
-  .then(setting => {
+  getDataFromParent('getSetting', (setting) => {
     if (setting) {
-      Object.keys(setting).forEach(key => {
-        SETTING[key] = setting[key];
-      });
+      updateSetting(setting);
+    }
+    else {
+      Logs.out('syncSetting setting is empty');
     }
-  })
-  .finally(() => {
-    setTimeout(() => {
-      syncSetting();
-    }, 10000);
   });
 }
 

+ 28 - 0
server/triangle/settings.js

@@ -0,0 +1,28 @@
+const Logs = require('../libs/logs');
+
+const SETTING = {
+  innerDefaultAmount: 10000,
+  minProfitAmount: 0,
+  innerRebateRatio: 0,
+  obRebateRatio: 0,
+  hgRebateRatio: 0,
+  runWorkerEnabled: false
+}
+
+const getSetting = (key) => {
+  if (key) {
+    return SETTING[key];
+  }
+  return SETTING;
+}
+
+const updateSetting = (fields) => {
+  Object.keys(fields).forEach(key => {
+    if (SETTING.hasOwnProperty(key) && SETTING[key] !== fields[key]) {
+      SETTING[key] = fields[key];
+      Logs.out('updateSetting', key, fields[key]);
+    }
+  });
+}
+
+module.exports = { getSetting, updateSetting };

+ 184 - 51
server/triangle/totalProfitCalc.js

@@ -1,7 +1,50 @@
+const Logs = require('../libs/logs');
+const { eventSolutions } = require('./eventSolutions');
+
+/**
+ * 精确浮点数字
+ * @param {number} number
+ * @param {number} x
+ * @returns {number}
+ */
 const fixFloat = (number, x = 2) => {
   return parseFloat(number.toFixed(x));
 }
 
+/**
+ * 计算输赢比例
+ * 与其他方法中的输赢逻辑相反
+ * 正数为输
+ * 负数为赢
+ */
+const CROSS_TYPE_MAP = { w: -1, l: 1, a: 1, h: 0.5, d: 0, r: 0 };
+const lossProportion = (sol) => {
+  const { cross_type, odds_side_a, odds_side_b, rebate_side_a, rebate_side_b } = sol;
+  const typeList = cross_type.split('_').map(part => {
+    return part.split('').map(key => CROSS_TYPE_MAP[key]);
+  }).map(([a, b])=> a * b);
+
+  let loss_proportion_a = 0, loss_proportion_b = 0;
+  if (typeList[0] >= 0) {
+    loss_proportion_a = typeList[0] * (1 - rebate_side_a);
+  }
+  else {
+    loss_proportion_a = typeList[0] * odds_side_a * (1 + rebate_side_a);
+  }
+
+  if (typeList[1] >= 0) {
+    loss_proportion_b = typeList[1] * (1 - rebate_side_b);
+  }
+  else {
+    loss_proportion_b = typeList[1] * odds_side_b * (1 + rebate_side_b);
+  }
+
+  return { loss_proportion_a, loss_proportion_b };
+}
+
+/**
+ * 不同组合的金额计算
+ */
 const HandicapCalc = function (data) {
   const { i, g, a, b, c, A, B, C, w, l } = data;
   const t = w + l;
@@ -132,39 +175,11 @@ const HandicapCalc = function (data) {
 }
 
 /**
- * 计算输赢比例
- * 与其他方法中的输赢逻辑相反
- * 正数为输
- * 负数为赢
+ * 根据预期盈利计算下注金额
  */
-const CROSS_TYPE_MAP = { w: -1, l: 1, a: 1, h: 0.5, d: 0, r: 0 };
-const lossProportion = (sol) => {
-  const { cross_type, odds_side_a, odds_side_b, rebate_side_a, rebate_side_b } = sol;
-  const typeList = cross_type.split('_').map(part => {
-    return part.split('').map(key => CROSS_TYPE_MAP[key]);
-  }).map(([a, b])=> a * b);
-
-  let loss_proportion_a = 0, loss_proportion_b = 0;
-  if (typeList[0] >= 0) {
-    loss_proportion_a = typeList[0] * (1 - rebate_side_a);
-  }
-  else {
-    loss_proportion_a = typeList[0] * odds_side_a * (1 + rebate_side_a);
-  }
-
-  if (typeList[1] >= 0) {
-    loss_proportion_b = typeList[1] * (1 - rebate_side_b);
-  }
-  else {
-    loss_proportion_b = typeList[1] * odds_side_b * (1 + rebate_side_b);
-  }
-
-  return { loss_proportion_a, loss_proportion_b };
-}
-
-const calcExternalHandicap = (data) => {
+const calcGoldsWithTarget = (data) => {
   const {
-    gold_side_inner: g,
+    inner_base: g,
     odds_side_a: a,
     odds_side_b: b,
     odds_side_c: c,
@@ -185,67 +200,78 @@ const calcExternalHandicap = (data) => {
   }
 }
 
-const calcGoldsWithWinTarget = (data) => {
-  const { gold_side_inner, win_target, sol1, sol2 } = data;
+/**
+ * 根据预期盈利计算金额和内盘盈亏
+ */
+const calcWinResultWithTarget = (data) => {
+  const { inner_base, inner_rebate, win_target, sol1, sol2 } = data;
   const {
+    cross_type: crossType1,
     odds_side_a: oddsA1,
     odds_side_b: oddsB1,
     odds_side_c: oddsC1,
+    rebate_side_a: rebateA1,
+    rebate_side_b: rebateB1,
+    rebate_side_c: rebateC1,
     inner_index: inner_index_1,
   } = sol1;
   const {
     gold_side_a: goldA1,
     gold_side_b: goldB1,
     gold_side_c: goldC1,
-  } = calcExternalHandicap({ ...sol1, gold_side_inner, win_target });
+  } = calcGoldsWithTarget({ ...sol1, inner_base, win_target });
 
   let loss_out_1 = 0, win_inner_1 = 0;
   switch (inner_index_1) {
     case 0:
-      loss_out_1 = goldB1 + goldC1;
-      win_inner_1 = gold_side_inner * (oddsA1 + 1);
+      loss_out_1 = goldB1 * (1 - rebateB1) + goldC1 * (1 - rebateC1);
+      win_inner_1 = inner_base * (oddsA1 + 1);
       break;
     case 1:
-      loss_out_1 = goldA1 + goldC1;
-      win_inner_1 = gold_side_inner * (oddsB1 + 1);
+      loss_out_1 = goldA1 * (1 - rebateA1) + goldC1 * (1 - rebateC1);
+      win_inner_1 = inner_base * (oddsB1 + 1);
       break;
     case 2:
       const { loss_proportion_a: lpA1, loss_proportion_b: lpB1 } = lossProportion(sol1);
-      loss_out_1 = goldA1 * lpA1 + goldB1 * lpB1 ;
-      win_inner_1 = gold_side_inner * (oddsC1 + 1)
+      loss_out_1 = goldA1 * lpA1 + goldB1 * lpB1;
+      win_inner_1 = inner_base * (oddsC1 + 1)
       break;
   }
 
   win_inner_1 = fixFloat(win_inner_1);
 
   const {
+    cross_type: crossType2,
     odds_side_a: oddsA2,
     odds_side_b: oddsB2,
     odds_side_c: oddsC2,
+    rebate_side_a: rebateA2,
+    rebate_side_b: rebateB2,
+    rebate_side_c: rebateC2,
     inner_index: inner_index_2,
   } = sol2;
   const {
     gold_side_a: goldA2,
     gold_side_b: goldB2,
     gold_side_c: goldC2,
-  } = calcExternalHandicap({ ...sol2, gold_side_inner, win_target, loss_out: loss_out_1 });
+  } = calcGoldsWithTarget({ ...sol2, inner_base, win_target, loss_out: loss_out_1 });
 
   let loss_out_2 = 0, win_inner_2 = 0, inner_base_key;
   switch (inner_index_2) {
     case 0:
       inner_base_key = 'goldA2';
-      loss_out_2 = gold_side_inner +goldB2 + goldC2 + loss_out_1;
+      loss_out_2 = inner_base + goldB2 * (1 - rebateB2) + goldC2 * (1 - rebateC2) + loss_out_1;
       win_inner_2 = win_inner_1 * (oddsA2 + 1);
       break;
     case 1:
       inner_base_key = 'goldB2';
-      loss_out_2 = gold_side_inner + goldA2 + goldC2 + loss_out_1;
+      loss_out_2 = inner_base + goldA2 * (1 - rebateA2) + goldC2 * (1 - rebateC2) + loss_out_1;
       win_inner_2 = win_inner_1 * (oddsB2 + 1);
       break;
     case 2:
       const { loss_proportion_a: lpA2, loss_proportion_b: lpB2  } = lossProportion(sol2);
       inner_base_key = 'goldC2';
-      loss_out_2 = gold_side_inner + goldA2 * lpA2 + goldB2 * lpB2 + loss_out_1;
+      loss_out_2 = inner_base + goldA2 * lpA2 + goldB2 * lpB2 + loss_out_1;
       win_inner_2 = win_inner_1 * (oddsC2 + 1);
       break;
   }
@@ -261,39 +287,51 @@ const calcGoldsWithWinTarget = (data) => {
   const result = {
     bet_info: [
       {
+        cross_type: crossType1,
         gold_side_a: goldsInfo.goldA1,
         gold_side_b: goldsInfo.goldB1,
         gold_side_c: goldsInfo.goldC1,
         odds_side_a: oddsA1,
         odds_side_b: oddsB1,
         odds_side_c: oddsC1,
+        rebate_side_a: rebateA1,
+        rebate_side_b: rebateB1,
+        rebate_side_c: rebateC1,
         inner_index: inner_index_1,
       },
       {
+        cross_type: crossType2,
         gold_side_a: goldsInfo.goldA2,
         gold_side_b: goldsInfo.goldB2,
         gold_side_c: goldsInfo.goldC2,
         odds_side_a: oddsA2,
         odds_side_b: oddsB2,
         odds_side_c: oddsC2,
+        rebate_side_a: rebateA2,
+        rebate_side_b: rebateB2,
+        rebate_side_c: rebateC2,
         inner_index: inner_index_2,
       }
     ],
     win_inner,
-    inner_base: gold_side_inner,
+    inner_base,
+    inner_rebate,
   }
 
   return result;
 }
 
-const calcTotalProfit = (sol1, sol2, gold_side_inner, rebate_side_inner) => {
-  const rebateInner = gold_side_inner * rebate_side_inner / 100;
+/**
+ * 根据单关盈亏计算综合利润
+ */
+const calcTotalProfit = (sol1, sol2, inner_base, inner_rebate) => {
+  const rebateInner = inner_base * inner_rebate;
   const winTarget1 = fixFloat(sol1.win_average - rebateInner);
   const winTarget2 = fixFloat(sol2.win_average - rebateInner);
   const winTarget = fixFloat(Math.min(winTarget1, winTarget2));
 
-  const win1 = calcGoldsWithWinTarget({ gold_side_inner, win_target: winTarget1, sol1, sol2 })?.win_inner;
-  const win2 = calcGoldsWithWinTarget({ gold_side_inner, win_target: winTarget2, sol1, sol2 })?.win_inner;
+  const win1 = calcWinResultWithTarget({ inner_base, inner_rebate, win_target: winTarget1, sol1, sol2 })?.win_inner;
+  const win2 = calcWinResultWithTarget({ inner_base, inner_rebate, win_target: winTarget2, sol1, sol2 })?.win_inner;
   const win_inner = fixFloat(Math.max(win1, win2), 2);
 
   const start = Math.max(winTarget, win_inner);
@@ -302,7 +340,7 @@ const calcTotalProfit = (sol1, sol2, gold_side_inner, rebate_side_inner) => {
 
   for (let i = start; i > end; i--) {
     const win_target = i;
-    const goldsInfo = calcGoldsWithWinTarget({ gold_side_inner, win_target, sol1, sol2 });
+    const goldsInfo = calcWinResultWithTarget({ inner_base, inner_rebate, win_target, sol1, sol2 });
     const { win_inner, ...goldsRest } = goldsInfo;
     const win_diff = Math.abs(fixFloat(win_target - goldsInfo.win_inner));
     const lastResult = result.at(-1);
@@ -313,7 +351,102 @@ const calcTotalProfit = (sol1, sol2, gold_side_inner, rebate_side_inner) => {
       break;
     }
   }
+
   return result.at(-1);
 }
 
-module.exports = calcTotalProfit;
+
+
+
+/**
+ * ----------------------------
+ * 计算第一关锁定之后的新利润
+ * 第一关过关后,重新计算第二关金额
+ * 结合第一关亏损计算第二关新利润
+ * ----------------------------
+ */
+
+/**
+ * 计算第二关利润
+ */
+const calcSecondProfit = (betInfo) => {
+  const {
+    inner_index, inner_odds_first,
+    odds_side_a: a, odds_side_b: b, odds_side_c: c,
+  } = betInfo;
+
+  let odds_side_a, odds_side_b, odds_side_c;
+  switch (inner_index) {
+    case 0:
+      odds_side_a = fixFloat((a+1) * (inner_odds_first+1) - 1);
+      odds_side_b = b;
+      odds_side_c = c;
+      break;
+    case 1:
+      odds_side_a = a;
+      odds_side_b = fixFloat((b+1) * (inner_odds_first+1) - 1);
+      odds_side_c = c;
+      break;
+    case 2:
+      odds_side_a = a;
+      odds_side_b = b;
+      odds_side_c = fixFloat((c+1) * (inner_odds_first+1) - 1);
+      break;
+  }
+
+  return eventSolutions({ ...betInfo, odds_side_a, odds_side_b, odds_side_c }, true);
+}
+
+/**
+ * 结合第一关亏损计算第二关新利润
+ */
+const calcTotalProfitWithFixedFirst = (betInfo1, betInfo2, inner_base, inner_rebate) => {
+  const {
+    cross_type: crossType1,
+    inner_index: inner_index_1,
+    gold_side_a: goldA1,
+    gold_side_b: goldB1,
+    gold_side_c: goldC1,
+    odds_side_a: oddsA1,
+    odds_side_b: oddsB1,
+    odds_side_c: oddsC1,
+    rebate_side_a: rebateA1,
+    rebate_side_b: rebateB1,
+    rebate_side_c: rebateC1,
+  } = betInfo1;
+
+  let loss_out_1 = 0, inner_ref_value = 0, inner_odds_1 = 0;
+  switch (inner_index_1) {
+    case 0:
+      loss_out_1 = goldB1 * (1 - rebateB1) + goldC1 * (1 - rebateC1);
+      inner_ref_value = goldA1;
+      inner_odds_1 = oddsA1;
+      break;
+    case 1:
+      loss_out_1 = goldA1 * (1 - rebateA1) + goldC1 * (1 - rebateC1);
+      inner_ref_value = goldB1;
+      inner_odds_1 = oddsB1;
+      break;
+    case 2:
+      const { loss_proportion_a: lpA1, loss_proportion_b: lpB1 } = lossProportion(betInfo1);
+      loss_out_1 = goldA1 * lpA1 + goldB1 * lpB1;
+      inner_ref_value = goldC1;
+      inner_odds_1 = oddsC1;
+      break;
+  }
+
+  if (inner_base && inner_base != inner_ref_value) {
+    throw new Error('inner_base is not equal to inner_ref_value');
+  }
+
+  const profitInfo = calcSecondProfit({ ...betInfo2, inner_base, inner_odds_first: inner_odds_1, inner_rebate });
+
+  // profitInfo.win_side_a = fixFloat(profitInfo.win_side_a - loss_out_1);
+  // profitInfo.win_side_b = fixFloat(profitInfo.win_side_b - loss_out_1);
+  // profitInfo.win_side_c = fixFloat(profitInfo.win_side_c - loss_out_1);
+  profitInfo.win_average = fixFloat(profitInfo.win_average - loss_out_1);
+
+  return profitInfo;
+}
+
+module.exports = { calcTotalProfit, calcTotalProfitWithFixedFirst };

+ 16 - 214
server/triangle/trangleCalc.js

@@ -1,21 +1,15 @@
 const crypto = require('crypto');
 const Logs = require('../libs/logs');
 const IOR_KEYS_MAP = require('./iorKeys');
-
-const SETTING = {
-  innerDefaultAmount: 10000,
-  minProfitAmount: 0,
-  innerRebateRatio: 0,
-  obRebateRatio: 0,
-  hgRebateRatio: 0,
-}
+const { getSetting } = require('./settings');
+const { eventSolutions } = require('./eventSolutions');
 
 /**
  * 筛选最优赔率
  */
 function getOptimalSelections(odds, rules) {
   const results = [];
-  const { obRebateRatio, hgRebateRatio } = SETTING;
+  const { obRebateRatio, hgRebateRatio } = getSetting();
   const rebateMap = {
     ob: 1 + obRebateRatio / 100,
     hg: 1 + hgRebateRatio / 100,
@@ -99,183 +93,6 @@ 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 / 100;
-  }
-  else if (innerIndex == 1) {
-    inner_rebate = y * innerRebateRatio / 100;
-  }
-  else if (innerIndex == 2) {
-    inner_rebate = z * innerRebateRatio / 100;
-  }
-
-  const k1 = a * (1 + A);
-  const k2 = 1 - A;
-  const k3 = b * (1 + B);
-  const k4 = 1 - B;
-  const k5 = c * (1 + C);
-  const k6 = 1 - C;
-
-  let win_side_a = 0, win_side_b = 0, win_side_c = 0;
-  win_side_a = k1*x - k4*y - k6*z;
-  win_side_b = k3*y - k2*x - k6*z;
-
-  switch (crossType) {
-    case 'la_wh_wa': // 全输 半赢 全赢
-      win_side_c = k5*z - k2*x + k3*y/2;
-      break;
-    case 'la_dr_wa': // 全输 和局 全赢
-      win_side_c = k5*z - k2*x;
-      break;
-    case 'la_lh_wa': // 全输 半输 全赢
-      win_side_c = k5*z - k2*x - k4*y/2;
-      break;
-    case 'lh_dr_wa': // 半输 和局 全赢
-      win_side_c = k5*z - k2*x/2;
-      break;
-    case 'lh_lh_wa': // 半输 半输 全赢
-      win_side_c = k5*z - k2*x/2 - k4*y/2;
-      break;
-    case 'la_la_wa': // 全输 全输 全赢
-      win_side_c = k5*z - k2*x - k4*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;
-  const k1 = a * (1 + A);
-  const k2 = 1 - A;
-  const k3 = b * (1 + B);
-  const k4 = 1 - B;
-  const k5 = c * (1 + C);
-  const k6 = 1 - C;
-  let x = innerDefaultAmount;
-  let y = (k1 + k2) * x / (k3 + k4);
-  let z;
-  switch (crossType) {
-    case 'la_wh_wa': // 全输 半赢 全赢
-      z = k3 * y / 2 / (k5 + k6);
-      break;
-    case 'la_dr_wa': // 全输 和局 全赢
-      z = k3 * y / (k5 + k6);
-      break;
-    case 'la_lh_wa': // 全输 半输 全赢
-      z = (k3 + k4 / 2) * y / (k5 + k6);
-      break;
-    case 'lh_dr_wa': // 半输 和局 全赢
-      z = (k3 * y - k2 * x / 2) / (k5 + k6);
-      break;
-    case 'lh_lh_wa': // 半输 半输 全赢
-      z = ((k3 + k4/2) * y - k2 * x / 2) / (k5 + k6);
-      break;
-    case 'la_la_wa': // 全输 全输 全赢
-      z = (k3 + k4) * y / (k5 + k6);
-      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: fixFloat(x),
-    gold_side_b: fixFloat(y),
-    gold_side_c: fixFloat(z),
-    odds_side_a: fixFloat(a),
-    odds_side_b: fixFloat(b),
-    odds_side_c: fixFloat(c),
-  };
-
-}
-
-const eventSolutions = (oddsInfo, oddsOption) => {
-  const { innerDefaultAmount, innerRebateRatio } = SETTING;
-  const goldsInfo = triangleGoldCalc(oddsInfo, oddsOption);
-  if (!goldsInfo) {
-    return;
-  }
-  const profitInfo = triangleProfitCalc(goldsInfo, oddsOption);
-
-  const { odds_side_a, odds_side_b, odds_side_c, gold_side_a, gold_side_b, gold_side_c } = goldsInfo;
-  const { win_side_a, win_side_b, win_side_c, win_average } = profitInfo;
-
-  let { crossType, innerIndex, rebateA, rebateB, rebateC } = oddsOption;
-  if (innerIndex == 0) {
-    rebateA = fixFloat(innerRebateRatio / 100, 3);
-  }
-  else if (innerIndex == 1) {
-    rebateB = fixFloat(innerRebateRatio / 100, 3);
-  }
-  else if (innerIndex == 2) {
-    rebateC = fixFloat(innerRebateRatio / 100, 3);
-  }
-
-  const win_profit = fixFloat(win_average / (gold_side_a + gold_side_b + gold_side_c) * 100);
-
-  return {
-    odds_side_a, odds_side_b, odds_side_c,
-    // gold_side_a, gold_side_b, gold_side_c,
-    // win_side_a, win_side_b, win_side_c,
-    win_average, win_profit,
-    rebate_side_a: rebateA,
-    rebate_side_b: rebateB,
-    rebate_side_c: rebateC,
-    cross_type: crossType,
-    inner_index: innerIndex,
-    inner_base: innerDefaultAmount,
-  }
-}
-
 /**
  * 盘口排序
  */
@@ -290,7 +107,7 @@ const sortCpr = (cpr) => {
  * 添加返佣
  */
 const attachRebate = (ior) => {
-  const { obRebateRatio, hgRebateRatio } = SETTING;
+  const { obRebateRatio, hgRebateRatio } = getSetting();
   const { p } = ior;
   let rebate = 0;
   if (p == 'ps') {
@@ -305,15 +122,8 @@ const attachRebate = (ior) => {
   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 eventsCombination = (passableEvents) => {
+  const { minProfitAmount, innerDefaultAmount, innerRebateRatio } = getSetting();
   const solutions = [];
   passableEvents.forEach(events => {
     const { odds, info } = events;
@@ -332,19 +142,20 @@ const eventsCombination = (passableEvents, setting) => {
           return;
         }
         const cpr = [ oddsSideA, oddsSideB, oddsSideC ];
-        const oddsInfo = {
+        const betInfo = {
+          cross_type: crossType,
+          inner_index: innerIndex,
+          inner_base: innerDefaultAmount,
+          inner_rebate: 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.r / 100).toFixed(4)),
+          rebate_side_b: parseFloat((oddsSideB.r / 100).toFixed(4)),
+          rebate_side_c: parseFloat((oddsSideC.r / 100).toFixed(4)),
         };
-        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 sol = eventSolutions(betInfo);
+        if (sol?.win_average > minProfitAmount) {
           const id = info.id;
           const sortedCpr = sortCpr(cpr);
           const keys = sortedCpr.map(item => `${item.k}`).join('_');
@@ -359,15 +170,6 @@ const eventsCombination = (passableEvents, setting) => {
   return solutions.sort((a, b) => {
     return b.sol.win_average - a.sol.win_average;
   });
-  // 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 };

+ 7 - 3
web/apps/web-antd/src/views/match/solutions/index.vue

@@ -106,7 +106,7 @@ const solutionsList = computed(() => {
   const startTimestamp = selectedSolutions[0]?.timestamp ?? 0;
   return solutions.value.map(item => {
     const selected = selectedSolutions.findIndex(sol => sol.sid === item.sid) >= 0;
-    const disabled = !selected && (item.info.ps.timestamp < startTimestamp + 1000 * 60 * 60 * 2);
+    const disabled = false && !selected && (item.info.ps.timestamp < startTimestamp + 1000 * 60 * 60 * 2);
     const currentSol = { ...item.sol };
 
     const psScale = psOptions.bet / currentSol.inner_base;
@@ -366,7 +366,7 @@ onUnmounted(() => {
 
     <div class="solution-list">
       <div class="solution-item"
-        v-for="{ sid, sol: { win_average }, info: { ps, ob, hg }, selected, disabled } in solutionsList" :key="sid"
+        v-for="{ sid, sol: { win_average, cross_type }, info: { ps, ob, hg }, selected, disabled } in solutionsList" :key="sid"
         :class="{ 'selected': selected, 'disabled': disabled }">
         <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
           :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
@@ -380,7 +380,10 @@ onUnmounted(() => {
           :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
           :selected="hg.selected ?? []" />
 
-        <div class="solution-profit" @click="!disabled && toggleSolution(sid, ps.timestamp)">{{ win_average }}</div>
+        <div class="solution-profit" @click="!disabled && toggleSolution(sid, ps.timestamp)">
+          <p>{{ win_average }}</p>
+          <p>{{ cross_type }}</p>
+        </div>
       </div>
     </div>
     <div class="list-empty" v-if="!solutionsList.length">暂无数据</div>
@@ -522,6 +525,7 @@ onUnmounted(() => {
 
     .solution-profit {
       display: flex;
+      flex-direction: column;
       width: 80px;
       align-items: center;
       justify-content: center;