Markets.js 14 KB

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