parseMarkets.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import getDateInTimezone from "./getDateInTimezone.js";
  2. /**
  3. * 精确浮点数字
  4. * @param {number} number
  5. * @param {number} x
  6. * @returns {number}
  7. */
  8. const fixFloat = (number, x=3) => {
  9. return parseFloat(number.toFixed(x));
  10. }
  11. /**
  12. * 清理对象中的undefined值
  13. * @param {*} obj
  14. * @returns {Object}
  15. */
  16. const cleanUndefined = (obj) => {
  17. return Object.fromEntries(
  18. Object.entries(obj).filter(([, v]) => v !== undefined)
  19. );
  20. }
  21. /**
  22. * 解析赛事标题
  23. * @param {*} eventTitle
  24. * @returns {Object}
  25. */
  26. const parseTeamData = (eventTitle) => {
  27. let titleSpliter;
  28. if (eventTitle.includes(' vs. ')) {
  29. titleSpliter = ' vs. ';
  30. }
  31. else if (eventTitle.includes(' vs ')) {
  32. titleSpliter = ' vs ';
  33. }
  34. if (!titleSpliter) {
  35. return {
  36. teamHomeName: '',
  37. teamAwayName: '',
  38. };
  39. }
  40. const teamData = eventTitle.replace(' - More Markets', '').split(titleSpliter);
  41. return {
  42. teamHomeName: teamData[0],
  43. teamAwayName: teamData[1],
  44. };
  45. }
  46. /**
  47. * 解析盘口键值对
  48. * 示例:
  49. * {
  50. * "Yes": 0.525,
  51. * "No": 0.475
  52. * }
  53. * @param {*} outcomes
  54. * @param {*} outcomePrices
  55. * @returns {Object}
  56. */
  57. const parseOutcomes = (outcomes, clobTokenIds, bestBid=0, bestAsk=0) => {
  58. if (!outcomes || !clobTokenIds) {
  59. return {};
  60. }
  61. const keys = JSON.parse(outcomes);
  62. const ids = JSON.parse(clobTokenIds);
  63. return keys.reduce((obj, key, index) => {
  64. let best_ask;
  65. let best_bid;
  66. if (index === 0) {
  67. best_ask = (bestAsk).toFixed(3);
  68. best_bid = (bestBid).toFixed(3);
  69. }
  70. else if (index === 1) {
  71. best_ask = (1 - bestBid).toFixed(3);
  72. best_bid = (1 - bestAsk).toFixed(3);
  73. }
  74. obj[key] = { id: ids[index], best_ask, best_bid };
  75. return obj;
  76. }, {});
  77. }
  78. /**
  79. * 解析让球方向
  80. * 让球方向为正数时,返回'a',否则返回''
  81. * @param {*} ratio
  82. * @returns {string}
  83. */
  84. const ratioAccept = (ratio) => {
  85. if (ratio > 0) {
  86. return 'a';
  87. }
  88. return '';
  89. }
  90. /**
  91. * 解析盘口比率
  92. * 比率取绝对值,并去掉小数点后的数字
  93. * @param {*} ratio
  94. * @returns {string}
  95. */
  96. const ratioString = (ratio) => {
  97. ratio = Math.abs(ratio);
  98. ratio = ratio.toString();
  99. ratio = ratio.replace(/\./, '');
  100. return ratio;
  101. }
  102. /**
  103. * 解析盘口数据
  104. * 使用卖单最优价格
  105. * @param {*} markets
  106. * @returns {Object}
  107. */
  108. export const parseOdds = (markets) => {
  109. const odds = {};
  110. Object.keys(markets).forEach(key => {
  111. const marketData = markets[key];
  112. if (key === 'moneyline') {
  113. Object.keys(marketData).forEach(side => {
  114. const askYes = marketData[side].outcomes['Yes']['best_ask'];
  115. const tokenYes = marketData[side].outcomes['Yes']['id'];
  116. const askNo = marketData[side].outcomes['No']['best_ask'];
  117. const tokenNo = marketData[side].outcomes['No']['id'];
  118. const slug = marketData[side].market.slug;
  119. if (askYes <= 0.1 || askNo <= 0.1) {
  120. return;
  121. }
  122. const iorYes = fixFloat(1 / askYes);
  123. const iorNo = fixFloat(1 / askNo);
  124. let iorKeyYes = '';
  125. let iorKeyNo = '';
  126. switch (side) {
  127. case 'Home':
  128. iorKeyYes = 'ior_mh';
  129. iorKeyNo = 'ior_moh';
  130. break;
  131. case 'Draw':
  132. iorKeyYes = 'ior_mn';
  133. iorKeyNo = 'ior_mon';
  134. break;
  135. case 'Away':
  136. iorKeyYes = 'ior_mc';
  137. iorKeyNo = 'ior_moc';
  138. break;
  139. }
  140. odds[iorKeyYes] = { v: iorYes, ask: askYes, token: tokenYes, slug };
  141. odds[iorKeyNo] = { v: iorNo, ask: askNo, token: tokenNo, slug };
  142. });
  143. }
  144. else if (key === 'spreads') {
  145. Object.keys(marketData).forEach(handicap => {
  146. const ratio = +handicap;
  147. const askHome = marketData[handicap].outcomes['Home']['best_ask'];
  148. const tokenHome = marketData[handicap].outcomes['Home']['id'];
  149. const askAway = marketData[handicap].outcomes['Away']['best_ask'];
  150. const tokenAway = marketData[handicap].outcomes['Away']['id'];
  151. const slug = marketData[handicap].market.slug;
  152. if (askHome <= 0.1 || askAway <= 0.1) {
  153. return;
  154. }
  155. const iorHome = fixFloat(1 / askHome);
  156. const iorAway = fixFloat(1 / askAway);
  157. odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, ask: askHome, token: tokenHome, slug };
  158. odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, ask: askAway, token: tokenAway, slug };
  159. });
  160. }
  161. else if (key === 'totals') {
  162. Object.keys(marketData).forEach(handicap => {
  163. const ratio = +handicap;
  164. const askOver = marketData[handicap].outcomes['Over']['best_ask'];
  165. const tokenOver = marketData[handicap].outcomes['Over']['id'];
  166. const askUnder = marketData[handicap].outcomes['Under']['best_ask'];
  167. const tokenUnder = marketData[handicap].outcomes['Under']['id'];
  168. const slug = marketData[handicap].market.slug;
  169. if (askOver <= 0.1 || askUnder <= 0.1) {
  170. return;
  171. }
  172. const iorOver = fixFloat(1 / askOver);
  173. const iorUnder = fixFloat(1 / askUnder);
  174. odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, ask: askOver, token: tokenOver, slug };
  175. odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, ask: askUnder, token: tokenUnder, slug };
  176. });
  177. }
  178. });
  179. return odds;
  180. }
  181. /**
  182. * 解析盘口数据
  183. * 当卖单最低价格与买单最高价格差值超过最小变动价位时
  184. * 创建新的买单价格,即买单最高价格+最小变动价位
  185. * 否则,使用买单最高价格
  186. * @param {*} markets
  187. * @returns {Object}
  188. */
  189. export const parseOddsBid = (markets) => {
  190. const odds = {};
  191. Object.keys(markets).forEach(key => {
  192. const marketData = markets[key];
  193. if (key === 'moneyline') {
  194. Object.keys(marketData).forEach(side => {
  195. const askYes = marketData[side].outcomes['Yes']['best_ask'];
  196. const bidYes = marketData[side].outcomes['Yes']['best_bid'];
  197. const tokenYes = marketData[side].outcomes['Yes']['id'];
  198. const askNo = marketData[side].outcomes['No']['best_ask'];
  199. const bidNo = marketData[side].outcomes['No']['best_bid'];
  200. const tokenNo = marketData[side].outcomes['No']['id'];
  201. const slug = marketData[side].market.slug;
  202. const tick_size = marketData[side].market.orderPriceMinTickSize;
  203. if (askYes <= 0.1 || askNo <= 0.1) {
  204. return;
  205. }
  206. const iorYes = fixFloat(1 / askYes);
  207. const iorNo = fixFloat(1 / askNo);
  208. let iorKeyYes = '';
  209. let iorKeyNo = '';
  210. switch (side) {
  211. case 'Home':
  212. iorKeyYes = 'ior_mh';
  213. iorKeyNo = 'ior_moh';
  214. break;
  215. case 'Draw':
  216. iorKeyYes = 'ior_mn';
  217. iorKeyNo = 'ior_mon';
  218. break;
  219. case 'Away':
  220. iorKeyYes = 'ior_mc';
  221. iorKeyNo = 'ior_moc';
  222. break;
  223. }
  224. odds[iorKeyYes] = { v: iorYes, ask: askYes, bid: bidYes, tick_size, token: tokenYes, slug };
  225. odds[iorKeyNo] = { v: iorNo, ask: askNo, bid: bidNo, tick_size, token: tokenNo, slug };
  226. });
  227. }
  228. else if (key === 'spreads') {
  229. Object.keys(marketData).forEach(handicap => {
  230. const ratio = +handicap;
  231. const askHome = marketData[handicap].outcomes['Home']['best_ask'];
  232. const bidHome = marketData[handicap].outcomes['Home']['best_bid'];
  233. const tokenHome = marketData[handicap].outcomes['Home']['id'];
  234. const askAway = marketData[handicap].outcomes['Away']['best_ask'];
  235. const bidAway = marketData[handicap].outcomes['Away']['best_bid'];
  236. const tokenAway = marketData[handicap].outcomes['Away']['id'];
  237. const slug = marketData[handicap].market.slug;
  238. const tick_size = marketData[handicap].market.orderPriceMinTickSize;
  239. if (askHome <= 0.1 || askAway <= 0.1) {
  240. return;
  241. }
  242. const iorHome = fixFloat(1 / askHome);
  243. const iorAway = fixFloat(1 / askAway);
  244. odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, ask: askHome, bid: bidHome, tick_size, token: tokenHome, slug };
  245. odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, ask: askAway, bid: bidAway, tick_size, token: tokenAway, slug };
  246. });
  247. }
  248. else if (key === 'totals') {
  249. Object.keys(marketData).forEach(handicap => {
  250. const ratio = +handicap;
  251. const askOver = marketData[handicap].outcomes['Over']['best_ask'];
  252. const bidOver = marketData[handicap].outcomes['Over']['best_bid'];
  253. const tokenOver = marketData[handicap].outcomes['Over']['id'];
  254. const askUnder = marketData[handicap].outcomes['Under']['best_ask'];
  255. const bidUnder = marketData[handicap].outcomes['Under']['best_bid'];
  256. const tokenUnder = marketData[handicap].outcomes['Under']['id'];
  257. const slug = marketData[handicap].market.slug;
  258. const tick_size = marketData[handicap].market.orderPriceMinTickSize;
  259. if (askOver <= 0.1 || askUnder <= 0.1) {
  260. return;
  261. }
  262. const iorOver = fixFloat(1 / askOver);
  263. const iorUnder = fixFloat(1 / askUnder);
  264. odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, ask: askOver, bid: bidOver, tick_size, token: tokenOver, slug };
  265. odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, ask: askUnder, bid: bidUnder, tick_size, token: tokenUnder, slug };
  266. });
  267. }
  268. });
  269. return odds;
  270. }
  271. /**
  272. * 解析胜平负盘口
  273. * @param {*} groupItemThreshold
  274. * @param {*} outcomesMap
  275. * @param {*} market
  276. * @returns {Object}
  277. */
  278. const parseMoneyline = (groupItemTitle, outcomesMap, teams, market) => {
  279. const { teamHomeName, teamAwayName } = teams;
  280. let key = '';
  281. if (groupItemTitle == teamHomeName) {
  282. key = 'Home';
  283. }
  284. else if (groupItemTitle == teamAwayName) {
  285. key = 'Away';
  286. }
  287. else if (groupItemTitle.startsWith('Draw')) {
  288. key = 'Draw';
  289. }
  290. if (!key) {
  291. return {};
  292. }
  293. return { [key]: { outcomes: outcomesMap, market } };
  294. };
  295. /**
  296. * 解析让球盘口
  297. * @param {*} groupItemTitle
  298. * @param {*} spreadsLine
  299. * @param {*} outcomesMap
  300. * @param {*} teams
  301. * @param {*} market
  302. * @returns {Object}
  303. */
  304. const parseSpreads = (groupItemTitle, spreadsLine, outcomesMap, teams, market) => {
  305. const { teamHomeName, teamAwayName } = teams;
  306. let spreadDirection = 0;
  307. if (groupItemTitle.includes(teamHomeName)) {
  308. spreadDirection = 1;
  309. }
  310. else if (groupItemTitle.includes(teamAwayName)) {
  311. spreadDirection = -1;
  312. }
  313. const spreads = spreadsLine * spreadDirection;
  314. const key = spreads > 0 ? `+${spreads}` : `${spreads}`;
  315. const spreadsMap = {};
  316. Object.keys(outcomesMap).forEach(key => {
  317. if (key == teamHomeName) {
  318. spreadsMap['Home'] = outcomesMap[key];
  319. }
  320. else if (key == teamAwayName) {
  321. spreadsMap['Away'] = outcomesMap[key];
  322. }
  323. });
  324. return { [key]: { outcomes: spreadsMap, market } };
  325. };
  326. /**
  327. * 解析大小盘口
  328. * @param {*} line
  329. * @param {*} outcomesMap
  330. * @param {*} market
  331. * @returns {Object}
  332. */
  333. const parseTotals = (line, outcomesMap, market) => {
  334. return { [line]: { outcomes: outcomesMap, market } };
  335. };
  336. /**
  337. * 解析市场数据
  338. * @param {*} markets
  339. * @param {*} teams
  340. * @returns {Object}
  341. */
  342. const parseMarketsData = (markets, teams) => {
  343. const marketsData = {};
  344. markets.forEach(market => {
  345. const { sportsMarketType, groupItemTitle, line, outcomes, clobTokenIds, bestBid=0, bestAsk=0 } = market;
  346. const outcomesMap = parseOutcomes(outcomes, clobTokenIds, bestBid, bestAsk);
  347. if (sportsMarketType === "moneyline") {
  348. if (!marketsData.moneyline) {
  349. marketsData.moneyline = {};
  350. }
  351. Object.assign(marketsData.moneyline, parseMoneyline(groupItemTitle, outcomesMap, teams, market))
  352. }
  353. else if (sportsMarketType === "spreads") {
  354. if (!marketsData.spreads) {
  355. marketsData.spreads = {};
  356. }
  357. Object.assign(marketsData.spreads, parseSpreads(groupItemTitle, line, outcomesMap, teams, market))
  358. }
  359. else if (sportsMarketType === "totals") {
  360. if (!marketsData.totals) {
  361. marketsData.totals = {};
  362. }
  363. Object.assign(marketsData.totals, parseTotals(line, outcomesMap, market));
  364. }
  365. });
  366. return marketsData;
  367. }
  368. /**
  369. * 解析赛事数据
  370. * @param {*} event
  371. * @returns {Object}
  372. */
  373. const parseEvent = (event) => {
  374. const { id, title, series, gameId, parentEventId, startTime } = event;
  375. const { teamHomeName, teamAwayName } = parseTeamData(title);
  376. const leagueId = series[0].id;
  377. const leagueName = series[0].title;
  378. const timestamp = new Date(startTime).getTime();
  379. return {
  380. id: +id,
  381. gameId: gameId ? +gameId : undefined,
  382. parentEventId: parentEventId ? +parentEventId : undefined,
  383. leagueId: +leagueId,
  384. teamHomeName, teamAwayName, leagueName,
  385. timestamp,
  386. startTime: getDateInTimezone('+8', timestamp, true),
  387. };
  388. }
  389. /**
  390. * 解析市场列表数据
  391. * @param {*} events
  392. * @returns {Object}
  393. */
  394. export const parseMarkets = (eventsData) => {
  395. const mergedMarketsData = {};
  396. Object.values(eventsData).map(event => {
  397. const item = parseEvent(event);
  398. const { markets } = event;
  399. const { teamHomeName, teamAwayName } = item;
  400. const marketsData = parseMarketsData(markets, { teamHomeName, teamAwayName });
  401. return { ...item, marketsData };
  402. }).sort((a, b) => a.id - b.id).forEach(item => {
  403. if (item.id && !item.parentEventId) {
  404. mergedMarketsData[item.id] = cleanUndefined(item);
  405. }
  406. else if (item.parentEventId && mergedMarketsData[item.parentEventId] && item.marketsData) {
  407. const { marketsData: parentEvents } = mergedMarketsData[item.parentEventId];
  408. Object.assign(parentEvents, item.marketsData);
  409. }
  410. });
  411. return mergedMarketsData;
  412. }
  413. // export default parseMarkets;