@catalyst-team/poly-sdk 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/README.md +665 -807
  2. package/README.zh-CN.md +645 -342
  3. package/dist/__tests__/integration/arbitrage-service.integration.test.d.ts +12 -0
  4. package/dist/__tests__/integration/arbitrage-service.integration.test.d.ts.map +1 -0
  5. package/dist/__tests__/integration/arbitrage-service.integration.test.js +267 -0
  6. package/dist/__tests__/integration/arbitrage-service.integration.test.js.map +1 -0
  7. package/dist/__tests__/integration/data-api.integration.test.js +6 -3
  8. package/dist/__tests__/integration/data-api.integration.test.js.map +1 -1
  9. package/dist/__tests__/integration/market-service.integration.test.d.ts +10 -0
  10. package/dist/__tests__/integration/market-service.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/integration/market-service.integration.test.js +173 -0
  12. package/dist/__tests__/integration/market-service.integration.test.js.map +1 -0
  13. package/dist/__tests__/integration/realtime-service-v2.integration.test.d.ts +10 -0
  14. package/dist/__tests__/integration/realtime-service-v2.integration.test.d.ts.map +1 -0
  15. package/dist/__tests__/integration/realtime-service-v2.integration.test.js +307 -0
  16. package/dist/__tests__/integration/realtime-service-v2.integration.test.js.map +1 -0
  17. package/dist/__tests__/integration/trading-service.integration.test.d.ts +10 -0
  18. package/dist/__tests__/integration/trading-service.integration.test.d.ts.map +1 -0
  19. package/dist/__tests__/integration/trading-service.integration.test.js +58 -0
  20. package/dist/__tests__/integration/trading-service.integration.test.js.map +1 -0
  21. package/dist/clients/clob-api.d.ts +73 -0
  22. package/dist/clients/clob-api.d.ts.map +1 -1
  23. package/dist/clients/clob-api.js +60 -0
  24. package/dist/clients/clob-api.js.map +1 -1
  25. package/dist/clients/ctf-client.d.ts +6 -4
  26. package/dist/clients/ctf-client.d.ts.map +1 -1
  27. package/dist/clients/ctf-client.js.map +1 -1
  28. package/dist/clients/data-api.d.ts +333 -15
  29. package/dist/clients/data-api.d.ts.map +1 -1
  30. package/dist/clients/data-api.js +398 -26
  31. package/dist/clients/data-api.js.map +1 -1
  32. package/dist/clients/gamma-api.d.ts +5 -0
  33. package/dist/clients/gamma-api.d.ts.map +1 -1
  34. package/dist/clients/gamma-api.js +2 -0
  35. package/dist/clients/gamma-api.js.map +1 -1
  36. package/dist/clients/subgraph.d.ts +196 -0
  37. package/dist/clients/subgraph.d.ts.map +1 -0
  38. package/dist/clients/subgraph.js +332 -0
  39. package/dist/clients/subgraph.js.map +1 -0
  40. package/dist/clients/websocket-manager.d.ts +3 -0
  41. package/dist/clients/websocket-manager.d.ts.map +1 -1
  42. package/dist/clients/websocket-manager.js +10 -3
  43. package/dist/clients/websocket-manager.js.map +1 -1
  44. package/dist/core/cache.d.ts +3 -0
  45. package/dist/core/cache.d.ts.map +1 -1
  46. package/dist/core/cache.js +5 -0
  47. package/dist/core/cache.js.map +1 -1
  48. package/dist/core/errors.d.ts +2 -1
  49. package/dist/core/errors.d.ts.map +1 -1
  50. package/dist/core/errors.js +2 -0
  51. package/dist/core/errors.js.map +1 -1
  52. package/dist/core/rate-limiter.d.ts +3 -1
  53. package/dist/core/rate-limiter.d.ts.map +1 -1
  54. package/dist/core/rate-limiter.js +12 -0
  55. package/dist/core/rate-limiter.js.map +1 -1
  56. package/dist/core/types.d.ts +205 -13
  57. package/dist/core/types.d.ts.map +1 -1
  58. package/dist/core/types.js +30 -0
  59. package/dist/core/types.js.map +1 -1
  60. package/dist/core/types.test.d.ts +7 -0
  61. package/dist/core/types.test.d.ts.map +1 -0
  62. package/dist/core/types.test.js +122 -0
  63. package/dist/core/types.test.js.map +1 -0
  64. package/dist/index.d.ts +84 -18
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +139 -132
  67. package/dist/index.js.map +1 -1
  68. package/dist/scripts/dip-arb/auto-trade.d.ts +20 -0
  69. package/dist/scripts/dip-arb/auto-trade.d.ts.map +1 -0
  70. package/dist/scripts/dip-arb/auto-trade.js +373 -0
  71. package/dist/scripts/dip-arb/auto-trade.js.map +1 -0
  72. package/dist/scripts/dip-arb/example-basic.d.ts +30 -0
  73. package/dist/scripts/dip-arb/example-basic.d.ts.map +1 -0
  74. package/dist/scripts/dip-arb/example-basic.js +222 -0
  75. package/dist/scripts/dip-arb/example-basic.js.map +1 -0
  76. package/dist/scripts/dip-arb/redeem-positions.d.ts +11 -0
  77. package/dist/scripts/dip-arb/redeem-positions.d.ts.map +1 -0
  78. package/dist/scripts/dip-arb/redeem-positions.js +201 -0
  79. package/dist/scripts/dip-arb/redeem-positions.js.map +1 -0
  80. package/dist/scripts/dip-arb/scan-markets.d.ts +6 -0
  81. package/dist/scripts/dip-arb/scan-markets.d.ts.map +1 -0
  82. package/dist/scripts/dip-arb/scan-markets.js +73 -0
  83. package/dist/scripts/dip-arb/scan-markets.js.map +1 -0
  84. package/dist/services/arbitrage-service.d.ts +3 -2
  85. package/dist/services/arbitrage-service.d.ts.map +1 -1
  86. package/dist/services/arbitrage-service.js +71 -43
  87. package/dist/services/arbitrage-service.js.map +1 -1
  88. package/dist/services/binance-service.d.ts +154 -0
  89. package/dist/services/binance-service.d.ts.map +1 -0
  90. package/dist/services/binance-service.js +266 -0
  91. package/dist/services/binance-service.js.map +1 -0
  92. package/dist/services/dip-arb-service.d.ts +209 -0
  93. package/dist/services/dip-arb-service.d.ts.map +1 -0
  94. package/dist/services/dip-arb-service.js +1602 -0
  95. package/dist/services/dip-arb-service.js.map +1 -0
  96. package/dist/services/dip-arb-types.d.ts +553 -0
  97. package/dist/services/dip-arb-types.d.ts.map +1 -0
  98. package/dist/services/dip-arb-types.js +164 -0
  99. package/dist/services/dip-arb-types.js.map +1 -0
  100. package/dist/services/market-service.d.ts +267 -8
  101. package/dist/services/market-service.d.ts.map +1 -1
  102. package/dist/services/market-service.js +771 -42
  103. package/dist/services/market-service.js.map +1 -1
  104. package/dist/services/onchain-service.d.ts +309 -0
  105. package/dist/services/onchain-service.d.ts.map +1 -0
  106. package/dist/services/onchain-service.js +417 -0
  107. package/dist/services/onchain-service.js.map +1 -0
  108. package/dist/services/realtime-service-v2.d.ts +362 -0
  109. package/dist/services/realtime-service-v2.d.ts.map +1 -0
  110. package/dist/services/realtime-service-v2.js +858 -0
  111. package/dist/services/realtime-service-v2.js.map +1 -0
  112. package/dist/services/realtime-service.d.ts +17 -17
  113. package/dist/services/realtime-service.d.ts.map +1 -1
  114. package/dist/services/realtime-service.js +91 -59
  115. package/dist/services/realtime-service.js.map +1 -1
  116. package/dist/services/smart-money-service.d.ts +352 -0
  117. package/dist/services/smart-money-service.d.ts.map +1 -0
  118. package/dist/services/smart-money-service.js +582 -0
  119. package/dist/services/smart-money-service.js.map +1 -0
  120. package/dist/services/trading-service.d.ts +177 -0
  121. package/dist/services/trading-service.d.ts.map +1 -0
  122. package/dist/services/trading-service.js +422 -0
  123. package/dist/services/trading-service.js.map +1 -0
  124. package/dist/services/wallet-service.d.ts +225 -3
  125. package/dist/services/wallet-service.d.ts.map +1 -1
  126. package/dist/services/wallet-service.js +511 -3
  127. package/dist/services/wallet-service.js.map +1 -1
  128. package/dist/src/__tests__/integration/arbitrage-service.integration.test.d.ts +12 -0
  129. package/dist/src/__tests__/integration/arbitrage-service.integration.test.d.ts.map +1 -0
  130. package/dist/src/__tests__/integration/arbitrage-service.integration.test.js +267 -0
  131. package/dist/src/__tests__/integration/arbitrage-service.integration.test.js.map +1 -0
  132. package/dist/src/__tests__/integration/bridge-client.integration.test.d.ts +11 -0
  133. package/dist/src/__tests__/integration/bridge-client.integration.test.d.ts.map +1 -0
  134. package/dist/src/__tests__/integration/bridge-client.integration.test.js +260 -0
  135. package/dist/src/__tests__/integration/bridge-client.integration.test.js.map +1 -0
  136. package/dist/src/__tests__/integration/ctf-client.integration.test.d.ts +17 -0
  137. package/dist/src/__tests__/integration/ctf-client.integration.test.d.ts.map +1 -0
  138. package/dist/src/__tests__/integration/ctf-client.integration.test.js +234 -0
  139. package/dist/src/__tests__/integration/ctf-client.integration.test.js.map +1 -0
  140. package/dist/src/__tests__/integration/data-api.integration.test.d.ts +9 -0
  141. package/dist/src/__tests__/integration/data-api.integration.test.d.ts.map +1 -0
  142. package/dist/src/__tests__/integration/data-api.integration.test.js +164 -0
  143. package/dist/src/__tests__/integration/data-api.integration.test.js.map +1 -0
  144. package/dist/src/__tests__/integration/gamma-api.integration.test.d.ts +9 -0
  145. package/dist/src/__tests__/integration/gamma-api.integration.test.d.ts.map +1 -0
  146. package/dist/src/__tests__/integration/gamma-api.integration.test.js +170 -0
  147. package/dist/src/__tests__/integration/gamma-api.integration.test.js.map +1 -0
  148. package/dist/src/__tests__/integration/market-service.integration.test.d.ts +10 -0
  149. package/dist/src/__tests__/integration/market-service.integration.test.d.ts.map +1 -0
  150. package/dist/src/__tests__/integration/market-service.integration.test.js +180 -0
  151. package/dist/src/__tests__/integration/market-service.integration.test.js.map +1 -0
  152. package/dist/src/__tests__/integration/realtime-service-v2.integration.test.d.ts +10 -0
  153. package/dist/src/__tests__/integration/realtime-service-v2.integration.test.d.ts.map +1 -0
  154. package/dist/src/__tests__/integration/realtime-service-v2.integration.test.js +307 -0
  155. package/dist/src/__tests__/integration/realtime-service-v2.integration.test.js.map +1 -0
  156. package/dist/src/__tests__/integration/trading-service.integration.test.d.ts +10 -0
  157. package/dist/src/__tests__/integration/trading-service.integration.test.d.ts.map +1 -0
  158. package/dist/src/__tests__/integration/trading-service.integration.test.js +58 -0
  159. package/dist/src/__tests__/integration/trading-service.integration.test.js.map +1 -0
  160. package/dist/src/__tests__/test-utils.d.ts +92 -0
  161. package/dist/src/__tests__/test-utils.d.ts.map +1 -0
  162. package/dist/src/__tests__/test-utils.js +143 -0
  163. package/dist/src/__tests__/test-utils.js.map +1 -0
  164. package/dist/src/clients/bridge-client.d.ts +388 -0
  165. package/dist/src/clients/bridge-client.d.ts.map +1 -0
  166. package/dist/src/clients/bridge-client.js +587 -0
  167. package/dist/src/clients/bridge-client.js.map +1 -0
  168. package/dist/src/clients/ctf-client.d.ts +475 -0
  169. package/dist/src/clients/ctf-client.d.ts.map +1 -0
  170. package/dist/src/clients/ctf-client.js +915 -0
  171. package/dist/src/clients/ctf-client.js.map +1 -0
  172. package/dist/src/clients/data-api.d.ts +452 -0
  173. package/dist/src/clients/data-api.d.ts.map +1 -0
  174. package/dist/src/clients/data-api.js +637 -0
  175. package/dist/src/clients/data-api.js.map +1 -0
  176. package/dist/src/clients/gamma-api.d.ts +421 -0
  177. package/dist/src/clients/gamma-api.d.ts.map +1 -0
  178. package/dist/src/clients/gamma-api.js +359 -0
  179. package/dist/src/clients/gamma-api.js.map +1 -0
  180. package/dist/src/clients/subgraph.d.ts +196 -0
  181. package/dist/src/clients/subgraph.d.ts.map +1 -0
  182. package/dist/src/clients/subgraph.js +332 -0
  183. package/dist/src/clients/subgraph.js.map +1 -0
  184. package/dist/src/core/cache-adapter-bridge.d.ts +36 -0
  185. package/dist/src/core/cache-adapter-bridge.d.ts.map +1 -0
  186. package/dist/src/core/cache-adapter-bridge.js +81 -0
  187. package/dist/src/core/cache-adapter-bridge.js.map +1 -0
  188. package/dist/src/core/cache.d.ts +43 -0
  189. package/dist/src/core/cache.d.ts.map +1 -0
  190. package/dist/src/core/cache.js +76 -0
  191. package/dist/src/core/cache.js.map +1 -0
  192. package/dist/src/core/errors.d.ts +39 -0
  193. package/dist/src/core/errors.d.ts.map +1 -0
  194. package/dist/src/core/errors.js +86 -0
  195. package/dist/src/core/errors.js.map +1 -0
  196. package/dist/src/core/rate-limiter.d.ts +33 -0
  197. package/dist/src/core/rate-limiter.d.ts.map +1 -0
  198. package/dist/src/core/rate-limiter.js +82 -0
  199. package/dist/src/core/rate-limiter.js.map +1 -0
  200. package/dist/src/core/types.d.ts +506 -0
  201. package/dist/src/core/types.d.ts.map +1 -0
  202. package/dist/src/core/types.js +49 -0
  203. package/dist/src/core/types.js.map +1 -0
  204. package/dist/src/core/types.test.d.ts +7 -0
  205. package/dist/src/core/types.test.d.ts.map +1 -0
  206. package/dist/src/core/types.test.js +122 -0
  207. package/dist/src/core/types.test.js.map +1 -0
  208. package/dist/src/core/unified-cache.d.ts +63 -0
  209. package/dist/src/core/unified-cache.d.ts.map +1 -0
  210. package/dist/src/core/unified-cache.js +114 -0
  211. package/dist/src/core/unified-cache.js.map +1 -0
  212. package/dist/src/index.d.ts +159 -0
  213. package/dist/src/index.d.ts.map +1 -0
  214. package/dist/src/index.js +262 -0
  215. package/dist/src/index.js.map +1 -0
  216. package/dist/src/services/arbitrage-service.d.ts +409 -0
  217. package/dist/src/services/arbitrage-service.d.ts.map +1 -0
  218. package/dist/src/services/arbitrage-service.js +1450 -0
  219. package/dist/src/services/arbitrage-service.js.map +1 -0
  220. package/dist/src/services/authorization-service.d.ts +97 -0
  221. package/dist/src/services/authorization-service.d.ts.map +1 -0
  222. package/dist/src/services/authorization-service.js +279 -0
  223. package/dist/src/services/authorization-service.js.map +1 -0
  224. package/dist/src/services/binance-service.d.ts +154 -0
  225. package/dist/src/services/binance-service.d.ts.map +1 -0
  226. package/dist/src/services/binance-service.js +266 -0
  227. package/dist/src/services/binance-service.js.map +1 -0
  228. package/dist/src/services/dip-arb-service.d.ts +245 -0
  229. package/dist/src/services/dip-arb-service.d.ts.map +1 -0
  230. package/dist/src/services/dip-arb-service.js +1865 -0
  231. package/dist/src/services/dip-arb-service.js.map +1 -0
  232. package/dist/src/services/dip-arb-types.d.ts +553 -0
  233. package/dist/src/services/dip-arb-types.d.ts.map +1 -0
  234. package/dist/src/services/dip-arb-types.js +164 -0
  235. package/dist/src/services/dip-arb-types.js.map +1 -0
  236. package/dist/src/services/market-service.d.ts +370 -0
  237. package/dist/src/services/market-service.d.ts.map +1 -0
  238. package/dist/src/services/market-service.js +1200 -0
  239. package/dist/src/services/market-service.js.map +1 -0
  240. package/dist/src/services/onchain-service.d.ts +309 -0
  241. package/dist/src/services/onchain-service.d.ts.map +1 -0
  242. package/dist/src/services/onchain-service.js +417 -0
  243. package/dist/src/services/onchain-service.js.map +1 -0
  244. package/dist/src/services/realtime-service-v2.d.ts +367 -0
  245. package/dist/src/services/realtime-service-v2.d.ts.map +1 -0
  246. package/dist/src/services/realtime-service-v2.js +876 -0
  247. package/dist/src/services/realtime-service-v2.js.map +1 -0
  248. package/dist/src/services/smart-money-service.d.ts +352 -0
  249. package/dist/src/services/smart-money-service.d.ts.map +1 -0
  250. package/dist/src/services/smart-money-service.js +582 -0
  251. package/dist/src/services/smart-money-service.js.map +1 -0
  252. package/dist/src/services/swap-service.d.ts +217 -0
  253. package/dist/src/services/swap-service.d.ts.map +1 -0
  254. package/dist/src/services/swap-service.js +695 -0
  255. package/dist/src/services/swap-service.js.map +1 -0
  256. package/dist/src/services/trading-service.d.ts +177 -0
  257. package/dist/src/services/trading-service.d.ts.map +1 -0
  258. package/dist/src/services/trading-service.js +422 -0
  259. package/dist/src/services/trading-service.js.map +1 -0
  260. package/dist/src/services/wallet-service.d.ts +316 -0
  261. package/dist/src/services/wallet-service.d.ts.map +1 -0
  262. package/dist/src/services/wallet-service.js +681 -0
  263. package/dist/src/services/wallet-service.js.map +1 -0
  264. package/dist/src/utils/price-utils.d.ts +153 -0
  265. package/dist/src/utils/price-utils.d.ts.map +1 -0
  266. package/dist/src/utils/price-utils.js +236 -0
  267. package/dist/src/utils/price-utils.js.map +1 -0
  268. package/dist/src/utils/price-utils.test.d.ts +5 -0
  269. package/dist/src/utils/price-utils.test.d.ts.map +1 -0
  270. package/dist/src/utils/price-utils.test.js +192 -0
  271. package/dist/src/utils/price-utils.test.js.map +1 -0
  272. package/dist/utils/price-utils.test.d.ts +5 -0
  273. package/dist/utils/price-utils.test.d.ts.map +1 -0
  274. package/dist/utils/price-utils.test.js +192 -0
  275. package/dist/utils/price-utils.test.js.map +1 -0
  276. package/package.json +6 -5
  277. package/README.en.md +0 -502
@@ -0,0 +1,1200 @@
1
+ /**
2
+ * Market Service
3
+ *
4
+ * Provides market data and analysis:
5
+ * - Market info and discovery
6
+ * - Orderbook data and analysis
7
+ * - K-Line aggregation from trade data
8
+ * - Spread analysis
9
+ * - Arbitrage detection
10
+ */
11
+ import { ClobClient, Side as ClobSide, PriceHistoryInterval, } from '@polymarket/clob-client';
12
+ import { Wallet } from 'ethers';
13
+ import { CACHE_TTL } from '../core/unified-cache.js';
14
+ import { ApiType } from '../core/rate-limiter.js';
15
+ import { PolymarketError, ErrorCode } from '../core/errors.js';
16
+ // CLOB Host
17
+ const CLOB_HOST = 'https://clob.polymarket.com';
18
+ // Chain IDs
19
+ export const POLYGON_MAINNET = 137;
20
+ /**
21
+ * Normalize timestamp to milliseconds.
22
+ * Polymarket API sometimes returns timestamps in seconds.
23
+ * Timestamps < 1e12 (year ~2001 in ms) are assumed to be in seconds.
24
+ */
25
+ function normalizeTimestamp(ts) {
26
+ return ts < 1e12 ? ts * 1000 : ts;
27
+ }
28
+ // Mapping from underlying asset to Binance symbol
29
+ const UNDERLYING_TO_SYMBOL = {
30
+ BTC: 'BTCUSDT',
31
+ ETH: 'ETHUSDT',
32
+ SOL: 'SOLUSDT',
33
+ };
34
+ // Map from KLineInterval to BinanceInterval (Binance doesn't support 30s or 12h)
35
+ const KLINE_TO_BINANCE_INTERVAL = {
36
+ '1m': '1m',
37
+ '5m': '5m',
38
+ '15m': '15m',
39
+ '30m': '30m',
40
+ '1h': '1h',
41
+ '4h': '4h',
42
+ '1d': '1d',
43
+ };
44
+ // ============================================================================
45
+ // MarketService Implementation
46
+ // ============================================================================
47
+ export class MarketService {
48
+ gammaApi;
49
+ dataApi;
50
+ rateLimiter;
51
+ cache;
52
+ config;
53
+ binanceService;
54
+ clobClient = null;
55
+ initialized = false;
56
+ constructor(gammaApi, dataApi, rateLimiter, cache, config, binanceService) {
57
+ this.gammaApi = gammaApi;
58
+ this.dataApi = dataApi;
59
+ this.rateLimiter = rateLimiter;
60
+ this.cache = cache;
61
+ this.config = config;
62
+ this.binanceService = binanceService;
63
+ }
64
+ // ============================================================================
65
+ // Initialization
66
+ // ============================================================================
67
+ async ensureInitialized() {
68
+ if (!this.initialized || !this.clobClient) {
69
+ const chainId = (this.config?.chainId || POLYGON_MAINNET);
70
+ if (this.config?.privateKey) {
71
+ // Authenticated client
72
+ const wallet = new Wallet(this.config.privateKey);
73
+ this.clobClient = new ClobClient(CLOB_HOST, chainId, wallet);
74
+ }
75
+ else {
76
+ // Read-only client (no auth needed for market data)
77
+ this.clobClient = new ClobClient(CLOB_HOST, chainId);
78
+ }
79
+ this.initialized = true;
80
+ }
81
+ return this.clobClient;
82
+ }
83
+ // ============================================================================
84
+ // CLOB Market Data Methods
85
+ // ============================================================================
86
+ /**
87
+ * Get market from CLOB by condition ID
88
+ */
89
+ async getClobMarket(conditionId) {
90
+ const cacheKey = `clob:market:${conditionId}`;
91
+ return this.cache.getOrSet(cacheKey, CACHE_TTL.MARKET_INFO, async () => {
92
+ const client = await this.ensureInitialized();
93
+ return this.rateLimiter.execute(ApiType.CLOB_API, async () => {
94
+ try {
95
+ const market = await client.getMarket(conditionId);
96
+ if (!market || !market.tokens) {
97
+ return null;
98
+ }
99
+ return this.normalizeClobMarket(market);
100
+ }
101
+ catch (error) {
102
+ // Handle 404 "market not found" gracefully
103
+ if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
104
+ return null;
105
+ }
106
+ throw error;
107
+ }
108
+ });
109
+ });
110
+ }
111
+ /**
112
+ * Get multiple markets from CLOB
113
+ */
114
+ async getClobMarkets(nextCursor) {
115
+ const client = await this.ensureInitialized();
116
+ return this.rateLimiter.execute(ApiType.CLOB_API, async () => {
117
+ const result = await client.getMarkets(nextCursor);
118
+ return {
119
+ markets: result.data.map(m => this.normalizeClobMarket(m)),
120
+ nextCursor: result.next_cursor,
121
+ };
122
+ });
123
+ }
124
+ /**
125
+ * Get orderbook for a single token
126
+ */
127
+ async getTokenOrderbook(tokenId) {
128
+ const client = await this.ensureInitialized();
129
+ return this.rateLimiter.execute(ApiType.CLOB_API, async () => {
130
+ const book = await client.getOrderBook(tokenId);
131
+ const bids = (book.bids || [])
132
+ .map((l) => ({
133
+ price: parseFloat(l.price),
134
+ size: parseFloat(l.size),
135
+ }))
136
+ .sort((a, b) => b.price - a.price);
137
+ const asks = (book.asks || [])
138
+ .map((l) => ({
139
+ price: parseFloat(l.price),
140
+ size: parseFloat(l.size),
141
+ }))
142
+ .sort((a, b) => a.price - b.price);
143
+ return {
144
+ tokenId: book.asset_id,
145
+ assetId: book.asset_id, // Backward compatibility
146
+ bids,
147
+ asks,
148
+ timestamp: parseInt(book.timestamp || '0', 10) || Date.now(),
149
+ market: book.market,
150
+ hash: book.hash,
151
+ };
152
+ });
153
+ }
154
+ /**
155
+ * Get orderbooks for multiple tokens
156
+ */
157
+ async getTokenOrderbooks(params) {
158
+ const client = await this.ensureInitialized();
159
+ return this.rateLimiter.execute(ApiType.CLOB_API, async () => {
160
+ const bookParams = params.map(p => ({
161
+ token_id: p.tokenId,
162
+ side: p.side === 'BUY' ? ClobSide.BUY : ClobSide.SELL,
163
+ }));
164
+ const books = await client.getOrderBooks(bookParams);
165
+ const result = new Map();
166
+ for (const book of books) {
167
+ const bids = (book.bids || [])
168
+ .map((l) => ({
169
+ price: parseFloat(l.price),
170
+ size: parseFloat(l.size),
171
+ }))
172
+ .sort((a, b) => b.price - a.price);
173
+ const asks = (book.asks || [])
174
+ .map((l) => ({
175
+ price: parseFloat(l.price),
176
+ size: parseFloat(l.size),
177
+ }))
178
+ .sort((a, b) => a.price - b.price);
179
+ result.set(book.asset_id, {
180
+ tokenId: book.asset_id,
181
+ assetId: book.asset_id, // Backward compatibility
182
+ bids,
183
+ asks,
184
+ timestamp: parseInt(book.timestamp || '0', 10) || Date.now(),
185
+ market: book.market,
186
+ hash: book.hash,
187
+ });
188
+ }
189
+ return result;
190
+ });
191
+ }
192
+ /**
193
+ * Get processed orderbook with arbitrage analysis for a market
194
+ */
195
+ async getProcessedOrderbook(conditionId) {
196
+ const market = await this.getClobMarket(conditionId);
197
+ if (!market) {
198
+ throw new PolymarketError(ErrorCode.MARKET_NOT_FOUND, `Market not found: ${conditionId}`);
199
+ }
200
+ // Use index-based access instead of name-based (supports Yes/No, Up/Down, Team1/Team2, etc.)
201
+ const yesToken = market.tokens[0]; // primary outcome
202
+ const noToken = market.tokens[1]; // secondary outcome
203
+ if (!yesToken || !noToken) {
204
+ throw new PolymarketError(ErrorCode.INVALID_RESPONSE, 'Missing tokens in market');
205
+ }
206
+ const [yesBook, noBook] = await Promise.all([
207
+ this.getTokenOrderbook(yesToken.tokenId),
208
+ this.getTokenOrderbook(noToken.tokenId),
209
+ ]);
210
+ return this.processOrderbooks(yesBook, noBook, yesToken.tokenId, noToken.tokenId);
211
+ }
212
+ /**
213
+ * Get price history for a token
214
+ */
215
+ async getPricesHistory(params) {
216
+ const client = await this.ensureInitialized();
217
+ return this.rateLimiter.execute(ApiType.CLOB_API, async () => {
218
+ const intervalMap = {
219
+ '1h': PriceHistoryInterval.ONE_HOUR,
220
+ '6h': PriceHistoryInterval.SIX_HOURS,
221
+ '1d': PriceHistoryInterval.ONE_DAY,
222
+ '1w': PriceHistoryInterval.ONE_WEEK,
223
+ 'max': PriceHistoryInterval.MAX,
224
+ };
225
+ const history = await client.getPricesHistory({
226
+ market: params.tokenId,
227
+ interval: params.interval ? intervalMap[params.interval] : undefined,
228
+ startTs: params.startTs,
229
+ endTs: params.endTs,
230
+ fidelity: params.fidelity,
231
+ });
232
+ const historyArray = Array.isArray(history)
233
+ ? history
234
+ : history?.history || [];
235
+ return historyArray.map((pt) => ({
236
+ timestamp: pt.t,
237
+ price: pt.p,
238
+ }));
239
+ });
240
+ }
241
+ /**
242
+ * Get midpoint price for a token
243
+ */
244
+ async getMidpoint(tokenId) {
245
+ const client = await this.ensureInitialized();
246
+ return this.rateLimiter.execute(ApiType.CLOB_API, async () => {
247
+ const midpoint = await client.getMidpoint(tokenId);
248
+ return Number(midpoint);
249
+ });
250
+ }
251
+ /**
252
+ * Get spread for a token
253
+ */
254
+ async getSpread(tokenId) {
255
+ const client = await this.ensureInitialized();
256
+ return this.rateLimiter.execute(ApiType.CLOB_API, async () => {
257
+ const spread = await client.getSpread(tokenId);
258
+ return Number(spread);
259
+ });
260
+ }
261
+ /**
262
+ * Get last trade price for a token
263
+ */
264
+ async getLastTradePrice(tokenId) {
265
+ const client = await this.ensureInitialized();
266
+ return this.rateLimiter.execute(ApiType.CLOB_API, async () => {
267
+ const price = await client.getLastTradePrice(tokenId);
268
+ return Number(price);
269
+ });
270
+ }
271
+ // ============================================================================
272
+ // Unified Market Access
273
+ // ============================================================================
274
+ /**
275
+ * Get market by slug or condition ID
276
+ */
277
+ async getMarket(identifier) {
278
+ const isConditionId = identifier.startsWith('0x') || /^\d+$/.test(identifier);
279
+ if (isConditionId) {
280
+ return this.getMarketByConditionId(identifier);
281
+ }
282
+ else {
283
+ return this.getMarketBySlug(identifier);
284
+ }
285
+ }
286
+ async getMarketBySlug(slug) {
287
+ if (!this.gammaApi) {
288
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, 'GammaApiClient is required for slug-based lookups');
289
+ }
290
+ const gammaMarket = await this.gammaApi.getMarketBySlug(slug);
291
+ if (!gammaMarket) {
292
+ throw new PolymarketError(ErrorCode.MARKET_NOT_FOUND, `Market not found: ${slug}`);
293
+ }
294
+ try {
295
+ const clobMarket = await this.getClobMarket(gammaMarket.conditionId);
296
+ if (clobMarket) {
297
+ return this.mergeMarkets(gammaMarket, clobMarket);
298
+ }
299
+ return this.fromGammaMarket(gammaMarket);
300
+ }
301
+ catch {
302
+ return this.fromGammaMarket(gammaMarket);
303
+ }
304
+ }
305
+ async getMarketByConditionId(conditionId) {
306
+ // Try to get data from both sources for best accuracy
307
+ let clobMarket = null;
308
+ let gammaMarket = null;
309
+ // Try CLOB first (authoritative for trading data)
310
+ try {
311
+ clobMarket = await this.getClobMarket(conditionId);
312
+ }
313
+ catch {
314
+ // CLOB failed, continue to try Gamma
315
+ }
316
+ // Always try Gamma for accurate slug and metadata (if available)
317
+ if (this.gammaApi) {
318
+ try {
319
+ gammaMarket = await this.gammaApi.getMarketByConditionId(conditionId);
320
+ }
321
+ catch {
322
+ // Gamma failed
323
+ }
324
+ }
325
+ // Merge if both available (preferred)
326
+ if (gammaMarket && clobMarket) {
327
+ return this.mergeMarkets(gammaMarket, clobMarket);
328
+ }
329
+ // Gamma only - still useful for metadata
330
+ if (gammaMarket) {
331
+ return this.fromGammaMarket(gammaMarket);
332
+ }
333
+ // CLOB only - slug might be stale, add warning
334
+ if (clobMarket) {
335
+ const market = this.fromClobMarket(clobMarket);
336
+ // Check if slug looks stale (doesn't match question keywords)
337
+ const questionWords = clobMarket.question.toLowerCase().split(/\s+/).slice(0, 3);
338
+ const slugWords = clobMarket.marketSlug.toLowerCase().split('-');
339
+ const hasMatchingWord = questionWords.some(qw => slugWords.some(sw => sw.includes(qw) || qw.includes(sw)));
340
+ if (!hasMatchingWord && clobMarket.marketSlug.length > 0) {
341
+ // Slug appears stale, use conditionId as fallback identifier
342
+ market.slug = `market-${conditionId.slice(0, 10)}`;
343
+ }
344
+ return market;
345
+ }
346
+ throw new PolymarketError(ErrorCode.MARKET_NOT_FOUND, `Market not found: ${conditionId}`);
347
+ }
348
+ // ===== K-Line Aggregation =====
349
+ /**
350
+ * Get K-Line candles for a market (single token)
351
+ *
352
+ * @param conditionId - Market condition ID
353
+ * @param interval - K-line interval (1s, 5s, 15s, 30s, 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d)
354
+ * @param options - Query options
355
+ * @param options.limit - Maximum number of trades to fetch for aggregation (default: 1000)
356
+ * @param options.tokenId - Filter by specific token ID
357
+ * @param options.outcomeIndex - Filter by outcome index (0 = primary, 1 = secondary)
358
+ * @param options.startTimestamp - Start timestamp (Unix ms) - filter trades after this time
359
+ * @param options.endTimestamp - End timestamp (Unix ms) - filter trades before this time
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * // Get 5s candles for the last 15 minutes
364
+ * const now = Date.now();
365
+ * const candles = await sdk.markets.getKLines(conditionId, '5s', {
366
+ * startTimestamp: now - 15 * 60 * 1000,
367
+ * endTimestamp: now,
368
+ * });
369
+ * ```
370
+ */
371
+ async getKLines(conditionId, interval, options) {
372
+ if (!this.dataApi) {
373
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, 'DataApiClient is required for K-Line data');
374
+ }
375
+ const trades = await this.dataApi.getTrades({
376
+ market: conditionId,
377
+ limit: options?.limit || 1000,
378
+ startTimestamp: options?.startTimestamp,
379
+ endTimestamp: options?.endTimestamp,
380
+ });
381
+ // Filter by token/outcome if specified
382
+ let filteredTrades = trades;
383
+ if (options?.tokenId) {
384
+ filteredTrades = trades.filter((t) => t.asset === options.tokenId);
385
+ }
386
+ else if (options?.outcomeIndex !== undefined) {
387
+ filteredTrades = trades.filter((t) => t.outcomeIndex === options.outcomeIndex);
388
+ }
389
+ return this.aggregateToKLines(filteredTrades, interval);
390
+ }
391
+ /**
392
+ * Get dual K-Lines (YES + NO tokens)
393
+ *
394
+ * @param conditionId - Market condition ID
395
+ * @param interval - K-line interval (1s, 5s, 15s, 30s, 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d)
396
+ * @param options - Query options
397
+ * @param options.limit - Maximum number of trades to fetch for aggregation (default: 1000)
398
+ * @param options.startTimestamp - Start timestamp (Unix ms) - filter trades after this time
399
+ * @param options.endTimestamp - End timestamp (Unix ms) - filter trades before this time
400
+ *
401
+ * @example
402
+ * ```typescript
403
+ * // Get 15s dual K-lines for a 15-minute market
404
+ * const now = Date.now();
405
+ * const data = await sdk.markets.getDualKLines(conditionId, '15s', {
406
+ * startTimestamp: now - 15 * 60 * 1000,
407
+ * endTimestamp: now,
408
+ * });
409
+ * console.log(`Up candles: ${data.yes.length}, Down candles: ${data.no.length}`);
410
+ * ```
411
+ */
412
+ async getDualKLines(conditionId, interval, options) {
413
+ if (!this.dataApi) {
414
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, 'DataApiClient is required for K-Line data');
415
+ }
416
+ const market = await this.getMarket(conditionId);
417
+ const trades = await this.dataApi.getTrades({
418
+ market: conditionId,
419
+ limit: options?.limit || 1000,
420
+ startTimestamp: options?.startTimestamp,
421
+ endTimestamp: options?.endTimestamp,
422
+ });
423
+ // Separate trades by outcome using index (more reliable than name matching)
424
+ // outcomeIndex 0 = primary (Yes/Up/Team1), outcomeIndex 1 = secondary (No/Down/Team2)
425
+ const yesTrades = trades.filter((t) => t.outcomeIndex === 0);
426
+ const noTrades = trades.filter((t) => t.outcomeIndex === 1);
427
+ const yesCandles = this.aggregateToKLines(yesTrades, interval);
428
+ const noCandles = this.aggregateToKLines(noTrades, interval);
429
+ // Get current orderbook for real-time spread analysis
430
+ let currentOrderbook;
431
+ let realtimeSpread;
432
+ try {
433
+ currentOrderbook = await this.getProcessedOrderbook(conditionId);
434
+ realtimeSpread = this.calculateRealtimeSpread(currentOrderbook);
435
+ }
436
+ catch {
437
+ // Orderbook not available
438
+ }
439
+ // Calculate historical spread from trade close prices (for backtesting)
440
+ const spreadAnalysis = this.analyzeHistoricalSpread(yesCandles, noCandles);
441
+ return {
442
+ conditionId,
443
+ interval,
444
+ market,
445
+ yes: yesCandles,
446
+ no: noCandles,
447
+ spreadAnalysis, // Historical (trade-based)
448
+ realtimeSpread, // Real-time (orderbook-based)
449
+ currentOrderbook,
450
+ };
451
+ }
452
+ // ===== Token vs Underlying Correlation =====
453
+ /**
454
+ * Get aligned K-line data for token and underlying asset
455
+ *
456
+ * This method fetches K-line data from both Polymarket (token prices)
457
+ * and Binance (underlying asset prices), aligns them by timestamp,
458
+ * and optionally calculates Pearson correlation coefficients.
459
+ *
460
+ * @param conditionId - Market condition ID
461
+ * @param underlying - Underlying asset (BTC, ETH, SOL)
462
+ * @param interval - K-line interval (must be supported by both Poly and Binance)
463
+ * @param options - Optional parameters
464
+ * @returns Aligned data with optional correlation coefficients
465
+ *
466
+ * @example
467
+ * ```typescript
468
+ * const data = await marketService.getTokenUnderlyingData(
469
+ * '0x123...',
470
+ * 'BTC',
471
+ * '1h',
472
+ * { limit: 100, calculateCorrelation: true }
473
+ * );
474
+ *
475
+ * // Access aligned data
476
+ * for (const point of data.data) {
477
+ * console.log(`${point.timestamp}: Up=${point.upPrice}, BTC=${point.underlyingPrice}`);
478
+ * }
479
+ *
480
+ * // Check correlation
481
+ * if (data.correlation) {
482
+ * console.log(`Correlation: ${data.correlation.upVsUnderlying}`);
483
+ * }
484
+ * ```
485
+ */
486
+ async getTokenUnderlyingData(conditionId, underlying, interval, options) {
487
+ // Validate BinanceService is available
488
+ if (!this.binanceService) {
489
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, 'BinanceService is required for token-underlying correlation analysis');
490
+ }
491
+ // Validate interval is supported by Binance
492
+ const binanceInterval = KLINE_TO_BINANCE_INTERVAL[interval];
493
+ if (!binanceInterval) {
494
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, `Interval ${interval} is not supported for correlation analysis. ` +
495
+ `Supported intervals: ${Object.keys(KLINE_TO_BINANCE_INTERVAL).join(', ')}`);
496
+ }
497
+ const limit = options?.limit || 500;
498
+ // Fetch data in parallel
499
+ const [dualKLines, binanceKLines] = await Promise.all([
500
+ this.getDualKLines(conditionId, interval, { limit }),
501
+ this.binanceService.getKLines(UNDERLYING_TO_SYMBOL[underlying], binanceInterval, { limit }),
502
+ ]);
503
+ // Create maps for quick lookup
504
+ const upMap = new Map(dualKLines.yes.map(c => [c.timestamp, c.close]));
505
+ const downMap = new Map(dualKLines.no.map(c => [c.timestamp, c.close]));
506
+ const binanceMap = new Map(binanceKLines.map(c => [c.timestamp, c.close]));
507
+ // Get all unique timestamps and sort them
508
+ const allTimestamps = new Set([
509
+ ...upMap.keys(),
510
+ ...downMap.keys(),
511
+ ...binanceMap.keys(),
512
+ ]);
513
+ const sortedTimestamps = [...allTimestamps].sort((a, b) => a - b);
514
+ // Find the first Binance price for calculating percentage change
515
+ const firstBinancePrice = binanceKLines.length > 0 ? binanceKLines[0].close : 0;
516
+ // Align data points
517
+ const alignedData = [];
518
+ let lastUpPrice;
519
+ let lastDownPrice;
520
+ let lastBinancePrice;
521
+ for (const timestamp of sortedTimestamps) {
522
+ // Get prices, falling back to previous values if not available
523
+ const upPrice = upMap.get(timestamp) ?? this.findNearestPrice(timestamp, upMap, sortedTimestamps);
524
+ const downPrice = downMap.get(timestamp) ?? this.findNearestPrice(timestamp, downMap, sortedTimestamps);
525
+ const binancePrice = binanceMap.get(timestamp) ?? this.findNearestPrice(timestamp, binanceMap, sortedTimestamps);
526
+ // Update last known prices
527
+ if (upPrice !== undefined)
528
+ lastUpPrice = upPrice;
529
+ if (downPrice !== undefined)
530
+ lastDownPrice = downPrice;
531
+ if (binancePrice !== undefined)
532
+ lastBinancePrice = binancePrice;
533
+ // Skip if we don't have underlying price
534
+ if (lastBinancePrice === undefined)
535
+ continue;
536
+ const priceSum = (lastUpPrice !== undefined && lastDownPrice !== undefined)
537
+ ? lastUpPrice + lastDownPrice
538
+ : undefined;
539
+ const underlyingChange = firstBinancePrice > 0
540
+ ? ((lastBinancePrice - firstBinancePrice) / firstBinancePrice) * 100
541
+ : 0;
542
+ alignedData.push({
543
+ timestamp,
544
+ upPrice: lastUpPrice,
545
+ downPrice: lastDownPrice,
546
+ priceSum,
547
+ underlyingPrice: lastBinancePrice,
548
+ underlyingChange,
549
+ });
550
+ }
551
+ // Calculate correlation if requested
552
+ let correlation;
553
+ if (options?.calculateCorrelation && alignedData.length >= 2) {
554
+ correlation = this.calculatePearsonCorrelation(alignedData);
555
+ }
556
+ return {
557
+ conditionId,
558
+ underlying,
559
+ interval,
560
+ data: alignedData,
561
+ correlation,
562
+ };
563
+ }
564
+ /**
565
+ * Find the nearest available price for a timestamp
566
+ */
567
+ findNearestPrice(targetTimestamp, priceMap, sortedTimestamps) {
568
+ if (priceMap.size === 0)
569
+ return undefined;
570
+ // Find the nearest timestamp that has a price
571
+ let nearestTimestamp;
572
+ let minDiff = Infinity;
573
+ for (const ts of sortedTimestamps) {
574
+ if (priceMap.has(ts)) {
575
+ const diff = Math.abs(ts - targetTimestamp);
576
+ if (diff < minDiff) {
577
+ minDiff = diff;
578
+ nearestTimestamp = ts;
579
+ }
580
+ }
581
+ }
582
+ return nearestTimestamp !== undefined ? priceMap.get(nearestTimestamp) : undefined;
583
+ }
584
+ /**
585
+ * Calculate Pearson correlation coefficients
586
+ */
587
+ calculatePearsonCorrelation(data) {
588
+ // Filter data points that have all required prices
589
+ const upData = data.filter(d => d.upPrice !== undefined && d.underlyingPrice !== undefined);
590
+ const downData = data.filter(d => d.downPrice !== undefined && d.underlyingPrice !== undefined);
591
+ const upVsUnderlying = this.pearson(upData.map(d => d.upPrice), upData.map(d => d.underlyingPrice));
592
+ const downVsUnderlying = this.pearson(downData.map(d => d.downPrice), downData.map(d => d.underlyingPrice));
593
+ return {
594
+ upVsUnderlying,
595
+ downVsUnderlying,
596
+ };
597
+ }
598
+ /**
599
+ * Calculate Pearson correlation coefficient between two arrays
600
+ * Returns a value between -1 and 1
601
+ */
602
+ pearson(x, y) {
603
+ const n = Math.min(x.length, y.length);
604
+ if (n < 2)
605
+ return 0;
606
+ // Calculate means
607
+ let sumX = 0, sumY = 0;
608
+ for (let i = 0; i < n; i++) {
609
+ sumX += x[i];
610
+ sumY += y[i];
611
+ }
612
+ const meanX = sumX / n;
613
+ const meanY = sumY / n;
614
+ // Calculate correlation
615
+ let numerator = 0;
616
+ let sumSqX = 0;
617
+ let sumSqY = 0;
618
+ for (let i = 0; i < n; i++) {
619
+ const dx = x[i] - meanX;
620
+ const dy = y[i] - meanY;
621
+ numerator += dx * dy;
622
+ sumSqX += dx * dx;
623
+ sumSqY += dy * dy;
624
+ }
625
+ const denominator = Math.sqrt(sumSqX * sumSqY);
626
+ if (denominator === 0)
627
+ return 0;
628
+ return numerator / denominator;
629
+ }
630
+ /**
631
+ * Aggregate trades into K-Line candles
632
+ *
633
+ * Note: Polymarket API may return timestamps in seconds or milliseconds.
634
+ * This function normalizes all timestamps to milliseconds for consistent handling.
635
+ */
636
+ aggregateToKLines(trades, interval) {
637
+ const intervalMs = getIntervalMs(interval);
638
+ const buckets = new Map();
639
+ // Group trades into time buckets
640
+ // Normalize timestamp to milliseconds (API sometimes returns seconds)
641
+ for (const trade of trades) {
642
+ const tradeTs = normalizeTimestamp(trade.timestamp);
643
+ const bucketTime = Math.floor(tradeTs / intervalMs) * intervalMs;
644
+ const bucket = buckets.get(bucketTime) || [];
645
+ bucket.push(trade);
646
+ buckets.set(bucketTime, bucket);
647
+ }
648
+ // Convert buckets to candles
649
+ const candles = [];
650
+ for (const [timestamp, bucketTrades] of buckets) {
651
+ if (bucketTrades.length === 0)
652
+ continue;
653
+ // Sort by normalized timestamp for correct open/close
654
+ bucketTrades.sort((a, b) => normalizeTimestamp(a.timestamp) - normalizeTimestamp(b.timestamp));
655
+ const prices = bucketTrades.map((t) => t.price);
656
+ const buyTrades = bucketTrades.filter((t) => t.side === 'BUY');
657
+ const sellTrades = bucketTrades.filter((t) => t.side === 'SELL');
658
+ candles.push({
659
+ timestamp,
660
+ open: bucketTrades[0].price,
661
+ high: Math.max(...prices),
662
+ low: Math.min(...prices),
663
+ close: bucketTrades[bucketTrades.length - 1].price,
664
+ volume: bucketTrades.reduce((sum, t) => sum + t.size * t.price, 0),
665
+ tradeCount: bucketTrades.length,
666
+ buyVolume: buyTrades.reduce((sum, t) => sum + t.size * t.price, 0),
667
+ sellVolume: sellTrades.reduce((sum, t) => sum + t.size * t.price, 0),
668
+ });
669
+ }
670
+ return candles.sort((a, b) => a.timestamp - b.timestamp);
671
+ }
672
+ /**
673
+ * Analyze historical spread from trade close prices (for backtesting)
674
+ *
675
+ * This uses trade close prices, not orderbook bid/ask.
676
+ * Useful for:
677
+ * - Historical analysis / backtesting
678
+ * - Understanding past price movements
679
+ * - Identifying patterns when orderbook data unavailable
680
+ */
681
+ analyzeHistoricalSpread(yesCandles, noCandles) {
682
+ const yesMap = new Map(yesCandles.map((c) => [c.timestamp, c]));
683
+ const noMap = new Map(noCandles.map((c) => [c.timestamp, c]));
684
+ const allTimestamps = [...new Set([...yesMap.keys(), ...noMap.keys()])].sort();
685
+ let lastYes = 0.5;
686
+ let lastNo = 0.5;
687
+ const analysis = [];
688
+ for (const ts of allTimestamps) {
689
+ const yesCandle = yesMap.get(ts);
690
+ const noCandle = noMap.get(ts);
691
+ if (yesCandle)
692
+ lastYes = yesCandle.close;
693
+ if (noCandle)
694
+ lastNo = noCandle.close;
695
+ const priceSum = lastYes + lastNo;
696
+ const priceSpread = priceSum - 1;
697
+ // Determine arb opportunity based on price deviation
698
+ // Note: This is indicative only - actual arb requires orderbook analysis
699
+ let arbOpportunity = '';
700
+ if (priceSpread < -0.005)
701
+ arbOpportunity = 'LONG'; // Sum < 0.995
702
+ else if (priceSpread > 0.005)
703
+ arbOpportunity = 'SHORT'; // Sum > 1.005
704
+ analysis.push({
705
+ timestamp: ts,
706
+ yesPrice: lastYes,
707
+ noPrice: lastNo,
708
+ priceSum,
709
+ priceSpread,
710
+ arbOpportunity,
711
+ });
712
+ }
713
+ return analysis;
714
+ }
715
+ /**
716
+ * Calculate real-time spread from orderbook (for live trading)
717
+ *
718
+ * This uses orderbook bid/ask prices for accurate arbitrage detection.
719
+ * Useful for:
720
+ * - Real-time arbitrage execution
721
+ * - Live trading decisions
722
+ * - Accurate profit calculations
723
+ */
724
+ calculateRealtimeSpread(orderbook) {
725
+ const { yes, no, summary } = orderbook;
726
+ // Determine arbitrage opportunity
727
+ let arbOpportunity = '';
728
+ let arbProfitPercent = 0;
729
+ if (summary.longArbProfit > 0.001) { // > 0.1% threshold
730
+ arbOpportunity = 'LONG';
731
+ arbProfitPercent = summary.longArbProfit * 100;
732
+ }
733
+ else if (summary.shortArbProfit > 0.001) { // > 0.1% threshold
734
+ arbOpportunity = 'SHORT';
735
+ arbProfitPercent = summary.shortArbProfit * 100;
736
+ }
737
+ return {
738
+ timestamp: Date.now(),
739
+ // Orderbook prices
740
+ yesBid: yes.bid,
741
+ yesAsk: yes.ask,
742
+ noBid: no.bid,
743
+ noAsk: no.ask,
744
+ // Spread metrics
745
+ askSum: summary.askSum,
746
+ bidSum: summary.bidSum,
747
+ askSpread: summary.askSum - 1,
748
+ bidSpread: summary.bidSum - 1,
749
+ // Arbitrage
750
+ longArbProfit: summary.longArbProfit,
751
+ shortArbProfit: summary.shortArbProfit,
752
+ arbOpportunity,
753
+ arbProfitPercent,
754
+ };
755
+ }
756
+ /**
757
+ * Get real-time spread analysis only (without K-lines)
758
+ * Use this for quick arbitrage checks
759
+ */
760
+ async getRealtimeSpread(conditionId) {
761
+ const orderbook = await this.getProcessedOrderbook(conditionId);
762
+ return this.calculateRealtimeSpread(orderbook);
763
+ }
764
+ // ===== Orderbook Analysis =====
765
+ /**
766
+ * Get processed orderbook with analytics (alias for getProcessedOrderbook)
767
+ */
768
+ async getOrderbook(conditionId) {
769
+ return this.getProcessedOrderbook(conditionId);
770
+ }
771
+ /**
772
+ * Detect arbitrage opportunity
773
+ *
774
+ * 使用有效价格(考虑镜像订单)计算套利机会
775
+ * 详细原理见: docs/01-polymarket-orderbook-arbitrage.md
776
+ */
777
+ async detectArbitrage(conditionId, threshold = 0.005) {
778
+ const orderbook = await this.getOrderbook(conditionId);
779
+ const { effectivePrices } = orderbook.summary;
780
+ if (orderbook.summary.longArbProfit > threshold) {
781
+ return {
782
+ type: 'long',
783
+ profit: orderbook.summary.longArbProfit,
784
+ // 使用有效价格描述实际操作
785
+ action: `Buy YES @ ${effectivePrices.effectiveBuyYes.toFixed(4)} + NO @ ${effectivePrices.effectiveBuyNo.toFixed(4)}, Merge for $1`,
786
+ expectedProfit: orderbook.summary.longArbProfit,
787
+ };
788
+ }
789
+ if (orderbook.summary.shortArbProfit > threshold) {
790
+ return {
791
+ type: 'short',
792
+ profit: orderbook.summary.shortArbProfit,
793
+ // 使用有效价格描述实际操作
794
+ action: `Split $1, Sell YES @ ${effectivePrices.effectiveSellYes.toFixed(4)} + NO @ ${effectivePrices.effectiveSellNo.toFixed(4)}`,
795
+ expectedProfit: orderbook.summary.shortArbProfit,
796
+ };
797
+ }
798
+ return null;
799
+ }
800
+ // ===== Market Discovery =====
801
+ /**
802
+ * Get trending markets
803
+ */
804
+ async getTrendingMarkets(limit = 20) {
805
+ if (!this.gammaApi) {
806
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, 'GammaApiClient is required for trending markets');
807
+ }
808
+ return this.gammaApi.getTrendingMarkets(limit);
809
+ }
810
+ /**
811
+ * Search markets
812
+ */
813
+ async searchMarkets(params) {
814
+ if (!this.gammaApi) {
815
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, 'GammaApiClient is required for market search');
816
+ }
817
+ return this.gammaApi.getMarkets(params);
818
+ }
819
+ /**
820
+ * Scan for short-term crypto markets (Up/Down markets ending soon)
821
+ *
822
+ * ## Market Types
823
+ * Polymarket has short-term crypto markets in two durations:
824
+ * - **5-minute markets**: slug pattern `{coin}-updown-5m-{timestamp}`
825
+ * - **15-minute markets**: slug pattern `{coin}-updown-15m-{timestamp}`
826
+ *
827
+ * ## Slug Pattern
828
+ * The timestamp in the slug is the START time of the time window:
829
+ * - 15-minute markets: `{coin}-updown-15m-{Math.floor(startTime / 900) * 900}`
830
+ * - 5-minute markets: `{coin}-updown-5m-{Math.floor(startTime / 300) * 300}`
831
+ *
832
+ * Example: `btc-updown-15m-1767456000` starts at 1767456000 (16:00:00 UTC)
833
+ * and ends 15 minutes later at 1767456900 (16:15:00 UTC)
834
+ *
835
+ * ## Supported Coins
836
+ * - BTC (Bitcoin)
837
+ * - ETH (Ethereum)
838
+ * - SOL (Solana)
839
+ * - XRP (Ripple)
840
+ *
841
+ * ## Market Lifecycle Rules
842
+ * 1. Markets are created ahead of time (before they become tradeable)
843
+ * 2. New markets may not have prices yet (show 0.5/0.5)
844
+ * 3. When one market ends, the next one is already open for trading
845
+ * 4. A market ending doesn't mean no price - it means resolution is pending
846
+ *
847
+ * ## Outcomes
848
+ * All crypto short-term markets have:
849
+ * - outcomes: ["Up", "Down"]
850
+ * - Resolution based on price movement during the time window
851
+ *
852
+ * @param options - Scan options
853
+ * @param options.minMinutesUntilEnd - Minimum minutes until market ends (default: 5)
854
+ * @param options.maxMinutesUntilEnd - Maximum minutes until market ends (default: 60)
855
+ * @param options.limit - Maximum number of markets to return (default: 20)
856
+ * @param options.sortBy - Sort field: 'endDate' | 'volume' | 'liquidity' (default: 'endDate')
857
+ * @param options.duration - Filter by duration: '5m' | '15m' | 'all' (default: 'all')
858
+ * @param options.coin - Filter by coin: 'BTC' | 'ETH' | 'SOL' | 'XRP' | 'all' (default: 'all')
859
+ * @returns Array of crypto short-term markets
860
+ *
861
+ * @example
862
+ * ```typescript
863
+ * // Find all 15-minute markets ending in 5-30 minutes
864
+ * const markets = await sdk.markets.scanCryptoShortTermMarkets({
865
+ * minMinutesUntilEnd: 5,
866
+ * maxMinutesUntilEnd: 30,
867
+ * duration: '15m',
868
+ * });
869
+ *
870
+ * // Find BTC 5-minute markets only
871
+ * const btcMarkets = await sdk.markets.scanCryptoShortTermMarkets({
872
+ * coin: 'BTC',
873
+ * duration: '5m',
874
+ * });
875
+ * ```
876
+ */
877
+ async scanCryptoShortTermMarkets(options) {
878
+ if (!this.gammaApi) {
879
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, 'GammaApiClient is required for market scanning');
880
+ }
881
+ const { minMinutesUntilEnd = 5, maxMinutesUntilEnd = 60, limit = 20, sortBy = 'endDate', duration = 'all', coin = 'all', } = options ?? {};
882
+ // Duration to interval seconds mapping
883
+ const durationIntervals = {
884
+ '5m': 300, // 5 minutes in seconds
885
+ '15m': 900, // 15 minutes in seconds
886
+ };
887
+ // Supported coins
888
+ const allCoins = ['btc', 'eth', 'sol', 'xrp'];
889
+ const targetCoins = coin === 'all' ? allCoins : [coin.toLowerCase()];
890
+ // Target durations
891
+ const targetDurations = duration === 'all' ? ['5m', '15m'] : [duration];
892
+ // Calculate time slots to fetch
893
+ const nowSeconds = Math.floor(Date.now() / 1000);
894
+ const minEndSeconds = nowSeconds + minMinutesUntilEnd * 60;
895
+ const maxEndSeconds = nowSeconds + maxMinutesUntilEnd * 60;
896
+ // Generate slugs for all combinations
897
+ const slugsToFetch = [];
898
+ for (const dur of targetDurations) {
899
+ const intervalSeconds = durationIntervals[dur];
900
+ const durationStr = dur.replace('m', 'm'); // 5m or 15m
901
+ // Calculate the current slot and extend to cover the time range
902
+ // The slug timestamp is the START time, endTime = startTime + interval
903
+ // So if we want markets ending after minEndSeconds:
904
+ // startTime + interval >= minEndSeconds => startTime >= minEndSeconds - interval
905
+ // And ending before maxEndSeconds:
906
+ // startTime + interval <= maxEndSeconds => startTime <= maxEndSeconds - interval
907
+ const minSlotStart = Math.floor((minEndSeconds - intervalSeconds) / intervalSeconds) * intervalSeconds;
908
+ const maxSlotStart = Math.ceil(maxEndSeconds / intervalSeconds) * intervalSeconds;
909
+ // Generate slots from minSlotStart to maxSlotStart
910
+ for (let slotStart = minSlotStart; slotStart <= maxSlotStart; slotStart += intervalSeconds) {
911
+ for (const coinName of targetCoins) {
912
+ slugsToFetch.push(`${coinName}-updown-${durationStr}-${slotStart}`);
913
+ }
914
+ }
915
+ }
916
+ // Fetch markets in parallel batches
917
+ const BATCH_SIZE = 10;
918
+ const allMarkets = [];
919
+ for (let i = 0; i < slugsToFetch.length; i += BATCH_SIZE) {
920
+ const batch = slugsToFetch.slice(i, i + BATCH_SIZE);
921
+ const results = await Promise.all(batch.map(async (slug) => {
922
+ try {
923
+ const markets = await this.gammaApi.getMarkets({ slug, limit: 1 });
924
+ return markets.length > 0 ? markets[0] : null;
925
+ }
926
+ catch {
927
+ return null;
928
+ }
929
+ }));
930
+ for (const market of results) {
931
+ if (market && market.active && !market.closed) {
932
+ allMarkets.push(market);
933
+ }
934
+ }
935
+ }
936
+ // Filter by end time range
937
+ const nowMs = Date.now();
938
+ const minEndTime = nowMs + minMinutesUntilEnd * 60 * 1000;
939
+ const maxEndTime = nowMs + maxMinutesUntilEnd * 60 * 1000;
940
+ const filteredMarkets = allMarkets.filter((market) => {
941
+ const endTime = market.endDate ? new Date(market.endDate).getTime() : 0;
942
+ return endTime >= minEndTime && endTime <= maxEndTime;
943
+ });
944
+ // Sort by preference
945
+ if (sortBy === 'volume') {
946
+ filteredMarkets.sort((a, b) => (b.volume24hr ?? 0) - (a.volume24hr ?? 0));
947
+ }
948
+ else if (sortBy === 'liquidity') {
949
+ filteredMarkets.sort((a, b) => (b.liquidity ?? 0) - (a.liquidity ?? 0));
950
+ }
951
+ else {
952
+ // Sort by endDate (soonest first)
953
+ filteredMarkets.sort((a, b) => {
954
+ const aEnd = a.endDate ? new Date(a.endDate).getTime() : Infinity;
955
+ const bEnd = b.endDate ? new Date(b.endDate).getTime() : Infinity;
956
+ return aEnd - bEnd;
957
+ });
958
+ }
959
+ return filteredMarkets.slice(0, limit);
960
+ }
961
+ // ===== Market Signal Detection =====
962
+ /**
963
+ * Detect market signals (volume surge, depth imbalance, whale trades)
964
+ */
965
+ async detectMarketSignals(conditionId) {
966
+ const signals = [];
967
+ if (!this.dataApi) {
968
+ throw new PolymarketError(ErrorCode.INVALID_CONFIG, 'DataApiClient is required for signal detection');
969
+ }
970
+ const market = await this.getMarket(conditionId);
971
+ const orderbook = await this.getOrderbook(conditionId);
972
+ const trades = await this.dataApi.getTradesByMarket(conditionId, 100);
973
+ // Volume surge detection
974
+ if (market.volume24hr && market.volume > 0) {
975
+ const avgDaily = market.volume / 7; // Approximate
976
+ const ratio = market.volume24hr / avgDaily;
977
+ if (ratio > 2) {
978
+ signals.push({
979
+ type: 'volume_surge',
980
+ severity: ratio > 5 ? 'high' : ratio > 3 ? 'medium' : 'low',
981
+ details: { volume24hr: market.volume24hr, avgDaily, ratio },
982
+ });
983
+ }
984
+ }
985
+ // Depth imbalance detection
986
+ if (orderbook.summary.imbalanceRatio > 1.5 || orderbook.summary.imbalanceRatio < 0.67) {
987
+ const ratio = orderbook.summary.imbalanceRatio;
988
+ signals.push({
989
+ type: 'depth_imbalance',
990
+ severity: ratio > 3 || ratio < 0.33 ? 'high' : 'medium',
991
+ details: {
992
+ imbalanceRatio: ratio,
993
+ bidDepth: orderbook.summary.totalBidDepth,
994
+ askDepth: orderbook.summary.totalAskDepth,
995
+ direction: ratio > 1 ? 'BUY_PRESSURE' : 'SELL_PRESSURE',
996
+ },
997
+ });
998
+ }
999
+ // Whale trade detection
1000
+ const recentLargeTrades = trades.filter((t) => t.size * t.price > 1000);
1001
+ for (const trade of recentLargeTrades.slice(0, 3)) {
1002
+ const value = trade.size * trade.price;
1003
+ signals.push({
1004
+ type: 'whale_trade',
1005
+ severity: value > 10000 ? 'high' : value > 5000 ? 'medium' : 'low',
1006
+ details: {
1007
+ size: trade.size,
1008
+ price: trade.price,
1009
+ usdValue: value,
1010
+ side: trade.side,
1011
+ outcome: trade.outcome,
1012
+ },
1013
+ });
1014
+ }
1015
+ return signals;
1016
+ }
1017
+ // ===== Helper Methods =====
1018
+ normalizeClobMarket(m) {
1019
+ return {
1020
+ conditionId: m.condition_id,
1021
+ questionId: m.question_id,
1022
+ marketSlug: m.market_slug,
1023
+ question: m.question,
1024
+ description: m.description,
1025
+ tokens: m.tokens.map(t => ({
1026
+ tokenId: t.token_id,
1027
+ outcome: t.outcome,
1028
+ price: t.price,
1029
+ winner: t.winner,
1030
+ })),
1031
+ active: m.active,
1032
+ closed: m.closed,
1033
+ acceptingOrders: m.accepting_orders,
1034
+ endDateIso: m.end_date_iso,
1035
+ negRisk: m.neg_risk,
1036
+ minimumOrderSize: m.minimum_order_size,
1037
+ minimumTickSize: m.minimum_tick_size,
1038
+ };
1039
+ }
1040
+ processOrderbooks(yesBook, noBook, yesTokenId, noTokenId) {
1041
+ const yesBestBid = yesBook.bids[0]?.price || 0;
1042
+ const yesBestAsk = yesBook.asks[0]?.price || 1;
1043
+ const noBestBid = noBook.bids[0]?.price || 0;
1044
+ const noBestAsk = noBook.asks[0]?.price || 1;
1045
+ const yesBidDepth = yesBook.bids.reduce((sum, l) => sum + l.price * l.size, 0);
1046
+ const yesAskDepth = yesBook.asks.reduce((sum, l) => sum + l.price * l.size, 0);
1047
+ const noBidDepth = noBook.bids.reduce((sum, l) => sum + l.price * l.size, 0);
1048
+ const noAskDepth = noBook.asks.reduce((sum, l) => sum + l.price * l.size, 0);
1049
+ const askSum = yesBestAsk + noBestAsk;
1050
+ const bidSum = yesBestBid + noBestBid;
1051
+ // Effective prices (accounting for mirroring)
1052
+ const effectivePrices = {
1053
+ effectiveBuyYes: Math.min(yesBestAsk, 1 - noBestBid),
1054
+ effectiveBuyNo: Math.min(noBestAsk, 1 - yesBestBid),
1055
+ effectiveSellYes: Math.max(yesBestBid, 1 - noBestAsk),
1056
+ effectiveSellNo: Math.max(noBestBid, 1 - yesBestAsk),
1057
+ };
1058
+ const effectiveLongCost = effectivePrices.effectiveBuyYes + effectivePrices.effectiveBuyNo;
1059
+ const effectiveShortRevenue = effectivePrices.effectiveSellYes + effectivePrices.effectiveSellNo;
1060
+ const longArbProfit = 1 - effectiveLongCost;
1061
+ const shortArbProfit = effectiveShortRevenue - 1;
1062
+ const yesSpread = yesBestAsk - yesBestBid;
1063
+ return {
1064
+ yes: {
1065
+ bid: yesBestBid,
1066
+ ask: yesBestAsk,
1067
+ bidSize: yesBook.bids[0]?.size || 0,
1068
+ askSize: yesBook.asks[0]?.size || 0,
1069
+ bidDepth: yesBidDepth,
1070
+ askDepth: yesAskDepth,
1071
+ spread: yesSpread,
1072
+ tokenId: yesTokenId,
1073
+ },
1074
+ no: {
1075
+ bid: noBestBid,
1076
+ ask: noBestAsk,
1077
+ bidSize: noBook.bids[0]?.size || 0,
1078
+ askSize: noBook.asks[0]?.size || 0,
1079
+ bidDepth: noBidDepth,
1080
+ askDepth: noAskDepth,
1081
+ spread: noBestAsk - noBestBid,
1082
+ tokenId: noTokenId,
1083
+ },
1084
+ summary: {
1085
+ askSum,
1086
+ bidSum,
1087
+ effectivePrices,
1088
+ effectiveLongCost,
1089
+ effectiveShortRevenue,
1090
+ longArbProfit,
1091
+ shortArbProfit,
1092
+ totalBidDepth: yesBidDepth + noBidDepth,
1093
+ totalAskDepth: yesAskDepth + noAskDepth,
1094
+ imbalanceRatio: (yesBidDepth + noBidDepth) / (yesAskDepth + noAskDepth + 0.001),
1095
+ yesSpread,
1096
+ },
1097
+ };
1098
+ }
1099
+ mergeMarkets(gamma, clob) {
1100
+ // Build tokens array from CLOB data, falling back to Gamma prices
1101
+ const tokens = clob.tokens.map((t, index) => ({
1102
+ tokenId: t.tokenId,
1103
+ outcome: t.outcome,
1104
+ price: t.price || gamma.outcomePrices[index] || 0.5,
1105
+ winner: t.winner,
1106
+ }));
1107
+ return {
1108
+ conditionId: clob.conditionId,
1109
+ slug: gamma.slug,
1110
+ question: clob.question,
1111
+ description: clob.description || gamma.description,
1112
+ tokens,
1113
+ volume: gamma.volume,
1114
+ volume24hr: gamma.volume24hr,
1115
+ liquidity: gamma.liquidity,
1116
+ spread: gamma.spread,
1117
+ oneDayPriceChange: gamma.oneDayPriceChange,
1118
+ oneWeekPriceChange: gamma.oneWeekPriceChange,
1119
+ active: clob.active,
1120
+ closed: clob.closed,
1121
+ acceptingOrders: clob.acceptingOrders,
1122
+ endDate: clob.endDateIso ? new Date(clob.endDateIso) : new Date(),
1123
+ source: 'merged',
1124
+ };
1125
+ }
1126
+ fromGammaMarket(gamma) {
1127
+ // Create tokens from Gamma outcomes - use actual outcome names from gamma data
1128
+ // This supports Yes/No, Up/Down, Team1/Team2, Heads/Tails, etc.
1129
+ const outcomes = gamma.outcomes || ['Yes', 'No'];
1130
+ const tokens = [
1131
+ { tokenId: '', outcome: outcomes[0], price: gamma.outcomePrices[0] || 0.5 },
1132
+ { tokenId: '', outcome: outcomes[1], price: gamma.outcomePrices[1] || 0.5 },
1133
+ ];
1134
+ return {
1135
+ conditionId: gamma.conditionId,
1136
+ slug: gamma.slug,
1137
+ question: gamma.question,
1138
+ description: gamma.description,
1139
+ tokens,
1140
+ volume: gamma.volume,
1141
+ volume24hr: gamma.volume24hr,
1142
+ liquidity: gamma.liquidity,
1143
+ spread: gamma.spread,
1144
+ oneDayPriceChange: gamma.oneDayPriceChange,
1145
+ oneWeekPriceChange: gamma.oneWeekPriceChange,
1146
+ active: gamma.active,
1147
+ closed: gamma.closed,
1148
+ acceptingOrders: !gamma.closed,
1149
+ endDate: gamma.endDate,
1150
+ source: 'gamma',
1151
+ };
1152
+ }
1153
+ fromClobMarket(clob) {
1154
+ // Convert CLOB tokens to UnifiedMarketToken format
1155
+ const tokens = clob.tokens.map(t => ({
1156
+ tokenId: t.tokenId,
1157
+ outcome: t.outcome,
1158
+ price: t.price,
1159
+ winner: t.winner,
1160
+ }));
1161
+ return {
1162
+ conditionId: clob.conditionId,
1163
+ slug: clob.marketSlug,
1164
+ question: clob.question,
1165
+ description: clob.description,
1166
+ tokens,
1167
+ volume: 0,
1168
+ volume24hr: undefined,
1169
+ liquidity: 0,
1170
+ spread: undefined,
1171
+ active: clob.active,
1172
+ closed: clob.closed,
1173
+ acceptingOrders: clob.acceptingOrders,
1174
+ endDate: clob.endDateIso ? new Date(clob.endDateIso) : new Date(),
1175
+ source: 'clob',
1176
+ };
1177
+ }
1178
+ }
1179
+ // ===== Utility Functions =====
1180
+ export function getIntervalMs(interval) {
1181
+ const map = {
1182
+ // Second-level intervals (for 15-minute crypto markets)
1183
+ '1s': 1 * 1000,
1184
+ '5s': 5 * 1000,
1185
+ '15s': 15 * 1000,
1186
+ '30s': 30 * 1000,
1187
+ // Minute-level intervals
1188
+ '1m': 60 * 1000,
1189
+ '5m': 5 * 60 * 1000,
1190
+ '15m': 15 * 60 * 1000,
1191
+ '30m': 30 * 60 * 1000,
1192
+ // Hour-level intervals
1193
+ '1h': 60 * 60 * 1000,
1194
+ '4h': 4 * 60 * 60 * 1000,
1195
+ '12h': 12 * 60 * 60 * 1000,
1196
+ '1d': 24 * 60 * 60 * 1000,
1197
+ };
1198
+ return map[interval];
1199
+ }
1200
+ //# sourceMappingURL=market-service.js.map