소스 검색

新增 让胜负大小 返还率限制

flyzto 3 주 전
부모
커밋
9162c6fdb6

+ 1 - 0
server/init.js

@@ -22,6 +22,7 @@ const Logs = require('./libs/logs');
     pcRebateRatio: 0,
     pcRebateType: 0,
     innerRouSubsidyValue: 0,
+    innerRouMaxAdjust: 100,
     innerWmSubsidyRatio: 0,
     innerOtSubsidyRatio: 0,
     halfTimeActiveTime: 0,

+ 174 - 88
server/models/GamesPs.js

@@ -284,6 +284,129 @@ const updateGamesList = (({ platform, mk, games } = {}) => {
 //   });
 // }
 
+/**
+ * 解析盘口关键词
+ */
+const parseRatio = (ratioString) => {
+  return parseFloat(`${ratioString[0]}.${ratioString.slice(1)}`);
+}
+const parseIorKey = (iorKey) => {
+  const iorKeyMatch = iorKey.match(/^ior_(r|ou|m|wm|ot|os)(a?)(h|c|n)?(_([\d-]+))?$/);
+  if (!iorKeyMatch) {
+    console.log('no iorKeyMatch', iorKey);
+    return null;
+  }
+  const [, type, accept, side, , ratioString] = iorKeyMatch;
+  let ratio = 0;
+  if (type === 'ot' || type === 'os') {
+    ratio = ratioString;
+  }
+  else if (ratioString) {
+    ratio = parseRatio(ratioString) * (accept ? 1 : -1);
+  }
+  return { type, side, ratio };
+}
+
+/**
+ * 格式化盘口数据
+ */
+const getRouEvents = (events) => {
+  const eventsMap = {};
+  Object.keys(events).forEach(key => {
+    const { type, side, ratio } = parseIorKey(key) ?? {};
+    if (!type) {
+      return;
+    }
+    let ratioKey, index;
+    if (type === 'r') {
+      if (side === 'h') {
+        ratioKey = ratio;
+        index = 0;
+      }
+      else if (side === 'c') {
+        ratioKey = -ratio;
+        index = 1;
+      }
+    }
+
+    else if (type === 'ou') {
+      ratioKey = `ou_${Math.abs(ratio)}`;
+      if (side === 'c') {
+        index = 0;
+      }
+      else if (side === 'h') {
+        index = 1;
+      }
+    }
+
+    if (typeof (ratioKey) == 'number') {
+      if (ratioKey > 0) {
+        ratioKey = `+${ratioKey}`;
+      }
+      else {
+        ratioKey = `${ratioKey}`;
+      }
+    }
+
+    if (!ratioKey || index === undefined) {
+      return;
+    }
+
+    if (!eventsMap[ratioKey]) {
+      eventsMap[ratioKey] = new Array(2).fill(undefined);
+    }
+
+    const v = events[key]?.v ?? 0;
+    const r = events[key]?.r;
+    const q = events[key]?.q ?? 1;
+    eventsMap[ratioKey][index] = { key, v, r, q };
+  });
+
+  return eventsMap;
+}
+
+/**
+ * 限制让胜负/大小球返还率
+ */
+const rouMaxAdjust = (events) => {
+  const { innerRouMaxAdjust } = getSetting();
+  const rouEvents = getRouEvents(events);
+  // Logs.outDev('rouEvents', rouEvents);
+
+  const adjustedEvents = {};
+  Object.values(rouEvents).forEach(item => {
+    const [a, b] = item;
+    const returnRate = (a.v*b.v)/(a.v+b.v)*100;
+    if (returnRate > innerRouMaxAdjust) {
+      const proportion = returnRate / innerRouMaxAdjust;
+      item.forEach(ior => {
+        const { key, v, r, q } = ior;
+        adjustedEvents[key] = {
+          v: fixFloat(v/proportion, 3),
+          r: r,
+          s: v,
+          q: q,
+          rt: fixFloat(returnRate, 3),
+        };
+      });
+    }
+  });
+
+  if (events['ior_mh']?.v > adjustedEvents['ior_rh_05']?.v) {
+    events['ior_mh'].s = events['ior_mh'].v;
+    events['ior_mh'].v = adjustedEvents['ior_rh_05'].v;
+    events['ior_mh'].rt = adjustedEvents['ior_rh_05'].rt;
+  }
+
+  if (events['ior_mc']?.v > adjustedEvents['ior_rc_05']?.v) {
+    events['ior_mc'].s = events['ior_mc'].v;
+    events['ior_mc'].v = adjustedEvents['ior_rc_05'].v;
+    events['ior_mc'].rt = adjustedEvents['ior_rc_05'].rt;
+  }
+
+  return Object.assign(events, adjustedEvents);
+}
+
 /**
  * 同步基准盘口
  */
@@ -308,33 +431,34 @@ const syncBaseEvents = ({ mk, games, outrights }) => {
     const { eventId, originId, stage, retime, score, wm, evtime, events } = game;
     const baseGame = baseMap.get(eventId);
     if (baseGame) {
-      Object.keys(events).forEach(ior => {
-        const regRm = /^ior_(r|mh|mc)/;
+      const adjustedEvents = rouMaxAdjust(events);
+      Object.keys(adjustedEvents).forEach(ior => {
+        // const regRm = /^ior_(r|mh|mc)/;
         const regWm = /^ior_(wm|mn)/;
         const regOu = /^ior_ou/;
         const regOt = /^ior_ot/;
         // 胜负/让胜负/大小球调水(数值)
-        if ((regRm.test(ior) || regOu.test(ior)) && innerRouSubsidyValue) {
-          const sourceOdds = events[ior].v;
-          events[ior].v = fixFloat(sourceOdds + innerRouSubsidyValue, 3);
-          events[ior].s = sourceOdds;
-        }
+        // if ((regRm.test(ior) || regOu.test(ior)) && innerRouSubsidyValue) {
+        //   const sourceOdds = adjustedEvents[ior].v;
+        //   adjustedEvents[ior].v = fixFloat(sourceOdds + innerRouSubsidyValue, 3);
+        //   adjustedEvents[ior].s = sourceOdds;
+        // }
         // 平局/让平调水(%)
-        else if (regWm.test(ior) && innerWmSubsidyRatio) {
-          const sourceOdds = events[ior].v;
-          events[ior].v = fixFloat(sourceOdds * (1 + innerWmSubsidyRatio / 100), 3);
-          events[ior].s = sourceOdds;
+        if (regWm.test(ior) && innerWmSubsidyRatio) {
+          const sourceOdds = adjustedEvents[ior].v;
+          adjustedEvents[ior].v = fixFloat(sourceOdds * (1 + innerWmSubsidyRatio / 100), 3);
+          adjustedEvents[ior].s = sourceOdds;
         }
         // 进球数调水(%)
         else if (regOt.test(ior) && innerOtSubsidyRatio) {
-          const sourceOdds = events[ior].v;
-          events[ior].v = fixFloat(sourceOdds * (1 + innerOtSubsidyRatio / 100), 3);
-          events[ior].s = sourceOdds;
+          const sourceOdds = adjustedEvents[ior].v;
+          adjustedEvents[ior].v = fixFloat(sourceOdds * (1 + innerOtSubsidyRatio / 100), 3);
+          adjustedEvents[ior].s = sourceOdds;
         }
         // 大小球最低赔率
         if (regOu.test(ior) && innerOuMinValue) {
-          const sourceOdds = events[ior].s ?? events[ior].v ?? 1;
-          events[ior].q = sourceOdds < innerOuMinValue ? 0 : 1;
+          const sourceOdds = adjustedEvents[ior].s ?? adjustedEvents[ior].v ?? 1;
+          adjustedEvents[ior].q = sourceOdds < innerOuMinValue ? 0 : 1;
         }
       });
       baseGame.originId = originId;
@@ -343,51 +467,10 @@ const syncBaseEvents = ({ mk, games, outrights }) => {
       baseGame.score = score;
       baseGame.wm = wm;
       baseGame.evtime = evtime;
-      baseGame.events = events;
+      baseGame.events = adjustedEvents;
     }
   });
 
-  // 浏览器采集特殊盘口用的
-  // outrights?.forEach(outright => {
-  //   const { parentId, sptime, special } = outright;
-  //   const baseGame = baseMap.get(parentId);
-  //   if (baseGame) { // 赛前特殊盘口
-  //     const { timestamp } = baseGame;
-  //     const isSubsidy = timestamp > nowTime && timestamp < nowTime + 1000*60*60*subsidyTime;
-  //     if (isSubsidy) {
-  //       Object.keys(special).forEach(ior => {
-  //         if (ior.startsWith('ior_ot') && subsidyAmount) {
-  //           const sourceOdds = special[ior].v;
-  //           special[ior].v = fixFloat(sourceOdds * (1 + subsidyAmount), 3);
-  //           special[ior].s = sourceOdds
-  //         }
-  //       });
-  //     }
-  //     baseGame.sptime = sptime;
-  //     baseGame.special = special;
-  //   }
-  //   else {
-  //     const originBaseMap = new Map(baseList[marketType].map(item => [item.originId, item]));
-  //     const originBaseGame = originBaseMap.get(parentId);
-  //     if (originBaseGame) { // 滚球特殊盘口
-  //       Object.keys(special).forEach(ior => {
-  //         if (ior.startsWith('ior_wm') && subsidyRbWmAmount) {
-  //           const sourceOdds = special[ior].v;
-  //           special[ior].v = fixFloat(sourceOdds * (1 + subsidyRbWmAmount), 3);
-  //           special[ior].s = sourceOdds
-  //         }
-  //         else if (ior.startsWith('ior_ot') && subsidyRbOtAmount) {
-  //           const sourceOdds = special[ior].v;
-  //           special[ior].v = fixFloat(sourceOdds * (1 + subsidyRbOtAmount), 3);
-  //           special[ior].s = sourceOdds
-  //         }
-  //       });
-  //       originBaseGame.sptime = sptime;
-  //       originBaseGame.special = special;
-  //     }
-  //   }
-  // });
-
   if (games?.length) {
     const gamesList = baseList[marketType]?.map(game => {
       const { evtime, events, sptime, special, ...gameInfo } = game;
@@ -402,12 +485,13 @@ const syncBaseEvents = ({ mk, games, outrights }) => {
       }
       const matches = PS_IOR_KEYS.map(([label, ...keys]) => {
         let match = keys.map(key => {
-          let value = odds[key]?.v ?? 0;
           return {
-            key, value,
+            key,
+            value: odds[key]?.v ?? 0,
             origin: odds[key]?.r,
             source: odds[key]?.s,
             qualified: odds[key]?.q ?? 1,
+            returnRate: odds[key]?.rt,
           };
         });
         if (label == 'jqs' || label == 'ou') {
@@ -458,12 +542,13 @@ const syncBaseEvents = ({ mk, games, outrights }) => {
       if (relatedGame) {
         const events = {};
         matches.forEach(({ label, match }) => {
-          match.forEach(({ key, value, origin, source, qualified }) => {
+          match.forEach(({ key, value, origin, source, qualified, returnRate }) => {
             events[key] = {
               v: value,
               r: origin,
               s: source,
               q: qualified,
+              rt: returnRate,
             };
           });
         });
@@ -1431,9 +1516,9 @@ const syncQbossConfig = () => {
   axios.get(`${BASE_API_URL}/p/QbossSystemConfig`, { proxy: false })
   .then(res => {
     const { data } = res;
-    Logs.outDev('syncQbossConfig', data);
+    Logs.outDev('QbossSystemConfig', data);
     if (!data?.data) {
-      throw new Error('syncQbossConfig data is empty');
+      throw new Error('QbossSystemConfig data is empty');
     }
     const {
       qboss_return_ratio,
@@ -1441,34 +1526,35 @@ const syncQbossConfig = () => {
       im_return_ratio, im_return_type, im_odds_more_than,
       hg_return_ratio, hg_return_type, hg_odds_more_than,
       pc_return_ratio, pc_return_type,
-      qboss_jq_add_odds, qboss_jq_add_hours,
-      qboss_gq_add_dy_odds, qboss_gq_add_jq_odds,
-      bc_before_hours,
+      // qboss_jq_add_odds, qboss_jq_add_hours,
+      // qboss_gq_add_dy_odds, qboss_gq_add_jq_odds,
+      bc_before_hours, qboss_ts_rqdx_max_return_rate_adjust,
       qboss_ts_sfdx_odds_add, qboss_ts_pjrq_odds_add, qboss_ts_jqs_odds_add,
      } = data.data;
     const qbossSetting = {
-      innerRebateRatio: qboss_return_ratio ? +qboss_return_ratio : 0,
-      obRebateRatio: ob_return_ratio ? +ob_return_ratio : 0,
-      obRebateType: ob_return_type ? +ob_return_type : 0,
-      obMaxDiff: ob_odds_more_than ? +ob_odds_more_than : 0,
-      imRebateRatio: im_return_ratio ? +im_return_ratio : 0,
-      imRebateType: im_return_type ? +im_return_type : 0,
-      imMaxDiff: im_odds_more_than ? +im_odds_more_than : 0,
-      hgRebateRatio: hg_return_ratio ? +hg_return_ratio : 0,
-      hgRebateType: hg_return_type ? +hg_return_type : 0,
-      hgMaxDiff: hg_odds_more_than ? +hg_odds_more_than : 0,
-      pcRebateRatio: pc_return_ratio ? +pc_return_ratio : 0,
-      pcRebateType: pc_return_type ? +pc_return_type : 0,
-      subsidyTime: qboss_jq_add_hours ? +qboss_jq_add_hours : 0,
-      subsidyAmount: qboss_jq_add_odds ? +qboss_jq_add_odds : 0,
-      subsidyRbWmAmount: qboss_gq_add_dy_odds ? +qboss_gq_add_dy_odds : 0,
-      subsidyRbOtAmount: qboss_gq_add_jq_odds ? +qboss_gq_add_jq_odds : 0,
-      innerRouSubsidyValue: qboss_ts_sfdx_odds_add ? +qboss_ts_sfdx_odds_add : 0,
-      innerWmSubsidyRatio: qboss_ts_pjrq_odds_add ? +qboss_ts_pjrq_odds_add : 0,
-      innerOtSubsidyRatio: qboss_ts_jqs_odds_add ? +qboss_ts_jqs_odds_add : 0,
-      halfTimeActiveTime: bc_before_hours ? +bc_before_hours : 0,
+      innerRebateRatio: +(qboss_return_ratio ?? '0'),
+      obRebateRatio: +(ob_return_ratio ?? '0'),
+      obRebateType: +(ob_return_type ?? '0'),
+      obMaxDiff: +(ob_odds_more_than ?? '0'),
+      imRebateRatio: +(im_return_ratio ?? '0'),
+      imRebateType: +(im_return_type ?? '0'),
+      imMaxDiff: +(im_odds_more_than ?? '0'),
+      hgRebateRatio: +(hg_return_ratio ?? '0'),
+      hgRebateType: +(hg_return_type ?? '0'),
+      hgMaxDiff: +(hg_odds_more_than ?? '0'),
+      pcRebateRatio: +(pc_return_ratio ?? '0'),
+      pcRebateType: +(pc_return_type ?? '0'),
+      // subsidyTime:  +(qboss_jq_add_hours ?? '0'),
+      // subsidyAmount: +(qboss_jq_add_odds ?? '0'),
+      // subsidyRbWmAmount: +(qboss_gq_add_dy_odds ?? '0'),
+      // subsidyRbOtAmount: +(qboss_gq_add_jq_odds ?? '0'),
+      innerRouSubsidyValue: +(qboss_ts_sfdx_odds_add ?? '0'),
+      innerRouMaxAdjust: +(qboss_ts_rqdx_max_return_rate_adjust ?? '100'),
+      innerWmSubsidyRatio: +(qboss_ts_pjrq_odds_add ?? '0'),
+      innerOtSubsidyRatio: +(qboss_ts_jqs_odds_add ?? '0'),
+      halfTimeActiveTime: +(bc_before_hours ?? '0'),
     };
-    Logs.outDev('syncQbossConfig', qbossSetting);
+    Logs.outDev('Sync QbossSystemConfig', qbossSetting);
     const settingFields = {};
     let needUpdate = false;
     Object.keys(qbossSetting).forEach(key => {
@@ -1486,7 +1572,7 @@ const syncQbossConfig = () => {
     // }
   })
   .catch(err => {
-    Logs.out('syncQbossConfig error', err.message);
+    Logs.out('Sync QbossSystemConfig error', err.message);
   })
   .finally(() => {
     setTimeout(syncQbossConfig, 1000*15);

+ 5 - 0
server/models/Setting.js

@@ -96,6 +96,11 @@ const systemSettingSchema = new Schema({
     required: true,
     default: 0
   },
+  innerRouMaxAdjust: {
+    type: Number,
+    required: true,
+    default: 100
+  },
   innerWmSubsidyRatio: {
     type: Number,
     required: true,

+ 1 - 0
server/triangle/settings.js

@@ -19,6 +19,7 @@ const SETTING = {
   pcRebateRatio: 0,
   pcRebateType: 0,
   innerRouSubsidyValue: 0,
+  innerRouMaxAdjust: 100,
   innerWmSubsidyRatio: 0,
   innerOtSubsidyRatio: 0,
   halfTimeActiveTime: 0,

+ 40 - 20
web/apps/web-antd/src/views/match/components/match_card.vue

@@ -1,4 +1,6 @@
 <script setup>
+import { Tooltip } from 'ant-design-vue';
+
 const parseEventKey = (key) => {
   if (key == 'm') {
     return '独赢';
@@ -62,28 +64,37 @@ defineProps({
           <tr v-for="item in events">
             <th>{{ parseEventKey(item[0]) }}</th>
             <td>
-              <span :class="{
+              <span class="value-box" :class="{
                 'selected': selected.includes(item[1]?.key),
                 'highlight': iorGroups?.includes(item[1]?.key),
-                'strikethrough': item[1]?.qualified === 0
-              }">{{ item[1]?.value ? item[1].value : '-' }}</span>
-              <em v-if="item[1]?.origin">{{ item[1].origin }}</em>
+                'strikethrough': item[1]?.qualified === 0,
+                'adjusted': item[1]?.returnRate
+              }">
+                <Tooltip :title="item[1]?.returnRate ? `${item[1].source} (${item[1].returnRate}%)` : ''">{{ item[1]?.value ? item[1].value : '-' }}</Tooltip>
+              </span>
+              <em class="origin-box" v-if="item[1]?.origin">{{ item[1].origin }}</em>
             </td>
             <td>
-              <span :class="{
+              <span class="value-box" :class="{
                 'selected': selected.includes(item[2]?.key),
                 'highlight': iorGroups?.includes(item[2]?.key),
-                'strikethrough': item[2]?.qualified === 0
-              }">{{ item[2]?.value ? item[2].value : '-' }}</span>
-              <em v-if="item[2]?.origin">{{ item[2].origin }}</em>
+                'strikethrough': item[2]?.qualified === 0,
+                'adjusted': item[2]?.returnRate
+              }">
+                <Tooltip :title="item[2]?.returnRate ? `${item[2].source} (${item[2].returnRate}%)` : ''">{{ item[2]?.value ? item[2].value : '-' }}</Tooltip>
+              </span>
+              <em class="origin-box" v-if="item[2]?.origin">{{ item[2].origin }}</em>
             </td>
             <td>
-              <span :class="{
+              <span class="value-box" :class="{
                 'selected': selected.includes(item[3]?.key),
                 'highlight': iorGroups?.includes(item[3]?.key),
-                'strikethrough': item[3]?.qualified === 0
-              }">{{ item[3]?.value ? item[3].value : '-' }}</span>
-              <em v-if="item[3]?.origin">{{ item[3].origin }}</em>
+                'strikethrough': item[3]?.qualified === 0,
+                'adjusted': item[3]?.returnRate
+              }">
+                <Tooltip :title="item[3]?.returnRate ? `${item[3].source} (${item[3].returnRate}%)` : ''">{{ item[3]?.value ? item[3].value : '-' }}</Tooltip>
+              </span>
+              <em class="origin-box" v-if="item[3]?.origin">{{ item[3].origin }}</em>
             </td>
           </tr>
         </table>
@@ -157,29 +168,38 @@ defineProps({
       width: calc((100% - 64px) / 2);
       font-size: 0;
     }
-    span {
+    .value-box {
       display: inline-block;
       height: 20px;
       padding: 0 5px;
       line-height: 20px;
       vertical-align: middle;
       font-size: 14px;
-      &.strikethrough {
-        text-decoration: line-through;
-        color: hsl(var(--foreground) / 0.35);
-      }
+      border-radius: 4px;
       &.highlight {
-        border-radius: 4px;
         background-color: hsl(var(--primary) / 0.15);
         color: hsl(var(--primary));
       }
       &.selected {
-        border-radius: 4px;
         background-color: hsl(var(--primary));
         color: hsl(var(--primary-foreground));
       }
+      &.strikethrough {
+        text-decoration: line-through;
+        color: hsl(var(--foreground) / 0.35);
+      }
+      &.adjusted {
+        color: #039f00;
+        &.highlight {
+          background-color: #e2fce1;
+        }
+        &.selected {
+          color: #fff;
+          background-color: #039f00;
+        }
+      }
     }
-    em {
+    .origin-box {
       display: block;
       height: 18px;
       margin-top: -3px;

+ 8 - 2
web/apps/web-antd/src/views/match/components/solution_item.vue

@@ -115,7 +115,9 @@ const formatPsEvents = (events) => {
       key,
       value: events[key]?.v ?? 0,
       origin: events[key]?.r,
+      source: events[key]?.s,
       qualified: events[key]?.q ?? 1,
+      returnRate: events[key]?.rt,
     }));
     return {
       label,
@@ -314,8 +316,12 @@ const ob = computed(() => {
       <Tooltip v-for="({sol}, index) in solutions" :key="index"
       class="switch-btn-item"
       :class="{ 'selected': index === currentIndex }"
-      :title="`${sol.win_profit_rate}% (${sol.cross_type})`"
-      @click.stop="switchSolution(index)">{{ sol.win_average_rate }}</Tooltip>
+      @click.stop="switchSolution(index)">
+        <template #title>
+          {{sol.win_profit_rate}}% ({{sol.cross_type}})
+        </template>
+        {{ sol.win_average_rate }}
+      </Tooltip>
     </div>
   </div>
   <div class="solution-content">

+ 12 - 4
web/apps/web-antd/src/views/system/parameter/index.vue

@@ -23,6 +23,7 @@ const initialFormState = {
   pcRebateRatio: 0,
   pcRebateType: 0,
   innerRouSubsidyValue: 0,
+  innerRouMaxAdjust: 100,
   innerWmSubsidyRatio: 0,
   innerOtSubsidyRatio: 0,
   halfTimeActiveTime: 0,
@@ -104,8 +105,8 @@ onUnmounted(() => {
     <Form
       :model="formState"
       layout="horizontal"
-      :label-col="{ span: 6 }"
-      :wrapper-col="{ span: 18 }"
+      :label-col="{ span: 8 }"
+      :wrapper-col="{ span: 16 }"
       class="parameter-form"
     >
       <Form.Item
@@ -251,10 +252,17 @@ onUnmounted(() => {
       </Form.Item>
 
       <Form.Item
-        label="让胜负/大小调水"
+        label="让胜负/大小调水"
         name="innerRouSubsidyValue"
       >
-        <InputNumber :disabled="formState.syncSettingEnabled" v-model:value="formState.innerRouSubsidyValue" :min="0" :step="0.01" style="width: 200px" />
+        <InputNumber :disabled="formState.syncSettingEnabled" v-model:value="formState.innerRouSubsidyValue" :min="-1" :step="0.01" style="width: 200px" />
+      </Form.Item>
+
+      <Form.Item
+        label="让胜负/大小返还率(%)"
+        name="innerRouMaxAdjust"
+      >
+        <InputNumber :disabled="formState.syncSettingEnabled" v-model:value="formState.innerRouMaxAdjust" :min="0" :step="0.1" style="width: 200px" />
       </Form.Item>
 
       <Form.Item