flyzto 6 сар өмнө
parent
commit
6e9c8fdc13

+ 3 - 3
server/models/Games.js

@@ -380,7 +380,7 @@ const relationSync = () => {
 /**
  * 更新中单方案
  */
-const setSolutions = (solutions) => {
+const updateSolutions = (solutions) => {
   if (solutions?.length) {
     const solutionsHistory = GAMES.Solutions;
     const updateIds = { add: [], update: [] }
@@ -504,8 +504,8 @@ const getTotalProfit = (sid1, sid2, gold_side_inner) => {
 //     events_child.send({ type: 'response', id, data: responseData });
 //   }
 //   else if (method == 'post') {
-//     if (type == 'setSolutions') {
-//       setSolutions(data);
+//     if (type == 'updateSolutions') {
+//       updateSolutions(data);
 //     }
 //   }
 //   else if (method == 'response' && id && callbacks[id]) {

+ 100 - 6
server/models/GamesPs.js

@@ -25,6 +25,7 @@ const GAMES = {
   List: {},
   Baselist: {},
   Relations: {},
+  Solutions: {},
 };
 
 const Request = {
@@ -429,13 +430,106 @@ const updateGamesResult = (result) => {
 }
 
 /**
- * 保存解决方案
+ * 同步中单方案
  */
-const setSolutions = (solutions) => {
-  //TODO: 保存解决方案
-  Logs.out('setSolutions', solutions);
+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 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);
+    // Logs.outDev('solutions history cleanup', updateIds);
+  }
 }
 
+/**
+ * 定时清理中单方案
+ */
+setInterval(() => {
+  solutionsCleanup();
+}, 1000*30);
+
 /**
  * 获取后台设置
  */
@@ -472,8 +566,8 @@ events_child.on('message', async (message) => {
     events_child.send({ type: 'response', id, data: responseData });
   }
   else if (method == 'post') {
-    if (type == 'setSolutions') {
-      setSolutions(data);
+    if (type == 'updateSolutions') {
+      updateSolutions(data);
     }
   }
   else if (method == 'response' && id && callbacks[id]) {

+ 6 - 4
server/triangle/eventsMatch.js

@@ -92,8 +92,8 @@ const getGamesRelation = () => {
 //   });
 // }
 
-const setSolutions = (solutions) => {
-  postDataToParent('setSolutions', solutions);
+const updateSolutions = (solutions) => {
+  postDataToParent('updateSolutions', solutions);
 }
 
 const extractOdds = ({ evtime, events, sptime, special }) => {
@@ -142,7 +142,7 @@ const eventMatch = () => {
     //   return true;
     // });
 
-    Logs.out('eventMatch relations', relations);
+    // Logs.out('eventMatch relations', relations);
 
     const relationLength = relations?.length;
     if (!relationLength) {
@@ -184,7 +184,9 @@ const eventMatch = () => {
     const solutions = eventsCombination(passableEvents, SETTING);
 
     // Logs.out('eventMatch solutions', solutions);
-    setSolutions(solutions);
+    if (solutions?.length) {
+      updateSolutions(solutions);
+    }
   })
   .finally(() => {
     setTimeout(() => {

+ 84 - 20
server/triangle/totalProfitCalc.js

@@ -3,8 +3,14 @@ const fixFloat = (number, x = 2) => {
 }
 
 const HandicapCalc = function (data) {
-  const { i, g, a, b, c, w, l } = data;
+  const { i, g, a, b, c, A, B, C, w, l } = data;
   const t = w + l;
+  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;
 
   const calcTemplate = (handlers) => {
     if (i > 2 || i < 0) {
@@ -12,8 +18,9 @@ const HandicapCalc = function (data) {
     }
     if (i === 2) {
       const z = g;
-      const x = (b + 1) * (t + z) / (a * b - 1);
-      const y = (a + 1) * (t + z) / (a * b - 1);
+      const m = t + k6 * z;
+      const x = (k3 + k4) * m / (k1 * k3 - k2 * k4);
+      const y = (k1 + k2) * m / (k1 * k3 - k2 * k4);
       return { x, y, z };
     };
     return handlers[i]?.() ?? {};
@@ -24,14 +31,15 @@ const HandicapCalc = function (data) {
       return calcTemplate([
         () => {
           const x = g;
-          const z = (t + x) / (2 * c + 1);
-          const y = (c + 1) * (t + x) / (c + 0.5) / b;
+          const m = t + x - A * x;
+          const z = m / k5 * 2 + k6;
+          const y = (k5 + k6) * m / (k3 * k5 + k3 * k6 / 2);
           return { x, y, z };
         },
         () => {
           const y = g;
-          const z = ((a + 1) * t + (1 - a * b / 2) * y) / (a * c - 1);
-          const x = ((c + 1) * t + (c - b / 2) * y) / (a * c - 1);
+          const z = ((k1 + k2) * t + (k2 * k4 - k1 * k3 / 2) * y) / (k1 * k5 - k2 * k6);
+          const x = ((k5 + k6) * t + (k4 * k5 - k3 * k6 / 2) * y) / (k1 * k5 - k2 * k6);
           return { x, y, z };
         }
       ]);
@@ -118,10 +126,53 @@ 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 { gold_side_inner: g, odds_side_a: a, odds_side_b: b, odds_side_c: c, inner_index: i, cross_type: t, win_target: w, loss_out_1 } = data;
+  const {
+    gold_side_inner: g,
+    odds_side_a: a,
+    odds_side_b: b,
+    odds_side_c: c,
+    rebate_side_a: A,
+    rebate_side_b: B,
+    rebate_side_c: C,
+    inner_index: i,
+    cross_type: t,
+    win_target: w,
+    loss_out_1,
+  } = data;
   const l = loss_out_1 ?? 0;
-  const calc = new HandicapCalc({ i, g, a, b, c, w, l });
+  const calc = new HandicapCalc({ i, g, a, b, c, A, B, C, w, l });
   const { x, y, z } = calc?.[t]() ?? {};
   return {
     gold_side_a: fixFloat(x),
@@ -129,7 +180,6 @@ const calcExternalHandicap = (data) => {
     gold_side_c: fixFloat(z),
     inner_index: i,
   }
-
 }
 
 const calcGoldsWithWinTarget = (data) => {
@@ -138,22 +188,29 @@ const calcGoldsWithWinTarget = (data) => {
     gold_side_a: goldA1,
     gold_side_b: goldB1,
     gold_side_c: goldC1,
-    inner_index: inner_index_1
+    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,
   } = calcExternalHandicap({ ...sol1, gold_side_inner, 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 * (sol1.odds_side_a + 1);
+      win_inner_1 = gold_side_inner * (oddsA1 + 1);
       break;
     case 1:
       loss_out_1 = goldA1 + goldC1;
-      win_inner_1 = gold_side_inner * (sol1.odds_side_b + 1);
+      win_inner_1 = gold_side_inner * (oddsB1 + 1);
       break;
     case 2:
-      loss_out_1 = goldA1 + goldB1;
-      win_inner_1 = gold_side_inner * (sol1.odds_side_c + 1)
+      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)
       break;
   }
 
@@ -161,7 +218,13 @@ const calcGoldsWithWinTarget = (data) => {
     gold_side_a: goldA2,
     gold_side_b: goldB2,
     gold_side_c: goldC2,
-    inner_index: inner_index_2
+    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,
   } = calcExternalHandicap({ ...sol2, gold_side_inner, win_target, loss_out_1 });
 
   let loss_out_2 = 0, win_inner_2 = 0, inner_base_key;
@@ -169,17 +232,18 @@ const calcGoldsWithWinTarget = (data) => {
     case 0:
       inner_base_key = 'goldA2';
       loss_out_2 = gold_side_inner +goldB2 + goldC2 + loss_out_1;
-      win_inner_2 = win_inner_1 * (sol2.odds_side_a + 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;
-      win_inner_2 = win_inner_1 * (sol2.odds_side_b + 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 + goldB2 + loss_out_1;
-      win_inner_2 = win_inner_1 * (sol2.odds_side_c + 1);
+      loss_out_2 = gold_side_inner + goldA2 * lpA2 + goldB2 * lpB2 + loss_out_1;
+      win_inner_2 = win_inner_1 * (oddsC2 + 1);
       break;
   }
 

+ 27 - 23
server/triangle/trangleCalc.js

@@ -1,3 +1,4 @@
+const crypto = require('crypto');
 const Logs = require('../libs/logs');
 const IOR_KEYS_MAP = require('./iorKeys');
 
@@ -230,6 +231,9 @@ const eventSolutions = (oddsInfo, oddsOption) => {
     odds_side_b,
     odds_side_c,
     win_average,
+    rebate_side_a: oddsOption.rebateA,
+    rebate_side_b: oddsOption.rebateB,
+    rebate_side_c: oddsOption.rebateC,
     cross_type: oddsOption.crossType,
     inner_index: oddsOption.innerIndex,
     inner_base: innerDefaultAmount,
@@ -237,13 +241,13 @@ const eventSolutions = (oddsInfo, oddsOption) => {
 }
 
 /**
- * 将数组中的指定索引的元素移动到最前面
-*/
-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 priority = { ps: 1, ob: 2, hg: 3 };
+const sortCpr = (cpr) => {
+  const temp = [...cpr];
+  temp.sort((a, b) => priority[a.p] - priority[b.p]);
+  return temp;
 }
 
 /**
@@ -274,7 +278,7 @@ const eventsCombination = (passableEvents, setting) => {
     }
   });
 
-  const solutions = {};
+  const solutions = [];
   passableEvents.forEach(events => {
     const { odds, info } = events;
     Object.keys(IOR_KEYS_MAP).forEach(iorGroup => {
@@ -306,28 +310,28 @@ const eventsCombination = (passableEvents, setting) => {
         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 sortedCpr = sortCpr(cpr);
           const keys = sortedCpr.map(item => `${item.k}`).join('_');
-          const sid = `${id}_${keys}`;
+          const sid = crypto.createHash('sha1').update(`${id}_${keys}`).digest('hex');
+          const crpGroup = `${id}_${sortedCpr[0].k}`;
           const timestamp = Date.now();
-          if (!solutions[crpGroup]) {
-            solutions[crpGroup] = [];
-          }
-          solutions[crpGroup].push({sid, sol, cpr, info, rule: `${iorGroup}:${index}`, timestamp});
+          solutions.push({sid, sol, cpr, info, group: crpGroup, 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;
+  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 };

+ 24 - 13
web/apps/web-antd/src/views/system/parameter/index.vue

@@ -14,16 +14,28 @@ const initialFormState = {
 };
 
 const formState = reactive({ ...initialFormState });
-const isFormChanged = ref(false);
 
-const checkFormChanged = () => {
-  isFormChanged.value = Object.keys(initialFormState).some(key =>
-    formState[key] !== initialFormState[key]
-  );
-};
+const formChangeTimer = ref(null);
+
+const formChanged = computed(() => {
+  const changed = {};
+  Object.keys(formState).forEach(key => {
+    if (formState[key] !== initialFormState[key]) {
+      changed[key] = formState[key];
+    }
+  });
+  return changed;
+});
 
 watch(formState, () => {
-  checkFormChanged();
+  if (formChangeTimer.value) {
+    clearTimeout(formChangeTimer.value);
+  }
+  formChangeTimer.value = setTimeout(() => {
+    if (Object.keys(formChanged.value).length > 0) {
+      saveSetting(formChanged.value);
+    }
+  }, 1000);
 }, { deep: true });
 
 const getSetting = async () => {
@@ -38,9 +50,9 @@ const getSetting = async () => {
   }
 }
 
-const saveSetting = async () => {
+const saveSetting = async (changed) => {
   try {
-    await requestClient.post('/system/update_setting', formState);
+    await requestClient.post('/system/update_setting', changed);
     message.success('保存成功');
     syncSetting();
   }
@@ -55,7 +67,6 @@ const syncSetting = () => {
     if (data) {
       Object.assign(formState, data);
       Object.assign(initialFormState, data);
-      isFormChanged.value = false;
     }
   });
 }
@@ -65,7 +76,7 @@ onMounted(() => {
 });
 
 onUnmounted(() => {
-
+  clearTimeout(formChangeTimer.value);
 });
 </script>
 
@@ -148,9 +159,9 @@ onUnmounted(() => {
         <Switch v-model:checked="formState.runWorkerEnabled" />
       </Form.Item>
 
-      <Form.Item :wrapper-col="{ offset: 6, span: 18 }">
+      <!-- <Form.Item :wrapper-col="{ offset: 6, span: 18 }">
         <Button type="primary" @click="saveSetting" :disabled="!isFormChanged">保存设置</Button>
-      </Form.Item>
+      </Form.Item> -->
     </Form>
   </Page>
 </template>