Markets.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import path from "path";
  2. import { fileURLToPath } from "url";
  3. import { getData } from "../libs/cache.js";
  4. import Logs from "../libs/logs.js";
  5. import eventSolutions from '../triangle/eventSolutions.js';
  6. import { getOrderBook, placeOrder as polymarketPlaceOrder } from "../../polymarket/libs/polymarketClient.js";
  7. import { getLineInfo, placeOrder as pinnaclePlaceOrder } from "../../pinnacle/libs/pinnacleClient.js";
  8. const __filename = fileURLToPath(import.meta.url);
  9. const __dirname = path.dirname(__filename);
  10. const polymarketMarketsCacheFile = path.join(__dirname, "../../polymarket/cache/polymarketMarketsCache.json");
  11. const pinnacleGamesCacheFile = path.join(__dirname, "../../pinnacle/cache/pinnacleGamesCache.json");
  12. /**
  13. * 最小盈利率
  14. */
  15. const MIN_PROFIT_RATE = -1;
  16. /**
  17. * 精确浮点数字
  18. * @param {number} number
  19. * @param {number} x
  20. * @returns {number}
  21. */
  22. const fixFloat = (number, x=3) => {
  23. return parseFloat(number.toFixed(x));
  24. }
  25. /**
  26. * 依次执行任务
  27. */
  28. const runSequentially = async (tasks) => {
  29. return new Promise(async (resolve, reject) => {
  30. const results = [];
  31. let isError = false;
  32. for (const task of tasks) {
  33. // task 必须是一个「返回 Promise 的函数」
  34. const res = await task().catch(err => {
  35. err.results = results;
  36. isError = true;
  37. reject(err);
  38. });
  39. if (isError) {
  40. break;
  41. }
  42. else if (res) {
  43. results.push(res);
  44. }
  45. }
  46. if (isError) {
  47. return;
  48. }
  49. resolve(results);
  50. })
  51. }
  52. /**
  53. * 计算符合比例的最大和最小数组
  54. */
  55. const findMaxMinGroup = (ratios, minVals, maxVals) => {
  56. const n = ratios.length;
  57. // 1. 计算比例总和 & 归一化比例
  58. const totalRatio = ratios.reduce((a, b) => a + b, 0);
  59. const proportions = ratios.map(r => r / totalRatio);
  60. // 2. 计算每个位置允许的最大/最小倍数
  61. let maxPossibleScale = Infinity;
  62. let minPossibleScale = 0; // 如果允许0,通常从0开始
  63. for (let i = 0; i < n; i++) {
  64. // 上限约束
  65. if (proportions[i] > 0) {
  66. maxPossibleScale = Math.min(maxPossibleScale, maxVals[i] / proportions[i]);
  67. }
  68. // 下限约束(如果 minVals[i] > 0 才有意义)
  69. if (proportions[i] > 0 && minVals[i] > 0) {
  70. minPossibleScale = Math.max(minPossibleScale, minVals[i] / proportions[i]);
  71. }
  72. }
  73. // 3. 最终取值
  74. const maxGroup = proportions.map(p => p * maxPossibleScale);
  75. const minGroup = proportions.map(p => p * Math.max(minPossibleScale, 0));
  76. return {
  77. maxGroup: maxGroup.map(v => fixFloat(v)), // 可控制精度
  78. minGroup: minGroup.map(v => fixFloat(v)),
  79. proportions: proportions.map(v => fixFloat(v)),
  80. scaleForMax: fixFloat(maxPossibleScale),
  81. scaleForMin: fixFloat(minPossibleScale)
  82. };
  83. }
  84. /**
  85. * 解析盘口信息
  86. */
  87. const parseRatio = (ratioString) => {
  88. if (!ratioString) {
  89. return null;
  90. }
  91. return parseFloat(`${ratioString[0]}.${ratioString.slice(1)}`);
  92. }
  93. const parseIor = (ior) => {
  94. const iorMatch = ior.match(/ior_(m|r|ou|wm|ot)([ao])?([hcn])?_?(\d+)?/);
  95. if (!iorMatch) {
  96. return null;
  97. }
  98. const [, type, action, side, ratio] = iorMatch;
  99. return { type, action, side, ratio };
  100. }
  101. const getPolymarketIorInfo = async (ior, id) => {
  102. const cacheData = await getData(polymarketMarketsCacheFile);
  103. const marketsData = cacheData[id]?.marketsData;
  104. if (!marketsData) {
  105. Logs.outDev('polymarket markets data not found', id);
  106. return null;
  107. }
  108. const iorOptions = parseIor(ior);
  109. if (!iorOptions) {
  110. Logs.outDev('polymarket ior options not found', ior);
  111. return null;
  112. }
  113. const { type, action, side, ratio } = iorOptions;
  114. let marketTypeData, outcomesSide;
  115. if (type === 'm' && !ratio) {
  116. const sideKey = side === 'h' ? 'Home' : side === 'c' ? 'Away' : 'Draw';
  117. const sideAction = action === 'o' ? 'No' : 'Yes';
  118. marketTypeData = marketsData.moneyline[sideKey];
  119. outcomesSide = sideAction;
  120. }
  121. else if (type === 'r') {
  122. const sideKey = side === 'h' ? 'Home' : side === 'c' ? 'Away' : '';
  123. let ratioDirection = 1;
  124. if (side === 'c' && action === 'a' || side === 'h' && !action) {
  125. ratioDirection = -1;
  126. }
  127. const ratioValue = parseRatio(ratio) * ratioDirection;
  128. const ratioKey = ratioValue > 0 ? `+${ratioValue}` : `${ratioValue}`;
  129. marketTypeData = marketsData.spreads?.[ratioKey];
  130. outcomesSide = sideKey;
  131. }
  132. else if (type === 'ou') {
  133. const sideKey = side === 'c' ? 'Over' : side === 'h' ? 'Under' : '';
  134. const ratioKey = parseRatio(ratio);
  135. marketTypeData = marketsData.totals[ratioKey];
  136. outcomesSide = sideKey;
  137. }
  138. const result = marketTypeData?.outcomes?.[outcomesSide];
  139. if (!result) {
  140. Logs.outDev('polymarket market type data not found', { ior, id, type, action, side, ratio, marketTypeData, outcomesSide });
  141. return null;
  142. }
  143. return result;
  144. }
  145. const getPinnacleIorInfo = async (ior, id) => {
  146. const cacheData = await getData(pinnacleGamesCacheFile);
  147. const gamesData = cacheData[id];
  148. if (!gamesData) {
  149. Logs.outDev('pinnacle games data not found', id);
  150. return null;
  151. }
  152. const iorOptions = parseIor(ior);
  153. if (!iorOptions) {
  154. Logs.outDev('pinnacle ior options not found', ior);
  155. return null;
  156. }
  157. const { type, action, side, ratio } = iorOptions;
  158. const { leagueId, id: eventId, home: homeTeamName, away: awayTeamName, periods={}, specials={}} = gamesData;
  159. const straightData = periods.straight ?? {};
  160. const { lineId: straightLineId, moneyline, spreads, totals } = straightData;
  161. const { winningMargin, exactTotalGoals } = specials;
  162. if (type === 'm' && moneyline && !action && !ratio) {
  163. const sideKey = side === 'h' ? 'home' : side === 'c' ? 'away' : 'draw';
  164. const team = side === 'h' ? 'TEAM1' : side === 'c' ? 'TEAM2' : 'DRAW';
  165. const odds = moneyline[sideKey];
  166. return { leagueId, eventId, betType: 'MONEYLINE', team, lineId: straightLineId, odds };
  167. }
  168. else if (type === 'r' && spreads) {
  169. let ratioDirection = 1;
  170. if (side === 'c' && action === 'a' || side === 'h' && !action) {
  171. ratioDirection = -1;
  172. }
  173. const ratioKey = parseRatio(ratio) * ratioDirection;
  174. const itemSpread = spreads.find(spread => spread.hdp == ratioKey);
  175. if (!itemSpread) {
  176. Logs.outDev('pinnacle item spread not found', id, type, action, side, ratio);
  177. return null;
  178. }
  179. const { altLineId=null, home, away } = itemSpread;
  180. const odds = side === 'h' ? home : away;
  181. const team = side === 'h' ? 'TEAM1' : 'TEAM2';
  182. const handicap = ratioKey * (side === 'h' ? 1 : -1);
  183. return { leagueId, eventId, handicap, betType: 'SPREAD', team, lineId: straightLineId, altLineId, odds };
  184. }
  185. else if (type === 'ou' && totals) {
  186. const ratioKey = parseRatio(ratio);
  187. const itemTotal = totals.find(total => total.points == ratioKey);
  188. if (!itemTotal) {
  189. Logs.outDev('pinnacle item total not found', id, type, action, side, ratio);
  190. return null;
  191. }
  192. const { altLineId=null, over, under } = itemTotal;
  193. const odds = side === 'c' ? over : under;
  194. const sideKey = side === 'c' ? 'OVER' : 'UNDER';
  195. return { leagueId, eventId, handicap: ratioKey, betType: 'TOTAL_POINTS', side: sideKey, lineId: straightLineId, altLineId, odds };
  196. }
  197. else if (type === 'wm' && winningMargin) {
  198. const ratioKey = parseRatio(ratio);
  199. const { id: specialId } = winningMargin;
  200. const wmName = side === 'h' ? `${homeTeamName} By ${ratioKey}` : side === 'c' ? `${awayTeamName} By ${ratioKey}` : '';
  201. const wmItem = winningMargin.contestants.find(contestant => contestant.name == wmName);
  202. if (!wmItem) {
  203. Logs.outDev('pinnacle item winning margin not found', id, type, action, side, ratio);
  204. return null;
  205. }
  206. const { id: contestantId, lineId, price } = wmItem;
  207. return { leagueId, eventId, specialId, contestantId, lineId, odds: price };
  208. }
  209. else if (type === 'ot' && exactTotalGoals) {
  210. const ratioKey = parseRatio(ratio);
  211. const { id: specialId } = exactTotalGoals;
  212. const otItem = exactTotalGoals.contestants.find(contestant => contestant.name == ratioKey);
  213. if (!otItem) {
  214. Logs.outDev('pinnacle item exact total goals not found', id, type, action, side, ratio);
  215. return null;
  216. }
  217. const { id: contestantId, lineId, price } = otItem;
  218. return { leagueId, eventId, specialId, contestantId, lineId, odds: price };
  219. }
  220. else {
  221. Logs.outDev('pinnacle ior type not found', ior, id);
  222. return null;
  223. }
  224. }
  225. /**
  226. * 获取平台盘口id信息
  227. */
  228. export const getPlatformIorInfo = async (ior, platform, id) => {
  229. const getInfo = {
  230. polymarket() {
  231. return getPolymarketIorInfo(ior, id);
  232. },
  233. pinnacle() {
  234. return getPinnacleIorInfo(ior, id);
  235. }
  236. }
  237. Logs.outDev('getPlatformIorInfo', { ior, platform, id });
  238. return getInfo[platform]?.();
  239. }
  240. /**
  241. * 获取polymarket盘口详细信息
  242. */
  243. const getPolymarketIorDetailInfo = async (info) => {
  244. const { id } = info;
  245. return getOrderBook(id);
  246. }
  247. /**
  248. * 获取pinnacle盘口详细信息
  249. */
  250. const getPinnacleIorDetailInfo = async (info, channel) => {
  251. return getLineInfo(info, channel);
  252. }
  253. /**
  254. * 获取平台盘口详细信息
  255. */
  256. export const getPlatformIorsDetailInfo = async (ior, platform, id, channel) => {
  257. const info = await getPlatformIorInfo(ior, platform, id);
  258. if (!info) {
  259. return Promise.reject(new Error('platform ior info not found', { cause: 400 }));
  260. }
  261. const getInfo = {
  262. polymarket() {
  263. return getPolymarketIorDetailInfo(info);
  264. },
  265. pinnacle() {
  266. return getPinnacleIorDetailInfo(info, channel);
  267. }
  268. }
  269. return getInfo[platform]?.();
  270. }
  271. /**
  272. * 平台盘口下注
  273. */
  274. export const placePlatformOrder = async (ior, platform, id, stake=0, channel) => {
  275. const iorInfo = await getPlatformIorsDetailInfo(ior, platform, id, channel);
  276. const betInfo = { ...iorInfo, stakeSize: stake };
  277. const placeOrder = {
  278. polymarket() {
  279. return polymarketPlaceOrder(betInfo);
  280. },
  281. pinnacle() {
  282. return pinnaclePlaceOrder(betInfo, channel);
  283. }
  284. }
  285. return placeOrder[platform]?.();
  286. }
  287. /**
  288. * 根据最新赔率获取策略
  289. */
  290. export const getSolutionByLatestIors = (iorsInfo, cross_type, retry=false) => {
  291. const askIndex = +retry;
  292. const iorsValues = iorsInfo.map(item => {
  293. if (item.asks) {
  294. const bestAsk = [...item.asks].sort((a, b) => a.price - b.price)[askIndex];
  295. const value = fixFloat(1 / bestAsk.price, 3);
  296. const maxStake = fixFloat(bestAsk.size * bestAsk.price);
  297. const minStake = fixFloat(item.min_order_size * bestAsk.price);
  298. return { value, maxStake, minStake, bestPrice: bestAsk.price };
  299. }
  300. else if (item.info) {
  301. const value = item.info.price;
  302. const maxStake = Math.floor(item.info.maxRiskStake);
  303. const minStake = Math.ceil(item.info.minRiskStake*10);
  304. return { value, maxStake, minStake };
  305. }
  306. });
  307. const nullIndex = iorsValues.findIndex(item => item.value == null);
  308. if (nullIndex >= 0) {
  309. return { error: `IORS_NULL_VALUE_AT_INDEX_${nullIndex}_RETRY_${askIndex}`, data: iorsInfo };
  310. }
  311. const baseIndex = iorsValues.reduce((minIdx, cur, idx) => cur.value < iorsValues[minIdx].value ? idx : minIdx, 0);
  312. if (iorsValues.length === 2) {
  313. iorsValues.push({ value: 1, maxStake: 0, minStake: 0 });
  314. }
  315. const betInfo = {
  316. cross_type,
  317. base_index: baseIndex,
  318. base_stake: 10000,
  319. odds_side_a: fixFloat(iorsValues[0].value - 1),
  320. odds_side_b: fixFloat(iorsValues[1].value - 1),
  321. odds_side_c: fixFloat(iorsValues[2].value - 1),
  322. };
  323. const sol = eventSolutions(betInfo, true);
  324. const { win_average, win_profit_rate, gold_side_a, gold_side_b, gold_side_c } = sol;
  325. if (win_profit_rate < MIN_PROFIT_RATE) {
  326. Logs.outDev('win_profit_rate is less than profit rate limit', sol, iorsValues, iorsInfo, cross_type);
  327. return { error: `WIN_PROFIT_RATE_LESS_THAN_MIN_PROFIT_RATE_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
  328. }
  329. const goldRatios = [gold_side_a, gold_side_b];
  330. if (gold_side_c) {
  331. goldRatios.push(gold_side_c);
  332. }
  333. const minVals = iorsValues.map(item => item.minStake);
  334. const maxVals = iorsValues.map(item => item.maxStake);
  335. const stakeLimit = findMaxMinGroup(goldRatios, minVals, maxVals);
  336. const { scaleForMax, scaleForMin } = stakeLimit;
  337. if (scaleForMax < scaleForMin) {
  338. Logs.outDev('scaleForMax is less than scaleForMin');
  339. if (!retry) {
  340. return getSolutionByLatestIors(iorsInfo, cross_type, true);
  341. }
  342. else {
  343. return { error: `NO_ENOUGH_STAKE_SIZE_TO_BET_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
  344. }
  345. }
  346. const winLimit = {
  347. max: fixFloat(win_average * scaleForMax / (gold_side_a + gold_side_b + gold_side_c)),
  348. min: fixFloat(win_average * scaleForMin / (gold_side_a + gold_side_b + gold_side_c)),
  349. }
  350. return { sol, iors: iorsValues, stakeLimit, winLimit };
  351. }
  352. export const getSoulutionBetResult = async ({ iors, iorsInfo, stakeLimit, stake=0 }) => {
  353. const maxStake = stakeLimit.maxGroup.reduce((acc, curr) => acc + curr, 0);
  354. const minStake = stakeLimit.minGroup.reduce((acc, curr) => acc + curr, 0);
  355. let betStakeGroup = [];
  356. if (stake > maxStake || stake < 0) {
  357. stake = maxStake;
  358. betStakeGroup = stakeLimit.maxGroup;
  359. }
  360. else if (stake < minStake) {
  361. stake = minStake;
  362. betStakeGroup = stakeLimit.minGroup;
  363. }
  364. else {
  365. betStakeGroup = stakeLimit.proportions.map(p => p * stake);
  366. }
  367. const betInfo = iorsInfo.map((item, index) => {
  368. if (item.asks) {
  369. const bestPrice = +iors[index].bestPrice;
  370. const stakeSize = fixFloat(betStakeGroup[index] / bestPrice, 0); // 必须保证买单金额小数不超过2位
  371. return { ...item, stakeSize, bestPrice, betIndex: index, platform: 'polymarket' }
  372. }
  373. else if (item.info) {
  374. const stakeSize = fixFloat(betStakeGroup[index], 0);
  375. return { ...item, stakeSize, betIndex: index, platform: 'pinnacle' }
  376. }
  377. }).sort((a, b) => {
  378. if (a.platform === 'polymarket' && b.platform === 'pinnacle') {
  379. return -1;
  380. }
  381. else if (a.platform === 'pinnacle' && b.platform === 'polymarket') {
  382. return 1;
  383. }
  384. else {
  385. return 0;
  386. }
  387. });
  388. // return { betInfo };
  389. return runSequentially(betInfo.map(item => async() => {
  390. if (item.asks) {
  391. const result = await polymarketPlaceOrder(item);
  392. return [result, item.betIndex]
  393. }
  394. else if (item.info) {
  395. const result = await pinnaclePlaceOrder(item);
  396. return [result, item.betIndex]
  397. }
  398. })).then(results => {
  399. return results.sort((a, b) => a[1] - b[1]).map(item => item[0]);
  400. }).catch(error => {
  401. Logs.errDev(error);
  402. return Promise.reject(error);
  403. });
  404. }