syncData.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import 'dotenv/config';
  2. import path from "node:path";
  3. import { fileURLToPath } from "node:url";
  4. import Logs from "./logs.js";
  5. import { setData } from "./cache.js";
  6. import getDateInTimezone from "./getDateInTimezone.js";
  7. import { getEvents, platformPost } from "./polymarketClient.js";
  8. import BestBidAskSizeWatcher from "./bestBidAskSizeWatcher.js";
  9. import { parseMarkets, parseOddsAsk, parseOddsBid, parseIorDetail } from "./parseMarkets.js";
  10. const __filename = fileURLToPath(import.meta.url);
  11. const __dirname = path.dirname(__filename);
  12. const IS_DEV = process.env.NODE_ENV == 'development';
  13. const IS_BID = process.env.PPAI_RUN_MODE == 'BID';
  14. const eventsCacheFile = path.join(__dirname, "../cache/polymarketEventsCache.json");
  15. const marketsDataFile = path.join(__dirname, "../cache/polymarketMarketsCache.json");
  16. const GLOBAL_DATA = {
  17. eventsData: {},
  18. marketsData: {},
  19. clobTokenMap: {},
  20. filteredLeagues: [],
  21. relatedGames: [],
  22. marketWatcher: null,
  23. wsClientData: null,
  24. lastChangeTime: 0,
  25. };
  26. // /**
  27. // * 获取有效联赛
  28. // * @param {*} marketsList
  29. // * @param {*} soccerSports
  30. // * @returns {Array}
  31. // */
  32. // const getLeagues = (marketsList, soccerSports) => {
  33. // const soccerSportsMap = new Map(soccerSports.map(item => [+item.series, item]));
  34. // const leaguesList = marketsList.map(item => {
  35. // const { leagueId: id, leagueName: name } = item;
  36. // const sport = soccerSportsMap.get(+id)?.sport;
  37. // return { id, name, sport };
  38. // });
  39. // // 去重并排序
  40. // const leaguesMap = new Map(leaguesList.map(item => [item.id, item]));
  41. // return Array.from(leaguesMap.values()).sort((a, b) => a.id - b.id);
  42. // }
  43. // /**
  44. // * 获取足球联赛
  45. // * @returns {Promise}
  46. // */
  47. // const getSoccerSports = async () => {
  48. // return fetchMarketData({ url: "/sports" })
  49. // .then(sportsData => {
  50. // return sportsData.filter(item => {
  51. // const { tags } = item;
  52. // const tagIds = tags.split(",").map(item => +item);
  53. // return tagIds.includes(100350);
  54. // });
  55. // });
  56. // }
  57. // /**
  58. // * 提交联赛和比赛数据
  59. // * @param {string} platform - 平台名称
  60. // * @param {string} url - 请求地址
  61. // * @param {Object} data - 请求数据
  62. // * @returns {Promise}
  63. // */
  64. // const submitLeaguesAndGames = async ({ leagues, games }) => {
  65. // const submitLeagues = platformPost('/api/platforms/update_leagues', { platform: 'polymarket', leagues });
  66. // const submitGames = platformPost('/api/platforms/update_games', { platform: 'polymarket', games });
  67. // return Promise.all([submitLeagues, submitGames]);
  68. // }
  69. // /**
  70. // * 更新联赛和比赛数据
  71. // */
  72. // const updateLeaguesAndGames = (marketsData) => {
  73. // const marketsList = Object.values(marketsData);
  74. // getSoccerSports()
  75. // .then(soccerSports => {
  76. // const leagues = getLeagues(marketsList, soccerSports);
  77. // const games = marketsList.map(game => {
  78. // const { marketsData, ...rest } = game;
  79. // return rest;
  80. // }).sort((a, b) => a.timestamp - b.timestamp);
  81. // return { leagues, games };
  82. // })
  83. // .then(data => {
  84. // Logs.outDev('update leagues and games list', data.leagues.length, data.games.length);
  85. // return submitLeaguesAndGames(data);
  86. // })
  87. // .then(() => {
  88. // Logs.outDev('leagues and games list updated');
  89. // })
  90. // .catch(error => {
  91. // Logs.out('failed to update leagues and games list', error.message);
  92. // Logs.errDev(error);
  93. // });
  94. // }
  95. // /**
  96. // * 获取过滤后的比赛数据
  97. // * @returns
  98. // */
  99. // const updateRelatedGames = () => {
  100. // platformGet('/api/platforms/get_related_games', { platform: 'polymarket' })
  101. // .then(res => {
  102. // const { data: relatedGames } = res;
  103. // Logs.outDev('relatedGames updated', relatedGames.length);
  104. // GLOBAL_DATA.relatedGames = relatedGames.map(item => item.id);
  105. // })
  106. // .catch(error => {
  107. // Logs.out('failed to update related games', error.message);\
  108. // Logs.errDev(error);
  109. // })
  110. // .finally(() => {
  111. // setTimeout(() => {
  112. // updateRelatedGames();
  113. // }, 1000 * 10);
  114. // });
  115. // }
  116. /**
  117. * 获取赛事数据
  118. * @returns {Promise}
  119. */
  120. const getMarketsData = async () => {
  121. const endDateMinStamps = Date.now() - 1000 * 60 * 60 * 2;
  122. const endDateMin = new Date(endDateMinStamps).toISOString();
  123. const tomorrowDateMinus4 = getDateInTimezone(-4, Date.now()+24*60*60*1000);
  124. const tomorrowGmtMinus4EndTime = new Date(`${tomorrowDateMinus4} 23:59:59 GMT-4`).getTime();
  125. const endDateMax = new Date(tomorrowGmtMinus4EndTime).toISOString();
  126. // const todayDateMinus4 = getDateInTimezone(-4, Date.now() + 24*60*60*1000);
  127. // const todayGmtMinus4EndTime = new Date(`${todayDateMinus4} 23:59:59 GMT-4`).getTime();
  128. // const startDateMax = new Date(todayGmtMinus4EndTime).toISOString();
  129. Logs.outDev('getMarketsData', endDateMin, endDateMax);
  130. return getEvents({ endDateMin, endDateMax })
  131. .then(events => {
  132. const { eventsData } = GLOBAL_DATA;
  133. events.forEach(event => {
  134. const { id } = event;
  135. if (!eventsData[id]) {
  136. eventsData[id] = event;
  137. }
  138. });
  139. const eventsMap = new Map(events.map(event => [event.id, event]));
  140. Object.keys(eventsData).forEach(id => {
  141. if (!eventsMap.has(id)) {
  142. delete eventsData[id];
  143. }
  144. });
  145. setData(eventsCacheFile, eventsData);
  146. return parseMarkets(eventsData);
  147. })
  148. .then(marketsData => {
  149. // updateLeaguesAndGames(marketsData);
  150. const { marketsData: oldMarketsData } = GLOBAL_DATA;
  151. const newMarketsData = marketsData;
  152. const marketsDataUpdate = {
  153. add: [],
  154. remove: []
  155. }
  156. Object.keys(oldMarketsData).forEach(id => {
  157. if (!newMarketsData[id]) {
  158. delete oldMarketsData[id];
  159. marketsDataUpdate.remove.push(id);
  160. }
  161. });
  162. Object.keys(newMarketsData).forEach(id => {
  163. if (!oldMarketsData[id]) {
  164. oldMarketsData[id] = newMarketsData[id];
  165. marketsDataUpdate.add.push(id);
  166. }
  167. });
  168. const clobToken = Object.values(oldMarketsData).map(item => item.marketsData)
  169. .flatMap(item => Object.values(item))
  170. .flatMap(item => Object.values(item))
  171. .flatMap(item => Object.values(item.outcomes));
  172. const { clobTokenMap: oldClobTokenMap } = GLOBAL_DATA;
  173. const newClobTokenMap = new Map(clobToken.map(item => [item.id, item]));
  174. const clobTokenUpdate = {
  175. add: [],
  176. remove: []
  177. }
  178. Object.keys(oldClobTokenMap).forEach(id => {
  179. if (!newClobTokenMap.has(id)) {
  180. delete oldClobTokenMap[id];
  181. clobTokenUpdate.remove.push(id);
  182. }
  183. });
  184. newClobTokenMap.forEach(item => {
  185. const { id } = item;
  186. if (!oldClobTokenMap[id]) {
  187. oldClobTokenMap[id] = newClobTokenMap.get(id);
  188. clobTokenUpdate.add.push(id);
  189. }
  190. });
  191. return clobTokenUpdate;
  192. });
  193. }
  194. /**
  195. * 循环更新
  196. */
  197. const updateGamesMarkets = () => {
  198. Logs.outDev('updateGamesMarkets');
  199. getMarketsData()
  200. .then(clobTokenUpdate => {
  201. const { marketWatcher } = GLOBAL_DATA;
  202. const { add, remove } = clobTokenUpdate;
  203. if (add.length > 0) {
  204. Logs.outDev('subscribeToTokensIds', add);
  205. marketWatcher?.subscribe(add);
  206. }
  207. if (remove.length > 0) {
  208. Logs.outDev('unsubscribeToTokensIds', remove);
  209. marketWatcher?.unsubscribe(remove);
  210. }
  211. })
  212. .catch(error => {
  213. Logs.out('failed to update games markets', error.message);
  214. Logs.errDev(error);
  215. })
  216. .finally(() => {
  217. setTimeout(() => {
  218. updateGamesMarkets();
  219. }, 1000 * 60);
  220. });
  221. }
  222. const syncMarketsData = () => {
  223. const { marketsData } = GLOBAL_DATA;
  224. setTimeout(() => {
  225. syncMarketsData();
  226. }, 1000 * 5);
  227. if (IS_DEV) {
  228. setData(marketsDataFile, marketsData);
  229. }
  230. }
  231. /**
  232. * 获取比赛盘口赔率数据
  233. * @returns {Object}
  234. */
  235. const getGamesEvents = () => {
  236. const { marketsData, lastChangeTime } = GLOBAL_DATA;
  237. // Logs.outDev('getGamesEvents', marketsData, lastChangeTime);
  238. const games = Object.values(marketsData).map(item => {
  239. const { marketsData, ...rest } = item;
  240. const odds = IS_BID ? parseOddsBid(marketsData) : parseOddsAsk(marketsData);
  241. // const odds = parseOddsBid(marketsData);
  242. return { ...rest, odds };
  243. }).sort((a, b) => a.timestamp - b.timestamp);
  244. return { games, timestamp: lastChangeTime };
  245. }
  246. /**
  247. * 更新赔率数据
  248. */
  249. const updateOdds = async () => {
  250. const { games, timestamp } = getGamesEvents();
  251. const expireTime = Date.now() - 30_000;
  252. if (!games.length || timestamp < expireTime ) {
  253. return Promise.resolve();
  254. }
  255. Logs.outDev('updateOdds', games, timestamp);
  256. return platformPost('/api/platforms/update_odds', { platform: 'polymarket', games, timestamp });
  257. }
  258. /**
  259. * 定时更新赔率数据
  260. */
  261. const updateOddsLoop = () => {
  262. updateOdds()
  263. .catch(error => {
  264. Logs.out('failed to update odds', error.message);
  265. Logs.errDev(error);
  266. })
  267. .finally(() => {
  268. setTimeout(() => {
  269. updateOddsLoop();
  270. }, 1000 * 2);
  271. });
  272. }
  273. const updateClobTokenBestBidAskSize = (data) => {
  274. const { tokenId, best_ask, best_ask_size, best_bid, best_bid_size } = data;
  275. const { clobTokenMap } = GLOBAL_DATA;
  276. const clobToken = clobTokenMap[tokenId];
  277. if (!clobToken) {
  278. return;
  279. }
  280. Object.assign(clobToken, { best_ask, best_ask_size, best_bid, best_bid_size });
  281. GLOBAL_DATA.lastChangeTime = Date.now();
  282. }
  283. /**
  284. * 启动同步市场数据
  285. */
  286. export const startSyncMarketsData = () => {
  287. const marketWatcher = new BestBidAskSizeWatcher();
  288. GLOBAL_DATA.marketWatcher = marketWatcher;
  289. marketWatcher.on('open', () => {
  290. // updateRelatedGames();
  291. updateGamesMarkets();
  292. syncMarketsData();
  293. updateOddsLoop();
  294. });
  295. marketWatcher.on('update', data => {
  296. updateClobTokenBestBidAskSize(data);
  297. });
  298. marketWatcher.on('error', error => {
  299. Logs.err('error', error);
  300. });
  301. marketWatcher.on('close', event => {
  302. Logs.outDev('close', event);
  303. });
  304. marketWatcher.start();
  305. }
  306. /**
  307. * 获取盘口详情
  308. * @param {*} ior
  309. * @param {*} id
  310. * @returns
  311. */
  312. export const getIorInfo = (ior, id) => {
  313. if (!id || !ior) {
  314. return Promise.reject({ cause: 400, message: 'id and ior are required', data: { id, ior } });
  315. }
  316. const { marketsData } = GLOBAL_DATA;
  317. const iorInfo = parseIorDetail(ior, id, marketsData);
  318. if (iorInfo.cause === 400) {
  319. return Promise.reject({ cause: 400, message: iorInfo.message, data: { id, ior } });
  320. }
  321. Logs.outDev('getIorInfo', { id, ior }, iorInfo);
  322. return Promise.resolve(iorInfo);
  323. }