@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,1602 @@
1
+ /**
2
+ * DipArbService - Dip Arbitrage Service
3
+ *
4
+ * 暴跌套利服务 - 针对 Polymarket 15分钟/5分钟 UP/DOWN 市场
5
+ *
6
+ * 策略原理:
7
+ * 1. 每个市场有一个 "price to beat"(开盘时的 Chainlink 价格)
8
+ * 2. 结算规则:
9
+ * - UP 赢:结束时价格 >= price to beat
10
+ * - DOWN 赢:结束时价格 < price to beat
11
+ *
12
+ * 3. 套利流程:
13
+ * - Leg1:检测暴跌 → 买入暴跌侧
14
+ * - Leg2:等待对冲条件 → 买入另一侧
15
+ * - 利润:总成本 < $1 时获得无风险利润
16
+ *
17
+ * 使用示例:
18
+ * ```typescript
19
+ * const sdk = await PolymarketSDK.create({ privateKey: '...' });
20
+ *
21
+ * // 自动找到并启动
22
+ * await sdk.dipArb.findAndStart({ coin: 'BTC' });
23
+ *
24
+ * // 监听信号
25
+ * sdk.dipArb.on('signal', (signal) => {
26
+ * console.log(`Signal: ${signal.type} ${signal.side}`);
27
+ * });
28
+ * ```
29
+ */
30
+ import { EventEmitter } from 'events';
31
+ import { CTFClient } from '../clients/ctf-client.js';
32
+ import { DEFAULT_DIP_ARB_CONFIG, DEFAULT_AUTO_ROTATE_CONFIG, createDipArbInitialStats, createDipArbRoundState, calculateDipArbProfitRate, estimateUpWinRate, detectMispricing, parseUnderlyingFromSlug, parseDurationFromSlug, isDipArbLeg1Signal, } from './dip-arb-types.js';
33
+ // ===== DipArbService =====
34
+ export class DipArbService extends EventEmitter {
35
+ // Dependencies
36
+ realtimeService;
37
+ tradingService = null;
38
+ marketService;
39
+ ctf = null;
40
+ // Configuration
41
+ config;
42
+ autoRotateConfig;
43
+ // State
44
+ market = null;
45
+ currentRound = null;
46
+ isRunning = false;
47
+ isExecuting = false;
48
+ lastExecutionTime = 0;
49
+ stats;
50
+ // Subscriptions
51
+ marketSubscription = null;
52
+ chainlinkSubscription = null;
53
+ // Auto-rotate state
54
+ rotateCheckInterval = null;
55
+ nextMarket = null;
56
+ // Pending redemption state (for background redemption after market resolution)
57
+ pendingRedemptions = [];
58
+ redeemCheckInterval = null;
59
+ // Orderbook state
60
+ upAsks = [];
61
+ downAsks = [];
62
+ // Price history for sliding window detection
63
+ // Each entry: { timestamp: number, upAsk: number, downAsk: number }
64
+ priceHistory = [];
65
+ MAX_HISTORY_LENGTH = 100; // Keep last 100 price points
66
+ // Price state
67
+ currentUnderlyingPrice = 0;
68
+ // Signal state - prevent duplicate signals within same round
69
+ leg1SignalEmitted = false;
70
+ constructor(realtimeService, tradingService, marketService, privateKey, chainId = 137) {
71
+ super();
72
+ this.realtimeService = realtimeService;
73
+ this.tradingService = tradingService;
74
+ this.marketService = marketService;
75
+ // Initialize with default config
76
+ this.config = { ...DEFAULT_DIP_ARB_CONFIG };
77
+ this.autoRotateConfig = { ...DEFAULT_AUTO_ROTATE_CONFIG };
78
+ this.stats = createDipArbInitialStats();
79
+ // Initialize CTF if private key provided
80
+ if (privateKey) {
81
+ this.ctf = new CTFClient({
82
+ privateKey,
83
+ rpcUrl: 'https://polygon-rpc.com',
84
+ chainId,
85
+ });
86
+ }
87
+ }
88
+ // ===== Public API: Configuration =====
89
+ /**
90
+ * Update configuration
91
+ */
92
+ updateConfig(config) {
93
+ this.config = {
94
+ ...this.config,
95
+ ...config,
96
+ };
97
+ this.log(`Config updated: ${JSON.stringify(config)}`);
98
+ }
99
+ /**
100
+ * Get current configuration
101
+ */
102
+ getConfig() {
103
+ return { ...this.config };
104
+ }
105
+ // ===== Public API: Market Discovery =====
106
+ /**
107
+ * Scan for upcoming UP/DOWN markets
108
+ *
109
+ * Uses MarketService.scanCryptoShortTermMarkets()
110
+ */
111
+ async scanUpcomingMarkets(options = {}) {
112
+ const { coin = 'all', duration = 'all', minMinutesUntilEnd = 5, maxMinutesUntilEnd = 60, limit = 20, } = options;
113
+ try {
114
+ const gammaMarkets = await this.marketService.scanCryptoShortTermMarkets({
115
+ coin: coin,
116
+ duration: duration,
117
+ minMinutesUntilEnd,
118
+ maxMinutesUntilEnd,
119
+ limit,
120
+ sortBy: 'endDate',
121
+ });
122
+ // Get full market info with token IDs for each market
123
+ const results = [];
124
+ for (const gm of gammaMarkets) {
125
+ // Retry up to 3 times for network errors
126
+ let retries = 3;
127
+ while (retries > 0) {
128
+ try {
129
+ // Get full market info from CLOB API via MarketService
130
+ const market = await this.marketService.getMarket(gm.conditionId);
131
+ // Find UP and DOWN tokens
132
+ const upToken = market.tokens.find(t => t.outcome.toLowerCase() === 'up' || t.outcome.toLowerCase() === 'yes');
133
+ const downToken = market.tokens.find(t => t.outcome.toLowerCase() === 'down' || t.outcome.toLowerCase() === 'no');
134
+ if (upToken?.tokenId && downToken?.tokenId) {
135
+ results.push({
136
+ name: gm.question,
137
+ slug: gm.slug,
138
+ conditionId: gm.conditionId,
139
+ upTokenId: upToken.tokenId,
140
+ downTokenId: downToken.tokenId,
141
+ underlying: parseUnderlyingFromSlug(gm.slug),
142
+ durationMinutes: parseDurationFromSlug(gm.slug),
143
+ endTime: gm.endDate,
144
+ });
145
+ }
146
+ break; // Success, exit retry loop
147
+ }
148
+ catch (error) {
149
+ retries--;
150
+ if (retries > 0) {
151
+ // Wait 1 second before retry
152
+ await new Promise(r => setTimeout(r, 1000));
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return results;
158
+ }
159
+ catch (error) {
160
+ this.emit('error', error instanceof Error ? error : new Error(String(error)));
161
+ return [];
162
+ }
163
+ }
164
+ /**
165
+ * Find the best market and start monitoring
166
+ */
167
+ async findAndStart(options = {}) {
168
+ const { coin, preferDuration = '15m' } = options;
169
+ const scanOptions = {
170
+ coin: coin || 'all',
171
+ duration: preferDuration,
172
+ minMinutesUntilEnd: 10,
173
+ maxMinutesUntilEnd: 60,
174
+ limit: 10,
175
+ };
176
+ const markets = await this.scanUpcomingMarkets(scanOptions);
177
+ if (markets.length === 0) {
178
+ this.log('No suitable markets found');
179
+ return null;
180
+ }
181
+ // Find the best market (prefer specified coin, then by time)
182
+ let bestMarket = markets[0];
183
+ if (coin) {
184
+ const coinMarket = markets.find(m => m.underlying === coin);
185
+ if (coinMarket) {
186
+ bestMarket = coinMarket;
187
+ }
188
+ }
189
+ await this.start(bestMarket);
190
+ return bestMarket;
191
+ }
192
+ // ===== Public API: Lifecycle =====
193
+ /**
194
+ * Start monitoring a market
195
+ */
196
+ async start(market) {
197
+ if (this.isRunning) {
198
+ throw new Error('DipArbService is already running. Call stop() first.');
199
+ }
200
+ // Validate token IDs
201
+ if (!market.upTokenId || !market.downTokenId) {
202
+ throw new Error(`Invalid market config: missing token IDs. upTokenId=${market.upTokenId}, downTokenId=${market.downTokenId}`);
203
+ }
204
+ this.market = market;
205
+ this.isRunning = true;
206
+ this.stats = createDipArbInitialStats();
207
+ this.priceHistory = []; // Clear price history for new market
208
+ this.log(`Starting Dip Arb monitor for: ${market.name}`);
209
+ this.log(`Condition ID: ${market.conditionId.slice(0, 20)}...`);
210
+ this.log(`Underlying: ${market.underlying}`);
211
+ this.log(`Duration: ${market.durationMinutes}m`);
212
+ this.log(`Auto Execute: ${this.config.autoExecute ? 'YES' : 'NO'}`);
213
+ // Initialize trading service if available
214
+ if (this.tradingService) {
215
+ try {
216
+ await this.tradingService.initialize();
217
+ this.log(`Wallet: ${this.ctf?.getAddress()}`);
218
+ }
219
+ catch (error) {
220
+ this.log(`Warning: Trading service init failed: ${error}`);
221
+ }
222
+ }
223
+ else {
224
+ this.log('No wallet configured - monitoring only');
225
+ }
226
+ // Connect realtime service and wait for connection
227
+ this.realtimeService.connect();
228
+ // Wait for WebSocket connection (with timeout)
229
+ await new Promise((resolve) => {
230
+ const timeout = setTimeout(() => {
231
+ this.log('Warning: WebSocket connection timeout, proceeding anyway');
232
+ resolve();
233
+ }, 10000);
234
+ // Check if already connected
235
+ if (this.realtimeService.isConnected?.()) {
236
+ clearTimeout(timeout);
237
+ resolve();
238
+ return;
239
+ }
240
+ this.realtimeService.once('connected', () => {
241
+ clearTimeout(timeout);
242
+ this.log('WebSocket connected');
243
+ resolve();
244
+ });
245
+ });
246
+ // Subscribe to market orderbook
247
+ this.log(`Subscribing to tokens: UP=${market.upTokenId.slice(0, 20)}..., DOWN=${market.downTokenId.slice(0, 20)}...`);
248
+ this.marketSubscription = this.realtimeService.subscribeMarkets([market.upTokenId, market.downTokenId], {
249
+ onOrderbook: (book) => {
250
+ if (this.config.debug) {
251
+ const bookTokenId = book.tokenId || book.assetId;
252
+ const tokenType = bookTokenId === market.upTokenId ? 'UP' :
253
+ bookTokenId === market.downTokenId ? 'DOWN' : 'UNKNOWN';
254
+ const bestAsk = book.asks[0]?.price ?? 'N/A';
255
+ this.log(`Orderbook [${tokenType}]: asks=${book.asks.length}, bestAsk=${bestAsk}`);
256
+ }
257
+ this.handleOrderbookUpdate(book);
258
+ },
259
+ onError: (error) => this.emit('error', error),
260
+ });
261
+ // Subscribe to Chainlink prices for the underlying asset
262
+ // Format: ETH -> ETH/USD
263
+ const chainlinkSymbol = `${market.underlying}/USD`;
264
+ console.log(`[DipArb] Subscribing to Chainlink prices: ${chainlinkSymbol}`);
265
+ this.chainlinkSubscription = this.realtimeService.subscribeCryptoChainlinkPrices([chainlinkSymbol], {
266
+ onPrice: (price) => {
267
+ console.log(`[DipArb] Chainlink price received: ${price.symbol} = $${price.price}`);
268
+ this.handleChainlinkPriceUpdate(price);
269
+ },
270
+ });
271
+ this.emit('started', market);
272
+ this.log('Monitoring for dip arbitrage opportunities...');
273
+ }
274
+ /**
275
+ * Stop monitoring
276
+ */
277
+ async stop() {
278
+ if (!this.isRunning)
279
+ return;
280
+ this.isRunning = false;
281
+ // Stop rotate check
282
+ this.stopRotateCheck();
283
+ // Unsubscribe
284
+ if (this.marketSubscription) {
285
+ this.marketSubscription.unsubscribe();
286
+ this.marketSubscription = null;
287
+ }
288
+ if (this.chainlinkSubscription) {
289
+ this.chainlinkSubscription.unsubscribe();
290
+ this.chainlinkSubscription = null;
291
+ }
292
+ // Update stats
293
+ this.stats.runningTimeMs = Date.now() - this.stats.startTime;
294
+ this.log('Stopped');
295
+ this.log(`Rounds monitored: ${this.stats.roundsMonitored}`);
296
+ this.log(`Rounds completed: ${this.stats.roundsSuccessful}`);
297
+ this.log(`Total profit: $${this.stats.totalProfit.toFixed(2)}`);
298
+ this.emit('stopped');
299
+ }
300
+ /**
301
+ * Check if service is running
302
+ */
303
+ isActive() {
304
+ return this.isRunning;
305
+ }
306
+ /**
307
+ * Get current market
308
+ */
309
+ getMarket() {
310
+ return this.market;
311
+ }
312
+ // ===== Public API: State Access =====
313
+ /**
314
+ * Get statistics
315
+ */
316
+ getStats() {
317
+ return {
318
+ ...this.stats,
319
+ runningTimeMs: this.isRunning ? Date.now() - this.stats.startTime : this.stats.runningTimeMs,
320
+ currentRound: this.currentRound ? {
321
+ roundId: this.currentRound.roundId,
322
+ phase: this.currentRound.phase,
323
+ priceToBeat: this.currentRound.priceToBeat,
324
+ leg1: this.currentRound.leg1 ? {
325
+ side: this.currentRound.leg1.side,
326
+ price: this.currentRound.leg1.price,
327
+ } : undefined,
328
+ } : undefined,
329
+ };
330
+ }
331
+ /**
332
+ * Get current round state
333
+ */
334
+ getCurrentRound() {
335
+ return this.currentRound ? { ...this.currentRound } : null;
336
+ }
337
+ /**
338
+ * Get current price to beat
339
+ */
340
+ getPriceToBeat() {
341
+ return this.currentRound?.priceToBeat ?? null;
342
+ }
343
+ // ===== Public API: Manual Execution =====
344
+ /**
345
+ * Execute Leg1 trade
346
+ */
347
+ async executeLeg1(signal) {
348
+ const startTime = Date.now();
349
+ if (!this.tradingService || !this.market || !this.currentRound) {
350
+ return {
351
+ success: false,
352
+ leg: 'leg1',
353
+ roundId: signal.roundId,
354
+ error: 'Trading service not available or no active round',
355
+ executionTimeMs: Date.now() - startTime,
356
+ };
357
+ }
358
+ try {
359
+ this.isExecuting = true;
360
+ // 计算拆分订单参数
361
+ const splitCount = Math.max(1, this.config.splitOrders);
362
+ const sharesPerOrder = signal.shares / splitCount;
363
+ const amountPerOrder = sharesPerOrder * signal.targetPrice;
364
+ // 检查每笔订单是否满足最小金额要求
365
+ if (amountPerOrder < 1) {
366
+ return {
367
+ success: false,
368
+ leg: 'leg1',
369
+ roundId: signal.roundId,
370
+ error: `Order amount ($${amountPerOrder.toFixed(2)}) is below Polymarket minimum ($1)`,
371
+ executionTimeMs: Date.now() - startTime,
372
+ };
373
+ }
374
+ let totalSharesFilled = 0;
375
+ let totalAmountSpent = 0;
376
+ let lastOrderId;
377
+ let failedOrders = 0;
378
+ // 执行多笔订单
379
+ for (let i = 0; i < splitCount; i++) {
380
+ const orderParams = {
381
+ tokenId: signal.tokenId,
382
+ side: 'BUY',
383
+ amount: amountPerOrder,
384
+ };
385
+ if (this.config.debug && splitCount > 1) {
386
+ this.log(`Leg1 order ${i + 1}/${splitCount}: ${sharesPerOrder.toFixed(2)} shares @ ${signal.targetPrice.toFixed(4)}`);
387
+ }
388
+ const result = await this.tradingService.createMarketOrder(orderParams);
389
+ if (result.success) {
390
+ totalSharesFilled += sharesPerOrder;
391
+ totalAmountSpent += amountPerOrder;
392
+ lastOrderId = result.orderId;
393
+ }
394
+ else {
395
+ failedOrders++;
396
+ this.log(`Leg1 order ${i + 1}/${splitCount} failed: ${result.errorMsg}`);
397
+ }
398
+ // 订单间隔
399
+ if (i < splitCount - 1 && this.config.orderIntervalMs > 0) {
400
+ await new Promise(resolve => setTimeout(resolve, this.config.orderIntervalMs));
401
+ }
402
+ }
403
+ // 至少有一笔成功
404
+ if (totalSharesFilled > 0) {
405
+ const avgPrice = totalAmountSpent / totalSharesFilled;
406
+ // Record leg1 fill
407
+ this.currentRound.leg1 = {
408
+ side: signal.dipSide,
409
+ price: avgPrice,
410
+ shares: totalSharesFilled,
411
+ timestamp: Date.now(),
412
+ tokenId: signal.tokenId,
413
+ };
414
+ this.currentRound.phase = 'leg1_filled';
415
+ this.stats.leg1Filled++;
416
+ this.lastExecutionTime = Date.now();
417
+ if (splitCount > 1) {
418
+ this.log(`Leg1 completed: ${splitCount - failedOrders}/${splitCount} orders filled, total ${totalSharesFilled.toFixed(2)} shares`);
419
+ }
420
+ return {
421
+ success: true,
422
+ leg: 'leg1',
423
+ roundId: signal.roundId,
424
+ side: signal.dipSide,
425
+ price: avgPrice,
426
+ shares: totalSharesFilled,
427
+ orderId: lastOrderId,
428
+ executionTimeMs: Date.now() - startTime,
429
+ };
430
+ }
431
+ else {
432
+ return {
433
+ success: false,
434
+ leg: 'leg1',
435
+ roundId: signal.roundId,
436
+ error: 'All orders failed',
437
+ executionTimeMs: Date.now() - startTime,
438
+ };
439
+ }
440
+ }
441
+ catch (error) {
442
+ return {
443
+ success: false,
444
+ leg: 'leg1',
445
+ roundId: signal.roundId,
446
+ error: error instanceof Error ? error.message : String(error),
447
+ executionTimeMs: Date.now() - startTime,
448
+ };
449
+ }
450
+ finally {
451
+ this.isExecuting = false;
452
+ }
453
+ }
454
+ /**
455
+ * Execute Leg2 trade
456
+ */
457
+ async executeLeg2(signal) {
458
+ const startTime = Date.now();
459
+ if (!this.tradingService || !this.market || !this.currentRound) {
460
+ return {
461
+ success: false,
462
+ leg: 'leg2',
463
+ roundId: signal.roundId,
464
+ error: 'Trading service not available or no active round',
465
+ executionTimeMs: Date.now() - startTime,
466
+ };
467
+ }
468
+ try {
469
+ this.isExecuting = true;
470
+ // 计算拆分订单参数
471
+ const splitCount = Math.max(1, this.config.splitOrders);
472
+ const sharesPerOrder = signal.shares / splitCount;
473
+ const amountPerOrder = sharesPerOrder * signal.targetPrice;
474
+ // 检查每笔订单是否满足最小金额要求
475
+ if (amountPerOrder < 1) {
476
+ return {
477
+ success: false,
478
+ leg: 'leg2',
479
+ roundId: signal.roundId,
480
+ error: `Order amount ($${amountPerOrder.toFixed(2)}) is below Polymarket minimum ($1)`,
481
+ executionTimeMs: Date.now() - startTime,
482
+ };
483
+ }
484
+ let totalSharesFilled = 0;
485
+ let totalAmountSpent = 0;
486
+ let lastOrderId;
487
+ let failedOrders = 0;
488
+ // 执行多笔订单
489
+ for (let i = 0; i < splitCount; i++) {
490
+ const orderParams = {
491
+ tokenId: signal.tokenId,
492
+ side: 'BUY',
493
+ amount: amountPerOrder,
494
+ };
495
+ if (this.config.debug && splitCount > 1) {
496
+ this.log(`Leg2 order ${i + 1}/${splitCount}: ${sharesPerOrder.toFixed(2)} shares @ ${signal.targetPrice.toFixed(4)}`);
497
+ }
498
+ const result = await this.tradingService.createMarketOrder(orderParams);
499
+ if (result.success) {
500
+ totalSharesFilled += sharesPerOrder;
501
+ totalAmountSpent += amountPerOrder;
502
+ lastOrderId = result.orderId;
503
+ }
504
+ else {
505
+ failedOrders++;
506
+ this.log(`Leg2 order ${i + 1}/${splitCount} failed: ${result.errorMsg}`);
507
+ }
508
+ // 订单间隔
509
+ if (i < splitCount - 1 && this.config.orderIntervalMs > 0) {
510
+ await new Promise(resolve => setTimeout(resolve, this.config.orderIntervalMs));
511
+ }
512
+ }
513
+ // 至少有一笔成功
514
+ if (totalSharesFilled > 0) {
515
+ const avgPrice = totalAmountSpent / totalSharesFilled;
516
+ const leg1Price = this.currentRound.leg1?.price || 0;
517
+ const actualTotalCost = leg1Price + avgPrice;
518
+ // Record leg2 fill
519
+ this.currentRound.leg2 = {
520
+ side: signal.hedgeSide,
521
+ price: avgPrice,
522
+ shares: totalSharesFilled,
523
+ timestamp: Date.now(),
524
+ tokenId: signal.tokenId,
525
+ };
526
+ this.currentRound.phase = 'completed';
527
+ this.currentRound.totalCost = actualTotalCost;
528
+ this.currentRound.profit = 1 - actualTotalCost;
529
+ this.stats.leg2Filled++;
530
+ this.stats.roundsSuccessful++;
531
+ this.stats.totalProfit += this.currentRound.profit * totalSharesFilled;
532
+ this.stats.totalSpent += actualTotalCost * totalSharesFilled;
533
+ this.lastExecutionTime = Date.now();
534
+ if (splitCount > 1) {
535
+ this.log(`Leg2 completed: ${splitCount - failedOrders}/${splitCount} orders filled, total ${totalSharesFilled.toFixed(2)} shares`);
536
+ }
537
+ const roundResult = {
538
+ roundId: signal.roundId,
539
+ status: 'completed',
540
+ leg1: this.currentRound.leg1,
541
+ leg2: this.currentRound.leg2,
542
+ totalCost: this.currentRound.totalCost,
543
+ profit: this.currentRound.profit,
544
+ profitRate: calculateDipArbProfitRate(this.currentRound.totalCost),
545
+ merged: false,
546
+ };
547
+ this.emit('roundComplete', roundResult);
548
+ // Auto merge if enabled
549
+ if (this.config.autoMerge) {
550
+ const mergeResult = await this.merge();
551
+ roundResult.merged = mergeResult.success;
552
+ roundResult.mergeTxHash = mergeResult.txHash;
553
+ }
554
+ return {
555
+ success: true,
556
+ leg: 'leg2',
557
+ roundId: signal.roundId,
558
+ side: signal.hedgeSide,
559
+ price: avgPrice,
560
+ shares: totalSharesFilled,
561
+ orderId: lastOrderId,
562
+ executionTimeMs: Date.now() - startTime,
563
+ };
564
+ }
565
+ else {
566
+ return {
567
+ success: false,
568
+ leg: 'leg2',
569
+ roundId: signal.roundId,
570
+ error: 'All orders failed',
571
+ executionTimeMs: Date.now() - startTime,
572
+ };
573
+ }
574
+ }
575
+ catch (error) {
576
+ return {
577
+ success: false,
578
+ leg: 'leg2',
579
+ roundId: signal.roundId,
580
+ error: error instanceof Error ? error.message : String(error),
581
+ executionTimeMs: Date.now() - startTime,
582
+ };
583
+ }
584
+ finally {
585
+ this.isExecuting = false;
586
+ }
587
+ }
588
+ /**
589
+ * Merge YES + NO tokens to USDC
590
+ */
591
+ async merge() {
592
+ const startTime = Date.now();
593
+ const roundId = this.currentRound?.roundId || 'unknown';
594
+ if (!this.ctf || !this.market || !this.currentRound) {
595
+ return {
596
+ success: false,
597
+ leg: 'merge',
598
+ roundId,
599
+ error: 'CTF client not available or no completed round',
600
+ executionTimeMs: Date.now() - startTime,
601
+ };
602
+ }
603
+ const shares = Math.min(this.currentRound.leg1?.shares || 0, this.currentRound.leg2?.shares || 0);
604
+ if (shares <= 0) {
605
+ return {
606
+ success: false,
607
+ leg: 'merge',
608
+ roundId,
609
+ error: 'No shares to merge',
610
+ executionTimeMs: Date.now() - startTime,
611
+ };
612
+ }
613
+ try {
614
+ const result = await this.ctf.merge(this.market.conditionId, shares.toString());
615
+ return {
616
+ success: result.success,
617
+ leg: 'merge',
618
+ roundId,
619
+ shares,
620
+ txHash: result.txHash,
621
+ executionTimeMs: Date.now() - startTime,
622
+ };
623
+ }
624
+ catch (error) {
625
+ return {
626
+ success: false,
627
+ leg: 'merge',
628
+ roundId,
629
+ error: error instanceof Error ? error.message : String(error),
630
+ executionTimeMs: Date.now() - startTime,
631
+ };
632
+ }
633
+ }
634
+ // ===== Private: Event Handlers =====
635
+ handleOrderbookUpdate(book) {
636
+ if (!this.market)
637
+ return;
638
+ // Determine which side this update is for
639
+ const tokenId = book.tokenId;
640
+ const isUpToken = tokenId === this.market.upTokenId;
641
+ const isDownToken = tokenId === this.market.downTokenId;
642
+ // OrderbookLevel has price and size as numbers
643
+ if (isUpToken) {
644
+ this.upAsks = book.asks.map(l => ({ price: l.price, size: l.size }));
645
+ }
646
+ else if (isDownToken) {
647
+ this.downAsks = book.asks.map(l => ({ price: l.price, size: l.size }));
648
+ }
649
+ // Record price history for sliding window detection
650
+ this.recordPriceHistory();
651
+ // Check if we need to start a new round (async but fire-and-forget to not block orderbook updates)
652
+ this.checkAndStartNewRound().catch(err => {
653
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
654
+ });
655
+ // Detect signals
656
+ const signal = this.detectSignal();
657
+ if (signal) {
658
+ this.handleSignal(signal);
659
+ }
660
+ }
661
+ /**
662
+ * Record current prices to history buffer for sliding window detection
663
+ */
664
+ recordPriceHistory() {
665
+ const upAsk = this.upAsks[0]?.price ?? 0;
666
+ const downAsk = this.downAsks[0]?.price ?? 0;
667
+ // Only record if we have valid prices
668
+ if (upAsk <= 0 || downAsk <= 0)
669
+ return;
670
+ this.priceHistory.push({
671
+ timestamp: Date.now(),
672
+ upAsk,
673
+ downAsk,
674
+ });
675
+ // Trim history to max length
676
+ if (this.priceHistory.length > this.MAX_HISTORY_LENGTH) {
677
+ this.priceHistory = this.priceHistory.slice(-this.MAX_HISTORY_LENGTH);
678
+ }
679
+ }
680
+ /**
681
+ * Get price from N milliseconds ago for sliding window detection
682
+ *
683
+ * @param side - 'UP' or 'DOWN'
684
+ * @param msAgo - Milliseconds ago (e.g., 3000 for 3 seconds)
685
+ * @returns Price from that time, or null if not available
686
+ */
687
+ getPriceFromHistory(side, msAgo) {
688
+ const targetTime = Date.now() - msAgo;
689
+ // Find the closest price point at or before targetTime
690
+ for (let i = this.priceHistory.length - 1; i >= 0; i--) {
691
+ const entry = this.priceHistory[i];
692
+ if (entry.timestamp <= targetTime) {
693
+ return side === 'UP' ? entry.upAsk : entry.downAsk;
694
+ }
695
+ }
696
+ return null;
697
+ }
698
+ handleChainlinkPriceUpdate(price) {
699
+ if (!this.market)
700
+ return;
701
+ // Only handle updates for our underlying (symbol format: ETH/USD)
702
+ const expectedSymbol = `${this.market.underlying}/USD`;
703
+ if (price.symbol !== expectedSymbol)
704
+ return;
705
+ if (this.config.debug) {
706
+ this.log(`Chainlink price update: ${price.symbol} = $${price.price.toFixed(2)}`);
707
+ }
708
+ this.currentUnderlyingPrice = price.price;
709
+ // Emit price update event
710
+ if (this.currentRound) {
711
+ const event = {
712
+ underlying: this.market.underlying,
713
+ value: price.price,
714
+ priceToBeat: this.currentRound.priceToBeat,
715
+ changePercent: this.currentRound.priceToBeat > 0
716
+ ? ((price.price - this.currentRound.priceToBeat) / this.currentRound.priceToBeat) * 100
717
+ : 0,
718
+ };
719
+ this.emit('priceUpdate', event);
720
+ }
721
+ }
722
+ // ===== Private: Round Management =====
723
+ async checkAndStartNewRound() {
724
+ if (!this.market)
725
+ return;
726
+ // If no current round or current round is completed/expired, start new round
727
+ if (!this.currentRound || this.currentRound.phase === 'completed' || this.currentRound.phase === 'expired') {
728
+ // Check if market is still active
729
+ if (new Date() >= this.market.endTime) {
730
+ // Always log market end (not just in debug mode)
731
+ if (!this.currentRound) {
732
+ console.log('[DipArb] Market has ended before round could start');
733
+ }
734
+ return;
735
+ }
736
+ // Get current prices
737
+ const upPrice = this.upAsks[0]?.price ?? 0.5;
738
+ const downPrice = this.downAsks[0]?.price ?? 0.5;
739
+ // Use current underlying price as price to beat (or fallback to 0)
740
+ const priceToBeat = this.currentUnderlyingPrice || 0;
741
+ // Create new round
742
+ const roundId = `${this.market.slug}-${Date.now()}`;
743
+ this.currentRound = createDipArbRoundState(roundId, priceToBeat, upPrice, downPrice, this.market.durationMinutes);
744
+ // Clear price history for new round - we only want to detect instant drops within this round
745
+ this.priceHistory = [];
746
+ // Reset signal state for new round
747
+ this.leg1SignalEmitted = false;
748
+ this.stats.roundsMonitored++;
749
+ const event = {
750
+ roundId,
751
+ priceToBeat,
752
+ upOpen: upPrice,
753
+ downOpen: downPrice,
754
+ startTime: this.currentRound.startTime,
755
+ endTime: this.currentRound.endTime,
756
+ };
757
+ this.emit('newRound', event);
758
+ this.log(`New round: ${roundId}, Price to Beat: ${priceToBeat.toFixed(2)}`);
759
+ }
760
+ // Check for round expiration - exit Leg1 if Leg2 times out
761
+ if (this.currentRound && this.currentRound.phase === 'leg1_filled') {
762
+ const elapsed = (Date.now() - (this.currentRound.leg1?.timestamp || this.currentRound.startTime)) / 1000;
763
+ if (elapsed > this.config.leg2TimeoutSeconds) {
764
+ // ✅ FIX: Exit Leg1 position to avoid unhedged exposure
765
+ this.log(`⚠️ Leg2 timeout (${elapsed.toFixed(0)}s > ${this.config.leg2TimeoutSeconds}s), exiting Leg1 position...`);
766
+ // Try to sell Leg1 position
767
+ const exitResult = await this.emergencyExitLeg1();
768
+ this.currentRound.phase = 'expired';
769
+ this.stats.roundsExpired++;
770
+ this.stats.roundsCompleted++;
771
+ const result = {
772
+ roundId: this.currentRound.roundId,
773
+ status: 'expired',
774
+ leg1: this.currentRound.leg1,
775
+ merged: false,
776
+ exitResult, // Include exit result for tracking
777
+ };
778
+ this.emit('roundComplete', result);
779
+ this.log(`Round expired: ${this.currentRound.roundId} | Exit: ${exitResult?.success ? 'SUCCESS' : 'FAILED'}`);
780
+ }
781
+ }
782
+ }
783
+ /**
784
+ * Emergency exit Leg1 position when Leg2 times out
785
+ * Sells the Leg1 tokens at market price to avoid unhedged exposure
786
+ */
787
+ async emergencyExitLeg1() {
788
+ if (!this.tradingService || !this.market || !this.currentRound?.leg1) {
789
+ this.log('Cannot exit Leg1: no trading service or position');
790
+ return null;
791
+ }
792
+ const leg1 = this.currentRound.leg1;
793
+ const startTime = Date.now();
794
+ try {
795
+ this.log(`Selling ${leg1.shares} ${leg1.side} tokens...`);
796
+ // Get current price for the token
797
+ const currentPrice = leg1.side === 'UP'
798
+ ? (this.upAsks[0]?.price ?? 0.5)
799
+ : (this.downAsks[0]?.price ?? 0.5);
800
+ // Market sell the position
801
+ const result = await this.tradingService.createMarketOrder({
802
+ tokenId: leg1.tokenId,
803
+ side: 'SELL',
804
+ amount: leg1.shares * currentPrice, // Sell all shares
805
+ });
806
+ if (result.success) {
807
+ const soldPrice = currentPrice; // Approximate
808
+ const loss = (leg1.price - soldPrice) * leg1.shares;
809
+ this.log(`✅ Leg1 exit successful: sold ${leg1.shares}x ${leg1.side} @ ~${soldPrice.toFixed(4)} | Loss: $${loss.toFixed(2)}`);
810
+ // Update stats with the loss
811
+ this.stats.totalProfit -= Math.abs(loss);
812
+ return {
813
+ success: true,
814
+ leg: 'exit',
815
+ roundId: this.currentRound.roundId,
816
+ side: leg1.side,
817
+ price: soldPrice,
818
+ shares: leg1.shares,
819
+ orderId: result.orderId,
820
+ executionTimeMs: Date.now() - startTime,
821
+ };
822
+ }
823
+ else {
824
+ this.log(`❌ Leg1 exit failed: ${result.errorMsg}`);
825
+ return {
826
+ success: false,
827
+ leg: 'exit',
828
+ roundId: this.currentRound.roundId,
829
+ error: result.errorMsg,
830
+ executionTimeMs: Date.now() - startTime,
831
+ };
832
+ }
833
+ }
834
+ catch (error) {
835
+ this.log(`❌ Leg1 exit error: ${error instanceof Error ? error.message : String(error)}`);
836
+ return {
837
+ success: false,
838
+ leg: 'exit',
839
+ roundId: this.currentRound.roundId,
840
+ error: error instanceof Error ? error.message : String(error),
841
+ executionTimeMs: Date.now() - startTime,
842
+ };
843
+ }
844
+ }
845
+ // ===== Private: Signal Detection =====
846
+ detectSignal() {
847
+ if (!this.currentRound || !this.market)
848
+ return null;
849
+ // Check based on current phase
850
+ if (this.currentRound.phase === 'waiting') {
851
+ return this.detectLeg1Signal();
852
+ }
853
+ else if (this.currentRound.phase === 'leg1_filled') {
854
+ return this.detectLeg2Signal();
855
+ }
856
+ return null;
857
+ }
858
+ detectLeg1Signal() {
859
+ if (!this.currentRound || !this.market)
860
+ return null;
861
+ // Check if within trading window (轮次开始后的交易窗口)
862
+ const elapsed = (Date.now() - this.currentRound.startTime) / 60000;
863
+ if (elapsed > this.config.windowMinutes) {
864
+ return null;
865
+ }
866
+ const upPrice = this.upAsks[0]?.price ?? 1;
867
+ const downPrice = this.downAsks[0]?.price ?? 1;
868
+ const { openPrices } = this.currentRound;
869
+ // Skip if no valid prices
870
+ if (upPrice >= 1 || downPrice >= 1 || openPrices.up <= 0 || openPrices.down <= 0) {
871
+ return null;
872
+ }
873
+ // ========================================
874
+ // Pattern 1: Instant Dip Detection (核心策略)
875
+ // ========================================
876
+ // 检测 slidingWindowMs (默认 3 秒) 内的瞬时暴跌
877
+ // 这是策略的核心!我们捕捉的是"情绪性暴跌",不是趋势
878
+ const upPriceAgo = this.getPriceFromHistory('UP', this.config.slidingWindowMs);
879
+ const downPriceAgo = this.getPriceFromHistory('DOWN', this.config.slidingWindowMs);
880
+ // UP instant dip: 3秒内暴跌 >= dipThreshold
881
+ if (upPriceAgo !== null && upPriceAgo > 0) {
882
+ const upInstantDrop = (upPriceAgo - upPrice) / upPriceAgo;
883
+ if (upInstantDrop >= this.config.dipThreshold) {
884
+ if (this.config.debug) {
885
+ this.log(`⚡ Instant DIP detected! UP: ${upPriceAgo.toFixed(4)} → ${upPrice.toFixed(4)} = -${(upInstantDrop * 100).toFixed(1)}% in ${this.config.slidingWindowMs}ms`);
886
+ }
887
+ const signal = this.createLeg1Signal('UP', upPrice, downPrice, 'dip', upInstantDrop, upPriceAgo);
888
+ if (signal && this.validateSignalProfitability(signal)) {
889
+ return signal;
890
+ }
891
+ }
892
+ }
893
+ // DOWN instant dip: 3秒内暴跌 >= dipThreshold
894
+ if (downPriceAgo !== null && downPriceAgo > 0) {
895
+ const downInstantDrop = (downPriceAgo - downPrice) / downPriceAgo;
896
+ if (downInstantDrop >= this.config.dipThreshold) {
897
+ if (this.config.debug) {
898
+ this.log(`⚡ Instant DIP detected! DOWN: ${downPriceAgo.toFixed(4)} → ${downPrice.toFixed(4)} = -${(downInstantDrop * 100).toFixed(1)}% in ${this.config.slidingWindowMs}ms`);
899
+ }
900
+ const signal = this.createLeg1Signal('DOWN', downPrice, upPrice, 'dip', downInstantDrop, downPriceAgo);
901
+ if (signal && this.validateSignalProfitability(signal)) {
902
+ return signal;
903
+ }
904
+ }
905
+ }
906
+ // ========================================
907
+ // Pattern 2: Surge Detection (if enabled)
908
+ // ========================================
909
+ // 暴涨检测:当 token 价格暴涨时,买入对手 token
910
+ if (this.config.enableSurge && upPriceAgo !== null && downPriceAgo !== null) {
911
+ // UP surged in sliding window, buy DOWN
912
+ if (upPriceAgo > 0) {
913
+ const upSurge = (upPrice - upPriceAgo) / upPriceAgo;
914
+ if (upSurge >= this.config.surgeThreshold) {
915
+ if (this.config.debug) {
916
+ this.log(`⚡ Instant SURGE detected! UP: ${upPriceAgo.toFixed(4)} → ${upPrice.toFixed(4)} = +${(upSurge * 100).toFixed(1)}% in ${this.config.slidingWindowMs}ms`);
917
+ }
918
+ // 买入 DOWN,参考价格是 DOWN 的历史价格
919
+ const signal = this.createLeg1Signal('DOWN', downPrice, upPrice, 'surge', upSurge, downPriceAgo);
920
+ if (signal && this.validateSignalProfitability(signal)) {
921
+ return signal;
922
+ }
923
+ }
924
+ }
925
+ // DOWN surged in sliding window, buy UP
926
+ if (downPriceAgo > 0) {
927
+ const downSurge = (downPrice - downPriceAgo) / downPriceAgo;
928
+ if (downSurge >= this.config.surgeThreshold) {
929
+ if (this.config.debug) {
930
+ this.log(`⚡ Instant SURGE detected! DOWN: ${downPriceAgo.toFixed(4)} → ${downPrice.toFixed(4)} = +${(downSurge * 100).toFixed(1)}% in ${this.config.slidingWindowMs}ms`);
931
+ }
932
+ // 买入 UP,参考价格是 UP 的历史价格
933
+ const signal = this.createLeg1Signal('UP', upPrice, downPrice, 'surge', downSurge, upPriceAgo);
934
+ if (signal && this.validateSignalProfitability(signal)) {
935
+ return signal;
936
+ }
937
+ }
938
+ }
939
+ }
940
+ // ========================================
941
+ // Pattern 3: Mispricing Detection
942
+ // ========================================
943
+ // 定价偏差:基于底层资产价格估计胜率,检测错误定价
944
+ if (this.currentRound.priceToBeat > 0 && this.currentUnderlyingPrice > 0) {
945
+ const estimatedWinRate = estimateUpWinRate(this.currentUnderlyingPrice, this.currentRound.priceToBeat);
946
+ const upMispricing = detectMispricing(upPrice, estimatedWinRate);
947
+ const downMispricing = detectMispricing(downPrice, 1 - estimatedWinRate);
948
+ // UP is underpriced
949
+ if (upMispricing >= this.config.dipThreshold) {
950
+ const signal = this.createLeg1Signal('UP', upPrice, downPrice, 'mispricing', upMispricing);
951
+ if (signal && this.validateSignalProfitability(signal)) {
952
+ return signal;
953
+ }
954
+ }
955
+ // DOWN is underpriced
956
+ if (downMispricing >= this.config.dipThreshold) {
957
+ const signal = this.createLeg1Signal('DOWN', downPrice, upPrice, 'mispricing', downMispricing);
958
+ if (signal && this.validateSignalProfitability(signal)) {
959
+ return signal;
960
+ }
961
+ }
962
+ }
963
+ return null;
964
+ }
965
+ createLeg1Signal(side, price, oppositeAsk, source, dropPercent, referencePrice // 用于 dip/surge: 滑动窗口前的价格
966
+ ) {
967
+ if (!this.currentRound || !this.market)
968
+ return null;
969
+ const targetPrice = price * (1 + this.config.maxSlippage);
970
+ const estimatedTotalCost = targetPrice + oppositeAsk;
971
+ const estimatedProfitRate = calculateDipArbProfitRate(estimatedTotalCost);
972
+ // openPrice: 对于 dip/surge 信号,使用滑动窗口参考价格;否则使用轮次开盘价
973
+ const openPrice = referencePrice ??
974
+ (side === 'UP' ? this.currentRound.openPrices.up : this.currentRound.openPrices.down);
975
+ const signal = {
976
+ type: 'leg1',
977
+ roundId: this.currentRound.roundId,
978
+ dipSide: side,
979
+ currentPrice: price,
980
+ openPrice, // 参考价格(3秒前的价格或轮次开盘价)
981
+ dropPercent,
982
+ targetPrice,
983
+ shares: this.config.shares,
984
+ tokenId: side === 'UP' ? this.market.upTokenId : this.market.downTokenId,
985
+ oppositeAsk,
986
+ estimatedTotalCost,
987
+ estimatedProfitRate,
988
+ source,
989
+ };
990
+ // Add BTC info if available
991
+ if (this.currentRound.priceToBeat > 0 && this.currentUnderlyingPrice > 0) {
992
+ const btcChangePercent = ((this.currentUnderlyingPrice - this.currentRound.priceToBeat) / this.currentRound.priceToBeat) * 100;
993
+ signal.btcInfo = {
994
+ btcPrice: this.currentUnderlyingPrice,
995
+ priceToBeat: this.currentRound.priceToBeat,
996
+ btcChangePercent,
997
+ estimatedWinRate: estimateUpWinRate(this.currentUnderlyingPrice, this.currentRound.priceToBeat),
998
+ };
999
+ }
1000
+ return signal;
1001
+ }
1002
+ detectLeg2Signal() {
1003
+ if (!this.currentRound || !this.market || !this.currentRound.leg1)
1004
+ return null;
1005
+ const leg1 = this.currentRound.leg1;
1006
+ const hedgeSide = leg1.side === 'UP' ? 'DOWN' : 'UP';
1007
+ const currentPrice = hedgeSide === 'UP' ? (this.upAsks[0]?.price ?? 1) : (this.downAsks[0]?.price ?? 1);
1008
+ if (currentPrice >= 1)
1009
+ return null;
1010
+ const targetPrice = currentPrice * (1 + this.config.maxSlippage);
1011
+ const totalCost = leg1.price + targetPrice;
1012
+ // Check if profitable - 只用 sumTarget 控制
1013
+ if (totalCost > this.config.sumTarget) {
1014
+ // 每 5 秒输出一次等待日志,避免刷屏
1015
+ if (this.config.debug && Date.now() % 5000 < 100) {
1016
+ const profitRate = calculateDipArbProfitRate(totalCost);
1017
+ this.log(`⏳ Waiting Leg2: ${hedgeSide} @ ${currentPrice.toFixed(4)}, cost ${totalCost.toFixed(4)} > ${this.config.sumTarget}, profit ${(profitRate * 100).toFixed(1)}%`);
1018
+ }
1019
+ return null;
1020
+ }
1021
+ const expectedProfitRate = calculateDipArbProfitRate(totalCost);
1022
+ if (this.config.debug) {
1023
+ this.log(`✅ Leg2 signal found! ${hedgeSide} @ ${currentPrice.toFixed(4)}, totalCost ${totalCost.toFixed(4)}, profit ${(expectedProfitRate * 100).toFixed(2)}%`);
1024
+ }
1025
+ // ✅ FIX: Use leg1.shares instead of config.shares to ensure balanced hedge
1026
+ // This is critical - Leg2 must buy exactly the same shares as Leg1 to create a perfect hedge
1027
+ return {
1028
+ type: 'leg2',
1029
+ roundId: this.currentRound.roundId,
1030
+ hedgeSide,
1031
+ leg1,
1032
+ currentPrice,
1033
+ targetPrice,
1034
+ totalCost,
1035
+ expectedProfitRate,
1036
+ shares: leg1.shares, // Must match Leg1 to ensure balanced hedge
1037
+ tokenId: hedgeSide === 'UP' ? this.market.upTokenId : this.market.downTokenId,
1038
+ };
1039
+ }
1040
+ validateSignalProfitability(signal) {
1041
+ // Leg1 验证:只检查跌幅是否足够大
1042
+ // 不在 Leg1 阶段检查 sumTarget,因为:
1043
+ // 1. Leg1 的目的是抄底,买入暴跌的一侧
1044
+ // 2. Leg2 会等待对侧价格下降后再买入
1045
+ // 3. sumTarget 应该在 Leg2 阶段检查
1046
+ // 只做基本验证:确保价格合理
1047
+ if (signal.currentPrice <= 0 || signal.currentPrice >= 1) {
1048
+ if (this.config.debug) {
1049
+ this.log(`❌ Signal rejected: invalid price ${signal.currentPrice.toFixed(4)}`);
1050
+ }
1051
+ return false;
1052
+ }
1053
+ // 确保跌幅达到阈值(这个已经在 detectLeg1Signal 中检查过,这里再确认一下)
1054
+ if (signal.dropPercent < this.config.dipThreshold) {
1055
+ if (this.config.debug) {
1056
+ this.log(`❌ Signal rejected: drop ${(signal.dropPercent * 100).toFixed(1)}% < threshold ${(this.config.dipThreshold * 100).toFixed(1)}%`);
1057
+ }
1058
+ return false;
1059
+ }
1060
+ if (this.config.debug) {
1061
+ this.log(`✅ Leg1 signal validated: ${signal.dipSide} @ ${signal.currentPrice.toFixed(4)}, drop ${(signal.dropPercent * 100).toFixed(1)}%`);
1062
+ this.log(` (Leg2 will check sumTarget when opposite price drops)`);
1063
+ }
1064
+ return true;
1065
+ }
1066
+ // ===== Private: Signal Handling =====
1067
+ async handleSignal(signal) {
1068
+ // Check if we can execute before emitting signal
1069
+ // This prevents logging signals that won't be executed
1070
+ if (!this.config.autoExecute) {
1071
+ // Manual mode: always emit signal for user to decide
1072
+ this.stats.signalsDetected++;
1073
+ this.emit('signal', signal);
1074
+ if (this.config.debug) {
1075
+ if (isDipArbLeg1Signal(signal)) {
1076
+ this.log(`Signal: Leg1 ${signal.dipSide} @ ${signal.currentPrice.toFixed(4)} (${signal.source})`);
1077
+ }
1078
+ else {
1079
+ this.log(`Signal: Leg2 ${signal.hedgeSide} @ ${signal.currentPrice.toFixed(4)}`);
1080
+ }
1081
+ }
1082
+ return;
1083
+ }
1084
+ // Auto-execute mode: only emit and log if we will actually execute
1085
+ if (this.isExecuting) {
1086
+ // Skip - already executing another order
1087
+ if (this.config.debug) {
1088
+ this.log(`Signal skipped (executing): ${isDipArbLeg1Signal(signal) ? 'Leg1' : 'Leg2'}`);
1089
+ }
1090
+ return;
1091
+ }
1092
+ const now = Date.now();
1093
+ if (now - this.lastExecutionTime < this.config.executionCooldown) {
1094
+ // Skip - within cooldown period
1095
+ if (this.config.debug) {
1096
+ const remaining = this.config.executionCooldown - (now - this.lastExecutionTime);
1097
+ this.log(`Signal skipped (cooldown ${remaining}ms): ${isDipArbLeg1Signal(signal) ? 'Leg1' : 'Leg2'}`);
1098
+ }
1099
+ return;
1100
+ }
1101
+ // Will execute - now emit signal and log
1102
+ this.stats.signalsDetected++;
1103
+ this.emit('signal', signal);
1104
+ if (this.config.debug) {
1105
+ if (isDipArbLeg1Signal(signal)) {
1106
+ this.log(`Signal: Leg1 ${signal.dipSide} @ ${signal.currentPrice.toFixed(4)} (${signal.source})`);
1107
+ }
1108
+ else {
1109
+ this.log(`Signal: Leg2 ${signal.hedgeSide} @ ${signal.currentPrice.toFixed(4)}`);
1110
+ }
1111
+ }
1112
+ // Execute
1113
+ let result;
1114
+ if (isDipArbLeg1Signal(signal)) {
1115
+ result = await this.executeLeg1(signal);
1116
+ }
1117
+ else {
1118
+ result = await this.executeLeg2(signal);
1119
+ }
1120
+ this.emit('execution', result);
1121
+ }
1122
+ // ===== Public API: Auto-Rotate =====
1123
+ /**
1124
+ * Configure and enable auto-rotate
1125
+ *
1126
+ * Auto-rotate 会自动:
1127
+ * 1. 监控当前市场到期时间
1128
+ * 2. 在市场结束前预加载下一个市场
1129
+ * 3. 市场结束时自动结算(redeem 或 sell)
1130
+ * 4. 无缝切换到下一个 15m 市场
1131
+ *
1132
+ * @example
1133
+ * ```typescript
1134
+ * sdk.dipArb.enableAutoRotate({
1135
+ * underlyings: ['BTC', 'ETH'],
1136
+ * duration: '15m',
1137
+ * autoSettle: true,
1138
+ * settleStrategy: 'redeem',
1139
+ * });
1140
+ * ```
1141
+ */
1142
+ enableAutoRotate(config = {}) {
1143
+ this.autoRotateConfig = {
1144
+ ...this.autoRotateConfig,
1145
+ ...config,
1146
+ enabled: true,
1147
+ };
1148
+ this.log(`Auto-rotate enabled: ${JSON.stringify(this.autoRotateConfig)}`);
1149
+ this.startRotateCheck();
1150
+ // Start background redemption check if using redeem strategy
1151
+ if (this.autoRotateConfig.settleStrategy === 'redeem') {
1152
+ this.startRedeemCheck();
1153
+ }
1154
+ }
1155
+ /**
1156
+ * Disable auto-rotate
1157
+ */
1158
+ disableAutoRotate() {
1159
+ this.autoRotateConfig.enabled = false;
1160
+ this.stopRotateCheck();
1161
+ this.stopRedeemCheck();
1162
+ this.log('Auto-rotate disabled');
1163
+ // Warn if there are pending redemptions
1164
+ if (this.pendingRedemptions.length > 0) {
1165
+ this.log(`Warning: ${this.pendingRedemptions.length} pending redemptions will not be processed`);
1166
+ }
1167
+ }
1168
+ /**
1169
+ * Get auto-rotate configuration
1170
+ */
1171
+ getAutoRotateConfig() {
1172
+ return { ...this.autoRotateConfig };
1173
+ }
1174
+ /**
1175
+ * Manually settle current position
1176
+ *
1177
+ * 结算策略:
1178
+ * - 'redeem': 等待市场结算后 redeem(需要等待结算完成)
1179
+ * - 'sell': 直接卖出 token(更快但可能有滑点)
1180
+ */
1181
+ async settle(strategy = 'redeem') {
1182
+ const startTime = Date.now();
1183
+ if (!this.market || !this.currentRound) {
1184
+ return {
1185
+ success: false,
1186
+ strategy,
1187
+ error: 'No active market or round',
1188
+ executionTimeMs: Date.now() - startTime,
1189
+ };
1190
+ }
1191
+ try {
1192
+ if (strategy === 'redeem') {
1193
+ return await this.settleByRedeem();
1194
+ }
1195
+ else {
1196
+ return await this.settleBySell();
1197
+ }
1198
+ }
1199
+ catch (error) {
1200
+ return {
1201
+ success: false,
1202
+ strategy,
1203
+ error: error instanceof Error ? error.message : String(error),
1204
+ executionTimeMs: Date.now() - startTime,
1205
+ };
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Manually rotate to next market
1210
+ */
1211
+ async rotateToNextMarket() {
1212
+ if (!this.autoRotateConfig.enabled) {
1213
+ this.log('Auto-rotate not enabled');
1214
+ return null;
1215
+ }
1216
+ // Find next market
1217
+ const nextMarket = await this.findNextMarket();
1218
+ if (!nextMarket) {
1219
+ this.log('No suitable next market found');
1220
+ return null;
1221
+ }
1222
+ // Stop current monitoring
1223
+ await this.stop();
1224
+ // Start new market
1225
+ await this.start(nextMarket);
1226
+ const event = {
1227
+ previousMarket: this.market?.conditionId,
1228
+ newMarket: nextMarket.conditionId,
1229
+ reason: 'manual',
1230
+ timestamp: Date.now(),
1231
+ };
1232
+ this.emit('rotate', event);
1233
+ return nextMarket;
1234
+ }
1235
+ // ===== Private: Auto-Rotate Implementation =====
1236
+ startRotateCheck() {
1237
+ if (this.rotateCheckInterval) {
1238
+ clearInterval(this.rotateCheckInterval);
1239
+ }
1240
+ // Check every 30 seconds
1241
+ this.rotateCheckInterval = setInterval(() => {
1242
+ this.checkRotation();
1243
+ }, 30000);
1244
+ // Also check immediately
1245
+ this.checkRotation();
1246
+ }
1247
+ stopRotateCheck() {
1248
+ if (this.rotateCheckInterval) {
1249
+ clearInterval(this.rotateCheckInterval);
1250
+ this.rotateCheckInterval = null;
1251
+ }
1252
+ }
1253
+ // ===== Private: Pending Redemption Processing =====
1254
+ startRedeemCheck() {
1255
+ if (this.redeemCheckInterval) {
1256
+ clearInterval(this.redeemCheckInterval);
1257
+ }
1258
+ const intervalMs = (this.autoRotateConfig.redeemRetryIntervalSeconds || 30) * 1000;
1259
+ // Check every 30 seconds (configurable)
1260
+ this.redeemCheckInterval = setInterval(() => {
1261
+ this.processPendingRedemptions();
1262
+ }, intervalMs);
1263
+ this.log(`Redeem check started (interval: ${intervalMs / 1000}s)`);
1264
+ }
1265
+ stopRedeemCheck() {
1266
+ if (this.redeemCheckInterval) {
1267
+ clearInterval(this.redeemCheckInterval);
1268
+ this.redeemCheckInterval = null;
1269
+ }
1270
+ }
1271
+ /**
1272
+ * Add a position to pending redemption queue
1273
+ */
1274
+ addPendingRedemption(market, round) {
1275
+ const pending = {
1276
+ market,
1277
+ round,
1278
+ marketEndTime: market.endTime.getTime(),
1279
+ addedAt: Date.now(),
1280
+ retryCount: 0,
1281
+ };
1282
+ this.pendingRedemptions.push(pending);
1283
+ this.log(`Added pending redemption: ${market.slug} (queue size: ${this.pendingRedemptions.length})`);
1284
+ }
1285
+ /**
1286
+ * Process all pending redemptions
1287
+ * Called periodically by redeemCheckInterval
1288
+ */
1289
+ async processPendingRedemptions() {
1290
+ if (this.pendingRedemptions.length === 0) {
1291
+ return;
1292
+ }
1293
+ const now = Date.now();
1294
+ const waitMs = (this.autoRotateConfig.redeemWaitMinutes || 5) * 60 * 1000;
1295
+ for (let i = this.pendingRedemptions.length - 1; i >= 0; i--) {
1296
+ const pending = this.pendingRedemptions[i];
1297
+ const timeSinceEnd = now - pending.marketEndTime;
1298
+ // Skip if not enough time has passed since market end
1299
+ if (timeSinceEnd < waitMs) {
1300
+ const waitLeft = Math.round((waitMs - timeSinceEnd) / 1000);
1301
+ if (this.config.debug) {
1302
+ this.log(`Pending redemption ${pending.market.slug}: waiting ${waitLeft}s more for resolution`);
1303
+ }
1304
+ continue;
1305
+ }
1306
+ // Try to redeem
1307
+ pending.retryCount++;
1308
+ pending.lastRetryAt = now;
1309
+ try {
1310
+ if (!this.ctf) {
1311
+ this.log(`Cannot redeem ${pending.market.slug}: CTF client not available`);
1312
+ continue;
1313
+ }
1314
+ // Check if market is resolved
1315
+ const resolution = await this.ctf.getMarketResolution(pending.market.conditionId);
1316
+ if (!resolution.isResolved) {
1317
+ this.log(`Pending redemption ${pending.market.slug}: market not yet resolved (retry ${pending.retryCount})`);
1318
+ // Give up after too many retries (10 minutes of trying)
1319
+ if (pending.retryCount > 20) {
1320
+ this.log(`Giving up on redemption ${pending.market.slug}: too many retries`);
1321
+ this.pendingRedemptions.splice(i, 1);
1322
+ this.emit('settled', {
1323
+ success: false,
1324
+ strategy: 'redeem',
1325
+ market: pending.market,
1326
+ error: 'Market not resolved after max retries',
1327
+ executionTimeMs: 0,
1328
+ });
1329
+ }
1330
+ continue;
1331
+ }
1332
+ // Market is resolved, try to redeem using Polymarket token IDs
1333
+ this.log(`Redeeming ${pending.market.slug}...`);
1334
+ const tokenIds = {
1335
+ yesTokenId: pending.market.upTokenId,
1336
+ noTokenId: pending.market.downTokenId,
1337
+ };
1338
+ const result = await this.ctf.redeemByTokenIds(pending.market.conditionId, tokenIds);
1339
+ // Remove from queue
1340
+ this.pendingRedemptions.splice(i, 1);
1341
+ const settleResult = {
1342
+ success: result.success,
1343
+ strategy: 'redeem',
1344
+ market: pending.market,
1345
+ txHash: result.txHash,
1346
+ amountReceived: result.usdcReceived ? parseFloat(result.usdcReceived) : undefined,
1347
+ executionTimeMs: 0,
1348
+ };
1349
+ this.emit('settled', settleResult);
1350
+ this.log(`Redemption successful: ${pending.market.slug} | Amount: $${settleResult.amountReceived?.toFixed(2) || 'N/A'}`);
1351
+ // Update stats
1352
+ if (settleResult.amountReceived) {
1353
+ this.stats.totalProfit += settleResult.amountReceived;
1354
+ }
1355
+ }
1356
+ catch (error) {
1357
+ this.log(`Redemption error for ${pending.market.slug}: ${error instanceof Error ? error.message : String(error)}`);
1358
+ // Give up after too many retries
1359
+ if (pending.retryCount > 20) {
1360
+ this.log(`Giving up on redemption ${pending.market.slug}: error after max retries`);
1361
+ this.pendingRedemptions.splice(i, 1);
1362
+ this.emit('settled', {
1363
+ success: false,
1364
+ strategy: 'redeem',
1365
+ market: pending.market,
1366
+ error: error instanceof Error ? error.message : String(error),
1367
+ executionTimeMs: 0,
1368
+ });
1369
+ }
1370
+ }
1371
+ }
1372
+ }
1373
+ /**
1374
+ * Get pending redemptions (for debugging/monitoring)
1375
+ */
1376
+ getPendingRedemptions() {
1377
+ return [...this.pendingRedemptions];
1378
+ }
1379
+ async checkRotation() {
1380
+ if (!this.autoRotateConfig.enabled || !this.market) {
1381
+ if (this.config.debug) {
1382
+ this.log(`checkRotation: skipped (enabled=${this.autoRotateConfig.enabled}, market=${!!this.market})`);
1383
+ }
1384
+ return;
1385
+ }
1386
+ const now = Date.now();
1387
+ const endTime = this.market.endTime.getTime();
1388
+ const timeUntilEnd = endTime - now;
1389
+ const preloadMs = (this.autoRotateConfig.preloadMinutes || 2) * 60 * 1000;
1390
+ if (this.config.debug) {
1391
+ const timeLeftSec = Math.round(timeUntilEnd / 1000);
1392
+ this.log(`checkRotation: timeUntilEnd=${timeLeftSec}s, preloadMs=${preloadMs / 1000}s, nextMarket=${this.nextMarket?.slug || 'none'}`);
1393
+ }
1394
+ // Preload next market when close to end
1395
+ if (timeUntilEnd <= preloadMs && !this.nextMarket) {
1396
+ this.log('Preloading next market...');
1397
+ this.nextMarket = await this.findNextMarket();
1398
+ if (this.nextMarket) {
1399
+ this.log(`Next market ready: ${this.nextMarket.slug}`);
1400
+ }
1401
+ else {
1402
+ this.log('No next market found during preload');
1403
+ }
1404
+ }
1405
+ // Market ended - settle and rotate
1406
+ if (timeUntilEnd <= 0) {
1407
+ this.log(`Market ended ${Math.round(-timeUntilEnd / 1000)}s ago, initiating rotation...`);
1408
+ // Settle if configured and has position
1409
+ if (this.autoRotateConfig.autoSettle && this.currentRound?.leg1) {
1410
+ const strategy = this.autoRotateConfig.settleStrategy || 'redeem';
1411
+ if (strategy === 'redeem') {
1412
+ // For redeem strategy, add to pending queue (will be processed after 5 min wait)
1413
+ this.addPendingRedemption(this.market, this.currentRound);
1414
+ this.log(`Position added to pending redemption queue (will redeem after ${this.autoRotateConfig.redeemWaitMinutes || 5}min)`);
1415
+ }
1416
+ else {
1417
+ // For sell strategy, execute immediately
1418
+ const settleResult = await this.settle('sell');
1419
+ this.emit('settled', settleResult);
1420
+ }
1421
+ }
1422
+ // Rotate to next market
1423
+ if (this.nextMarket) {
1424
+ const previousMarket = this.market;
1425
+ const newMarket = this.nextMarket;
1426
+ this.nextMarket = null;
1427
+ // Stop current market (this clears the rotate check interval)
1428
+ await this.stop();
1429
+ // Start new market
1430
+ await this.start(newMarket);
1431
+ // Restart the rotate check interval for the new market
1432
+ this.startRotateCheck();
1433
+ const event = {
1434
+ previousMarket: previousMarket.conditionId,
1435
+ newMarket: newMarket.conditionId,
1436
+ reason: 'marketEnded',
1437
+ timestamp: Date.now(),
1438
+ };
1439
+ this.emit('rotate', event);
1440
+ }
1441
+ else {
1442
+ // Try to find a market
1443
+ this.log('No preloaded market, searching...');
1444
+ const newMarket = await this.findNextMarket();
1445
+ if (newMarket) {
1446
+ const previousMarket = this.market;
1447
+ // Stop current market (this clears the rotate check interval)
1448
+ await this.stop();
1449
+ // Start new market
1450
+ await this.start(newMarket);
1451
+ // Restart the rotate check interval for the new market
1452
+ this.startRotateCheck();
1453
+ const event = {
1454
+ previousMarket: previousMarket.conditionId,
1455
+ newMarket: newMarket.conditionId,
1456
+ reason: 'marketEnded',
1457
+ timestamp: Date.now(),
1458
+ };
1459
+ this.emit('rotate', event);
1460
+ }
1461
+ else {
1462
+ this.log('No next market available, stopping...');
1463
+ await this.stop();
1464
+ }
1465
+ }
1466
+ }
1467
+ }
1468
+ async findNextMarket() {
1469
+ const markets = await this.scanUpcomingMarkets({
1470
+ coin: this.autoRotateConfig.underlyings.length === 1
1471
+ ? this.autoRotateConfig.underlyings[0]
1472
+ : 'all',
1473
+ duration: this.autoRotateConfig.duration,
1474
+ minMinutesUntilEnd: 5,
1475
+ maxMinutesUntilEnd: 30,
1476
+ limit: 10,
1477
+ });
1478
+ // Filter to configured underlyings
1479
+ const filtered = markets.filter(m => this.autoRotateConfig.underlyings.includes(m.underlying));
1480
+ // Exclude current market
1481
+ const available = filtered.filter(m => m.conditionId !== this.market?.conditionId);
1482
+ // Return the soonest one
1483
+ return available.length > 0 ? available[0] : null;
1484
+ }
1485
+ async settleByRedeem() {
1486
+ const startTime = Date.now();
1487
+ if (!this.ctf || !this.market) {
1488
+ return {
1489
+ success: false,
1490
+ strategy: 'redeem',
1491
+ error: 'CTF client or market not available',
1492
+ executionTimeMs: Date.now() - startTime,
1493
+ };
1494
+ }
1495
+ try {
1496
+ // Check market resolution first
1497
+ const resolution = await this.ctf.getMarketResolution(this.market.conditionId);
1498
+ if (!resolution.isResolved) {
1499
+ return {
1500
+ success: false,
1501
+ strategy: 'redeem',
1502
+ error: 'Market not yet resolved',
1503
+ executionTimeMs: Date.now() - startTime,
1504
+ };
1505
+ }
1506
+ // Redeem winning tokens using Polymarket token IDs
1507
+ const tokenIds = {
1508
+ yesTokenId: this.market.upTokenId,
1509
+ noTokenId: this.market.downTokenId,
1510
+ };
1511
+ const result = await this.ctf.redeemByTokenIds(this.market.conditionId, tokenIds);
1512
+ return {
1513
+ success: result.success,
1514
+ strategy: 'redeem',
1515
+ txHash: result.txHash,
1516
+ amountReceived: result.usdcReceived ? parseFloat(result.usdcReceived) : undefined,
1517
+ executionTimeMs: Date.now() - startTime,
1518
+ };
1519
+ }
1520
+ catch (error) {
1521
+ return {
1522
+ success: false,
1523
+ strategy: 'redeem',
1524
+ error: error instanceof Error ? error.message : String(error),
1525
+ executionTimeMs: Date.now() - startTime,
1526
+ };
1527
+ }
1528
+ }
1529
+ async settleBySell() {
1530
+ const startTime = Date.now();
1531
+ if (!this.tradingService || !this.market || !this.currentRound) {
1532
+ return {
1533
+ success: false,
1534
+ strategy: 'sell',
1535
+ error: 'Trading service or market not available',
1536
+ executionTimeMs: Date.now() - startTime,
1537
+ };
1538
+ }
1539
+ try {
1540
+ let totalReceived = 0;
1541
+ // Sell leg1 position if exists
1542
+ if (this.currentRound.leg1) {
1543
+ const leg1Shares = this.currentRound.leg1.shares;
1544
+ const result = await this.tradingService.createMarketOrder({
1545
+ tokenId: this.currentRound.leg1.tokenId,
1546
+ side: 'SELL',
1547
+ amount: leg1Shares,
1548
+ });
1549
+ if (result.success) {
1550
+ totalReceived += leg1Shares * (this.currentRound.leg1.side === 'UP'
1551
+ ? (this.upAsks[0]?.price ?? 0.5)
1552
+ : (this.downAsks[0]?.price ?? 0.5));
1553
+ }
1554
+ }
1555
+ // Sell leg2 position if exists
1556
+ if (this.currentRound.leg2) {
1557
+ const leg2Shares = this.currentRound.leg2.shares;
1558
+ const result = await this.tradingService.createMarketOrder({
1559
+ tokenId: this.currentRound.leg2.tokenId,
1560
+ side: 'SELL',
1561
+ amount: leg2Shares,
1562
+ });
1563
+ if (result.success) {
1564
+ totalReceived += leg2Shares * (this.currentRound.leg2.side === 'UP'
1565
+ ? (this.upAsks[0]?.price ?? 0.5)
1566
+ : (this.downAsks[0]?.price ?? 0.5));
1567
+ }
1568
+ }
1569
+ return {
1570
+ success: true,
1571
+ strategy: 'sell',
1572
+ amountReceived: totalReceived,
1573
+ executionTimeMs: Date.now() - startTime,
1574
+ };
1575
+ }
1576
+ catch (error) {
1577
+ return {
1578
+ success: false,
1579
+ strategy: 'sell',
1580
+ error: error instanceof Error ? error.message : String(error),
1581
+ executionTimeMs: Date.now() - startTime,
1582
+ };
1583
+ }
1584
+ }
1585
+ // ===== Private: Helpers =====
1586
+ log(message) {
1587
+ const shouldLog = this.config.debug || message.startsWith('Starting') || message.startsWith('Stopped');
1588
+ if (!shouldLog)
1589
+ return;
1590
+ const formatted = `[DipArb] ${message}`;
1591
+ // Use custom log handler if provided
1592
+ if (this.config.logHandler) {
1593
+ this.config.logHandler(formatted);
1594
+ }
1595
+ else {
1596
+ console.log(formatted);
1597
+ }
1598
+ }
1599
+ }
1600
+ // Re-export types
1601
+ export * from './dip-arb-types.js';
1602
+ //# sourceMappingURL=dip-arb-service.js.map