소스 검색

跑单支持所有可能的结果

flyzto 1 개월 전
부모
커밋
962bf04183
6개의 변경된 파일194개의 추가작업 그리고 49개의 파일을 삭제
  1. 44 0
      pinnacle/libs/cache.js
  2. 15 2
      pinnacle/libs/pinnacleClient.js
  3. 69 17
      pinnacle/main.js
  4. 4 4
      server/models/GamesPs.js
  5. 58 22
      server/triangle/trangleCalc.js
  6. 4 4
      web/apps/web-antd/src/views/match/solutions/index.vue

+ 44 - 0
pinnacle/libs/cache.js

@@ -0,0 +1,44 @@
+import fs from 'fs';
+import path from 'path';
+
+export const getData = (file) => {
+  let data = null;
+
+  if (fs.existsSync(file)) {
+    const arrayBuffer = fs.readFileSync(file);
+
+    try {
+      data = JSON.parse(arrayBuffer.toString());
+    }
+    catch (e) {}
+  }
+
+  return Promise.resolve(data);
+}
+
+export const setData = (file, data, indent = 2) => {
+  return new Promise((resolve, reject) => {
+
+    if (typeof (data) != 'string') {
+      try {
+        data = JSON.stringify(data, null, indent);
+      }
+      catch (error) {
+        reject(error);
+      }
+    }
+
+    const directoryPath = path.dirname(file);
+    if(!fs.existsSync(directoryPath)) {
+      fs.mkdirSync(directoryPath, { recursive: true });
+    }
+
+    try {
+      fs.writeFileSync(file, data);
+      resolve();
+    }
+    catch (error) {
+      reject(error);
+    }
+  });
+}

+ 15 - 2
pinnacle/libs/pinnacleClient.js

@@ -1,5 +1,6 @@
 import axios from "axios";
 import { HttpsProxyAgent } from "https-proxy-agent";
+import { Logs } from "./logs.js";
 
 const BaseURL = {
   pinnacle: "https://api.pinnacle888.com",
@@ -73,7 +74,13 @@ export const updateBaseEvents = async (data) => {
     proxy: false,
   };
 
-  return axios(axiosConfig).then(res => res.data);
+  axios(axiosConfig).then(res => res.data)
+  .then(() => {
+    Logs.outDev('update base events success', data);
+  })
+  .catch(err => {
+    Logs.err('failed to update base events:', err.message);
+  });
 }
 
 export const notifyException = async (message) => {
@@ -84,5 +91,11 @@ export const notifyException = async (message) => {
     data: { message },
     proxy: false,
   };
-  return axios(axiosConfig).then(res => res.data);
+  axios(axiosConfig).then(res => res.data)
+  .then(() => {
+    Logs.out('notify exception success');
+  })
+  .catch(err => {
+    Logs.err('failed to notify exception:', err.message);
+  });
 }

+ 69 - 17
pinnacle/main.js

@@ -1,11 +1,12 @@
-import { writeFileSync } from 'fs';
 import 'dotenv/config';
 
 import { pinnacleRequest, getPsteryRelations, updateBaseEvents, notifyException } from "./libs/pinnacleClient.js";
 import { Logs } from "./libs/logs.js";
+import { getData, setData } from "./libs/cache.js";
 
 
-const cacheFilePath = 'data/gamesCache.json';
+const gamesMapCacheFile = 'data/gamesCache.json';
+const globalDataCacheFile = 'data/globalDataCache.json';
 
 
 const GLOBAL_DATA = {
@@ -22,7 +23,7 @@ const GLOBAL_DATA = {
   // specialsOddsCount: 0,
   requestErrorCount: 0,
   loopActive: false,
-  loopResultTime: Date.now(),
+  loopResultTime: 0,
 };
 
 
@@ -666,6 +667,7 @@ const pinnacleDataLoop = () => {
     if (!GLOBAL_DATA.loopActive) {
       GLOBAL_DATA.loopActive = true;
       Logs.out('loop active');
+      notifyException('Pinnacle API startup.');
     }
 
     if (GLOBAL_DATA.requestErrorCount > 0) {
@@ -683,15 +685,16 @@ const pinnacleDataLoop = () => {
     const games = getGames();
     const data = { games, timestamp };
 
-    updateBaseEvents(data)
+    updateBaseEvents(data);
+
+    setData(gamesMapCacheFile, GLOBAL_DATA.gamesMap)
     .then(() => {
-      Logs.outDev('games data', data);
+      Logs.outDev('games map saved');
     })
     .catch(err => {
-      Logs.err('failed to update base events:', err.message);
+      Logs.err('failed to save games map', err.message);
     });
 
-    writeFileSync(cacheFilePath, JSON.stringify(GLOBAL_DATA.gamesMap, null, 2));
   })
   .catch(err => {
     Logs.err(err.message, err.source);
@@ -702,14 +705,7 @@ const pinnacleDataLoop = () => {
       GLOBAL_DATA.loopActive = false;
 
       Logs.out('loop inactive');
-
-      notifyException(`Pinnacle API ${exceptionMessage}, ${err.message}`)
-      .then(() => {
-        Logs.out('notify exception success');
-      })
-      .catch(err => {
-        Logs.err('failed to notify exception:', err.message);
-      });
+      notifyException(`Pinnacle API paused. ${exceptionMessage}. ${err.message}`);
     }
   })
   .finally(() => {
@@ -738,13 +734,69 @@ const pinnacleDataLoop = () => {
 }
 
 
+/**
+ * 缓存GLOBAL_DATA数据到文件
+ */
+const saveGlobalDataToCache = async () => {
+  return setData(globalDataCacheFile, GLOBAL_DATA);
+}
+
+const loadGlobalDataFromCache = async () => {
+  return getData(globalDataCacheFile)
+  .then(data => {
+    if (!data) {
+      return;
+    }
+    Object.keys(GLOBAL_DATA).forEach(key => {
+      if (key in data) {
+        GLOBAL_DATA[key] = data[key];
+      }
+    });
+  });
+}
+
+// 监听进程退出事件,保存GLOBAL_DATA数据
+const saveExit = (code) => {
+  saveGlobalDataToCache()
+  .then(() => {
+    Logs.out('global data saved');
+  })
+  .catch(err => {
+    Logs.err('failed to save global data', err.message);
+  })
+  .finally(() => {
+    process.exit(code);
+  });
+}
+process.on('SIGINT', () => {
+  saveExit(0);
+});
+process.on('SIGTERM', () => {
+  saveExit(0);
+});
+process.on('SIGUSR2', () => {
+  saveExit(0);
+});
+
+
 (() => {
   if (!process.env.PINNACLE_USERNAME || !process.env.PINNACLE_PASSWORD) {
     Logs.err('USERNAME or PASSWORD is not set');
     return;
   }
-  GLOBAL_DATA.loopActive = true;
-  getFiltedGames().then(pinnacleDataLoop);
+  loadGlobalDataFromCache()
+  .then(() => {
+    Logs.out('global data loaded');
+  })
+  .catch(err => {
+    Logs.err('failed to load global data', err.message);
+  })
+  .finally(() => {
+    GLOBAL_DATA.loopResultTime = Date.now();
+    GLOBAL_DATA.loopActive = true;
+    return getFiltedGames();
+  })
+  .then(pinnacleDataLoop);
 })();
 
 

+ 4 - 4
server/models/GamesPs.js

@@ -874,8 +874,8 @@ const updateSolutions = (solutions, eventsLogsMap) => {
 
     // syncSolutions(solutionUpdate);
 
-    if (updateIds.add.length / solutions.length > 0.25 ||
-      updateIds.remove.length / solutions.length > 0.25
+    if (updateIds.add.length / solutions.length > 0.2 ||
+      updateIds.remove.length / solutions.length > 0.2
     ) {
       const { expireEvents, removeEvents } = eventsLogsMap;
       const expireEvemtsMap = {};
@@ -1319,9 +1319,9 @@ const saveGamesToCache = () => {
  * 从缓存文件加载GAMES数据
  */
 const loadGamesFromCache = () => {
-  const gamesCacheData = Cache.getData(GamesCacheFile, true);
+  const gamesCacheData = Cache.getData(GamesCacheFile, true) ?? {};
   Object.keys(GAMES).forEach(key => {
-    if (gamesCacheData[key]) {
+    if (key in gamesCacheData) {
       GAMES[key] = gamesCacheData[key];
     }
   });

+ 58 - 22
server/triangle/trangleCalc.js

@@ -23,6 +23,12 @@ const oddRebateValue = (odds, platform, key) => {
   }
   return odds + rebateRatio / 100;
 }
+
+const cartesianOdds = (selection) => {
+  const [a, b, c] = selection;
+  return a.flatMap(itemA => b.flatMap(itemB => c.map(itemC => [itemA, itemB, itemC])));
+}
+
 const getOptimalSelections = (odds, rules) => {
   const results = [];
 
@@ -48,42 +54,55 @@ const getOptimalSelections = (odds, rules) => {
             isValid = false;
             break;
           }
-          selection.push({
+          selection.push([{
             k: key,
             p: 'ps',
             v: item.ps.v,
             r: item.ps.r,
             s: item.ps.s,
             o: item
-          });
+          }]);
         }
         else {
-          const candidates = ['ob', 'hg', 'no'].filter((k) => k in item);
+          const { ps, ...itemRest } = item;
+          // const candidates = ['ob', 'hg', 'no'].filter((k) => k in item);
+          const candidates = Object.keys(itemRest);
           if (candidates.length === 0) {
             isValid = false;
             break;
           }
           // Logs.out('candidates', candidates)
-          const best = candidates.reduce((a, b) => {
-            const aValue = oddRebateValue(item[a].v-1, a, key);
-            const bValue = oddRebateValue(item[b].v-1, b, key);
-            const seletcted = aValue > bValue ? a : b;
-            return seletcted;
-          });
+          // const best = candidates.reduce((a, b) => {
+          //   const aValue = oddRebateValue(item[a].v-1, a, key);
+          //   const bValue = oddRebateValue(item[b].v-1, b, key);
+          //   const seletcted = aValue > bValue ? a : b;
+          //   return seletcted;
+          // });
           // Logs.out('best', item, best)
-          selection.push({
+          // selection.push({
+          //   k: key,
+          //   p: best,
+          //   v: item[best].v,
+          //   r: item[best].r,
+          //   s: item[best].s,
+          //   o: item
+          // });
+          selection.push(candidates.map(k => ({
             k: key,
-            p: best,
-            v: item[best].v,
-            r: item[best].r,
-            s: item[best].s,
+            p: k,
+            v: item[k].v,
+            r: item[k].r,
+            s: item[k].s,
             o: item
-          });
+          })));
         }
       }
 
       if (isValid) {
-        validOptions.push(selection);
+        const cartesian = cartesianOdds(selection);
+        cartesian.forEach(item => {
+          validOptions.push(item);
+        });
       }
     }
 
@@ -100,7 +119,10 @@ const getOptimalSelections = (odds, rules) => {
     //   results.push({ rule, iors, ruleIndex });
     // }
   });
-
+  // if (results.length) {
+  //   Logs.outDev('results', results);
+  // }
+  // return [];
   return results;
 }
 
@@ -124,6 +146,15 @@ const sortCpr = (cpr) => {
   return temp;
 }
 
+/**
+ * 获取平台类型
+ */
+const getPlatformKey = (cpr) => {
+  const platforms = sortCpr(cpr).map(item => item.p);
+  return [...new Set(platforms)].join('_');
+}
+
+
 /**
  * 添加返佣
  */
@@ -150,6 +181,7 @@ const attachRebate = (ior) => {
 }
 
 const eventsCombination = (passableEvents) => {
+  // Logs.outDev('passableEvents', passableEvents);
   const { innerDefaultAmount, innerRebateRatio } = getSetting();
   const solutions = [];
   passableEvents.forEach(events => {
@@ -189,12 +221,16 @@ const eventsCombination = (passableEvents) => {
         }
         if (!isNaN(sol?.win_average)) {
           const id = info.id;
-          const sortedCpr = sortCpr(cpr);
-          const keys = sortedCpr.map(item => `${item.k}`).join('_');
-          const sid = crypto.createHash('sha1').update(`${id}_${keys}`).digest('hex');
-          const crpGroup = `${id}_${sortedCpr[0].k}`;
+          // const sortedCpr = sortCpr(cpr);
+          const keys = cpr.map(item => `${item.k}-${item.p}`).join('##');
+          // Logs.outDev('keys', `${id}##${keys}`)
+          const sid = crypto.createHash('sha1').update(`${id}-${keys}`).digest('hex');
+          const crpGroup = `${id}_${cpr.find(item => item.p == 'ps').k}`;
+          // Logs.outDev('crpGroup', crpGroup)
+          const platformKey = getPlatformKey(cpr);
+          // Logs.outDev('platformKey', platformKey, cpr)
           const timestamp = Date.now();
-          solutions.push({sid, sol, cpr, info, group: crpGroup, rule: `${iorGroup}:${ruleIndex}`, timestamp});
+          solutions.push({sid, sol, cpr, cross: platformKey, info, group: crpGroup, rule: `${iorGroup}:${ruleIndex}`, timestamp});
         }
       });
     });

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

@@ -217,10 +217,10 @@ onUnmounted(() => {
         <Form layout="inline" class="sol-opt-container">
           <Form.Item label="比赛类型" class="sol-opt-item">
             <RadioGroup v-model:value="marketType">
-              <Radio :value="-1">全部({{ markCount.all ?? 0 }})</Radio>
-              <Radio :value="2">滚球({{ markCount.rollball ?? 0 }})</Radio>
-              <Radio :value="1">今日({{ markCount.today ?? 0 }})</Radio>
-              <Radio :value="0">早盘({{ markCount.early ?? 0 }})</Radio>
+              <Radio :value="-1">全部({{ markCount?.all ?? 0 }})</Radio>
+              <Radio :value="2">滚球({{ markCount?.rollball ?? 0 }})</Radio>
+              <Radio :value="1">今日({{ markCount?.today ?? 0 }})</Radio>
+              <Radio :value="0">早盘({{ markCount?.early ?? 0 }})</Radio>
             </RadioGroup>
           </Form.Item>
           <Form.Item label="盘口类型" class="sol-opt-item">