bestBidAskSizeWatcher.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import { EventEmitter } from "node:events";
  2. import { MarketWsClient } from "./polymarketClient.js";
  3. const normalizeTokenIds = (tokenIds) => {
  4. if (!Array.isArray(tokenIds)) {
  5. throw new Error("tokenIds must be an array", { cause: 400 });
  6. }
  7. return [...new Set(tokenIds.map(String).filter(Boolean))];
  8. };
  9. const normalizeSize = (size) => {
  10. const value = Number(size);
  11. return Number.isFinite(value) ? value : 0;
  12. };
  13. const applyLevels = (levels = [], target) => {
  14. target.clear();
  15. levels.forEach(({ price, size }) => {
  16. if (!price) {
  17. return;
  18. }
  19. const normalizedSize = normalizeSize(size);
  20. if (normalizedSize > 0) {
  21. target.set(String(price), String(size));
  22. }
  23. });
  24. };
  25. const updateLevel = (target, price, size) => {
  26. if (!price) {
  27. return;
  28. }
  29. if (normalizeSize(size) <= 0) {
  30. target.delete(String(price));
  31. return;
  32. }
  33. target.set(String(price), String(size));
  34. };
  35. const getBestLevel = (levels, compare) => {
  36. let bestPrice;
  37. let bestSize;
  38. levels.forEach((size, price) => {
  39. if (bestPrice === undefined || compare(Number(price), Number(bestPrice))) {
  40. bestPrice = price;
  41. bestSize = size;
  42. }
  43. });
  44. if (bestPrice === undefined) {
  45. return { price: null, size: null };
  46. }
  47. return { price: bestPrice, size: bestSize };
  48. };
  49. const parseSide = (side) => {
  50. const normalizedSide = String(side || "").toLowerCase();
  51. if (["ask", "asks", "sell"].includes(normalizedSide)) {
  52. return "asks";
  53. }
  54. if (["bid", "bids", "buy"].includes(normalizedSide)) {
  55. return "bids";
  56. }
  57. return "";
  58. };
  59. class BestBidAskSizeWatcher extends EventEmitter {
  60. #tokenIds = [];
  61. #client;
  62. #books = new Map();
  63. #started = false;
  64. constructor(tokenIds = []) {
  65. super();
  66. this.#client = new MarketWsClient();
  67. this.subscribe(tokenIds);
  68. }
  69. get tokenIds() {
  70. return [...this.#tokenIds];
  71. }
  72. start() {
  73. if (this.#started) {
  74. return this;
  75. }
  76. this.#started = true;
  77. this.#client.on("open", () => {
  78. if (this.#tokenIds.length > 0) {
  79. this.#client.subscribeToTokensIds(this.#tokenIds);
  80. }
  81. this.emit("open");
  82. });
  83. this.#client.on("message", data => this.#handleMessage(data));
  84. this.#client.on("error", error => this.emit("error", error));
  85. this.#client.on("close", event => this.emit("close", event));
  86. this.#client.connect();
  87. return this;
  88. }
  89. stop() {
  90. this.#client.close();
  91. this.#started = false;
  92. return this;
  93. }
  94. subscribe(tokenIds) {
  95. const ids = normalizeTokenIds(tokenIds);
  96. const newIds = ids.filter(tokenId => !this.#books.has(tokenId));
  97. if (newIds.length === 0) {
  98. return this;
  99. }
  100. this.#tokenIds = [...this.#tokenIds, ...newIds];
  101. newIds.forEach(tokenId => {
  102. this.#books.set(tokenId, { asks: new Map(), bids: new Map() });
  103. });
  104. if (this.#started) {
  105. this.#client.subscribeToTokensIds(newIds);
  106. }
  107. return this;
  108. }
  109. unsubscribe(tokenIds) {
  110. const ids = normalizeTokenIds(tokenIds);
  111. const removeIds = ids.filter(tokenId => this.#books.has(tokenId));
  112. if (removeIds.length === 0) {
  113. return this;
  114. }
  115. const removeIdSet = new Set(removeIds);
  116. this.#tokenIds = this.#tokenIds.filter(tokenId => !removeIdSet.has(tokenId));
  117. removeIds.forEach(tokenId => {
  118. this.#books.delete(tokenId);
  119. });
  120. if (this.#started) {
  121. this.#client.unsubscribeToTokensIds(removeIds);
  122. }
  123. return this;
  124. }
  125. snapshot() {
  126. return Object.fromEntries(
  127. this.#tokenIds.map(tokenId => [tokenId, this.#getBestBidAskSize(tokenId)])
  128. );
  129. }
  130. #getBook(tokenId) {
  131. const id = String(tokenId);
  132. if (!this.#books.has(id)) {
  133. this.#books.set(id, { asks: new Map(), bids: new Map() });
  134. }
  135. return this.#books.get(id);
  136. }
  137. #getBestBidAskSize(tokenId) {
  138. const book = this.#getBook(tokenId);
  139. const ask = getBestLevel(book.asks, (price, bestPrice) => price < bestPrice);
  140. const bid = getBestLevel(book.bids, (price, bestPrice) => price > bestPrice);
  141. return {
  142. tokenId: String(tokenId),
  143. best_ask: ask.price,
  144. best_ask_size: ask.size,
  145. best_bid: bid.price,
  146. best_bid_size: bid.size,
  147. };
  148. }
  149. #emitUpdate(tokenId) {
  150. const data = this.#getBestBidAskSize(tokenId);
  151. this.emit("update", data);
  152. this.emit(String(tokenId), data);
  153. }
  154. #handleBook({ asset_id, asks = [], bids = [] }) {
  155. const tokenId = String(asset_id);
  156. if (!this.#books.has(tokenId)) {
  157. return;
  158. }
  159. const book = this.#getBook(tokenId);
  160. applyLevels(asks, book.asks);
  161. applyLevels(bids, book.bids);
  162. this.#emitUpdate(tokenId);
  163. }
  164. #handlePriceChanges(priceChanges = []) {
  165. const changedTokenIds = new Set();
  166. priceChanges.forEach(({ asset_id, price, size, side }) => {
  167. const tokenId = String(asset_id);
  168. if (!this.#books.has(tokenId)) {
  169. return;
  170. }
  171. const parsedSide = parseSide(side);
  172. if (!parsedSide) {
  173. return;
  174. }
  175. const book = this.#getBook(tokenId);
  176. updateLevel(book[parsedSide], price, size);
  177. changedTokenIds.add(tokenId);
  178. });
  179. changedTokenIds.forEach(tokenId => this.#emitUpdate(tokenId));
  180. }
  181. #handleMessage(data) {
  182. switch (data?.event_type) {
  183. case "book":
  184. this.#handleBook(data);
  185. break;
  186. case "price_change":
  187. this.#handlePriceChanges(data.price_changes);
  188. break;
  189. }
  190. }
  191. }
  192. export const watchBestBidAskSizes = (tokenIds) => {
  193. return new BestBidAskSizeWatcher(tokenIds).start();
  194. };
  195. export default BestBidAskSizeWatcher;