@catalyst-team/poly-sdk 0.3.0 → 0.4.3

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