Markets.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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') {
  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) {
  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) => {
  255. return getLineInfo(info);
  256. }
  257. /**
  258. * 获取平台盘口详细信息
  259. */
  260. export const getPlatformIorsDetailInfo = async (ior, platform, id) => {
  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);
  271. }
  272. }
  273. return getInfo[platform]?.();
  274. }
  275. /**
  276. * 根据最新赔率获取策略
  277. */
  278. export const getSolutionByLatestIors = (iorsInfo, cross_type, retry=false) => {
  279. const askIndex = +retry;
  280. const iorsValues = iorsInfo.map(item => {
  281. if (item.asks) {
  282. const bestAsk = [...item.asks].sort((a, b) => a.price - b.price)[askIndex];
  283. const value = fixFloat(1 / bestAsk.price, 3);
  284. const maxStake = fixFloat(bestAsk.size * bestAsk.price * USDC_RATE);
  285. const minStake = fixFloat(item.min_order_size * bestAsk.price * USDC_RATE);
  286. return { value, maxStake, minStake, bestPrice: bestAsk.price };
  287. }
  288. else if (item.info) {
  289. const value = item.info.price;
  290. const maxStake = Math.floor(item.info.maxRiskStake);
  291. const minStake = Math.ceil(item.info.minRiskStake*10);
  292. return { value, maxStake, minStake };
  293. }
  294. });
  295. const nullIndex = iorsValues.findIndex(item => item.value == null);
  296. if (nullIndex >= 0) {
  297. return { error: `IORS_NULL_VALUE_AT_INDEX_${nullIndex}_RETRY_${askIndex}`, data: iorsInfo };
  298. }
  299. if (iorsValues.length === 2) {
  300. iorsValues.push({ value: 1, maxStake: 0, minStake: 0 });
  301. }
  302. const baseIndex = iorsValues.reduce((minIdx, cur, idx) => cur.value < iorsValues[minIdx].value ? idx : minIdx, 0);
  303. const betInfo = {
  304. cross_type,
  305. base_index: baseIndex,
  306. base_stake: 10000,
  307. odds_side_a: fixFloat(iorsValues[0].value - 1),
  308. odds_side_b: fixFloat(iorsValues[1].value - 1),
  309. odds_side_c: fixFloat(iorsValues[2].value - 1),
  310. };
  311. const sol = eventSolutions(betInfo, true);
  312. const { win_average, win_profit_rate, gold_side_a, gold_side_b, gold_side_c } = sol;
  313. if (win_profit_rate < MIN_PROFIT_RATE) {
  314. Logs.outDev('win_profit_rate is less than profit rate limit', sol, iorsValues, iorsInfo, cross_type);
  315. return { error: `WIN_PROFIT_RATE_LESS_THAN_MIN_PROFIT_RATE_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
  316. }
  317. const goldRatios = [gold_side_a, gold_side_b];
  318. if (gold_side_c) {
  319. goldRatios.push(gold_side_c);
  320. }
  321. const minVals = iorsValues.map(item => item.minStake);
  322. const maxVals = iorsValues.map(item => item.maxStake);
  323. const stakeLimit = findMaxMinGroup(goldRatios, minVals, maxVals);
  324. const { scaleForMax, scaleForMin } = stakeLimit;
  325. if (scaleForMax < scaleForMin) {
  326. Logs.outDev('scaleForMax is less than scaleForMin');
  327. if (!retry) {
  328. return getSolutionByLatestIors(iorsInfo, cross_type, true);
  329. }
  330. else {
  331. return { error: `NO_ENOUGH_STAKE_SIZE_TO_BET_RETRY_${askIndex}`, data: { sol, iorsValues, iorsInfo } };
  332. }
  333. }
  334. const winLimit = {
  335. max: fixFloat(win_average * scaleForMax / (gold_side_a + gold_side_b + gold_side_c)),
  336. min: fixFloat(win_average * scaleForMin / (gold_side_a + gold_side_b + gold_side_c)),
  337. }
  338. return { sol, iors: iorsValues, stakeLimit, winLimit };
  339. }
  340. export const getSoulutionBetResult = async ({ iors, iorsInfo, stakeLimit, stake=0 }) => {
  341. const maxStake = stakeLimit.maxGroup.reduce((acc, curr) => acc + curr, 0);
  342. const minStake = stakeLimit.minGroup.reduce((acc, curr) => acc + curr, 0);
  343. let betStakeGroup = [];
  344. if (stake > maxStake || stake < 0) {
  345. stake = maxStake;
  346. betStakeGroup = stakeLimit.maxGroup;
  347. }
  348. else if (stake < minStake) {
  349. stake = minStake;
  350. betStakeGroup = stakeLimit.minGroup;
  351. }
  352. else {
  353. betStakeGroup = stakeLimit.proportions.map(p => p * stake);
  354. }
  355. const betInfo = iorsInfo.map((item, index) => {
  356. if (item.asks) {
  357. const bestPrice = +iors[index].bestPrice;
  358. const stakeSize = fixFloat(betStakeGroup[index] / USDC_RATE / bestPrice, 0); // 必须保证买单金额小数不超过2位
  359. return { ...item, stakeSize, bestPrice, betIndex: index, platform: 'polymarket' }
  360. }
  361. else if (item.info) {
  362. const stakeSize = fixFloat(betStakeGroup[index], 0);
  363. return { ...item, stakeSize, betIndex: index, platform: 'pinnacle' }
  364. }
  365. }).sort((a, b) => {
  366. if (a.platform === 'polymarket' && b.platform === 'pinnacle') {
  367. return -1;
  368. }
  369. else if (a.platform === 'pinnacle' && b.platform === 'polymarket') {
  370. return 1;
  371. }
  372. else {
  373. return 0;
  374. }
  375. });
  376. // return { betInfo };
  377. return runSequentially(betInfo.map(item => async() => {
  378. if (item.asks) {
  379. const result = await polymarketPlaceOrder(item);
  380. return [result, item.betIndex]
  381. }
  382. else if (item.info) {
  383. const result = await pinnaclePlaceOrder(item);
  384. return [result, item.betIndex]
  385. }
  386. })).then(results => {
  387. return results.sort((a, b) => a[1] - b[1]).map(item => item[0]);
  388. }).catch(error => {
  389. Logs.errDev(error);
  390. return Promise.reject(error);
  391. });
  392. }