@catalyst-team/poly-sdk 0.2.0 → 0.3.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 (242) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +548 -813
  3. package/README.zh-CN.md +805 -0
  4. package/dist/__tests__/clob-api.test.d.ts +5 -0
  5. package/dist/__tests__/clob-api.test.d.ts.map +1 -0
  6. package/dist/__tests__/clob-api.test.js +240 -0
  7. package/dist/__tests__/clob-api.test.js.map +1 -0
  8. package/dist/__tests__/integration/arbitrage-service.integration.test.d.ts +12 -0
  9. package/dist/__tests__/integration/arbitrage-service.integration.test.d.ts.map +1 -0
  10. package/dist/__tests__/integration/arbitrage-service.integration.test.js +267 -0
  11. package/dist/__tests__/integration/arbitrage-service.integration.test.js.map +1 -0
  12. package/dist/__tests__/integration/bridge-client.integration.test.d.ts +11 -0
  13. package/dist/__tests__/integration/bridge-client.integration.test.d.ts.map +1 -0
  14. package/dist/__tests__/integration/bridge-client.integration.test.js +260 -0
  15. package/dist/__tests__/integration/bridge-client.integration.test.js.map +1 -0
  16. package/dist/__tests__/integration/clob-api.integration.test.d.ts +13 -0
  17. package/dist/__tests__/integration/clob-api.integration.test.d.ts.map +1 -0
  18. package/dist/__tests__/integration/clob-api.integration.test.js +170 -0
  19. package/dist/__tests__/integration/clob-api.integration.test.js.map +1 -0
  20. package/dist/__tests__/integration/ctf-client.integration.test.d.ts +17 -0
  21. package/dist/__tests__/integration/ctf-client.integration.test.d.ts.map +1 -0
  22. package/dist/__tests__/integration/ctf-client.integration.test.js +234 -0
  23. package/dist/__tests__/integration/ctf-client.integration.test.js.map +1 -0
  24. package/dist/__tests__/integration/data-api.integration.test.d.ts +9 -0
  25. package/dist/__tests__/integration/data-api.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/integration/data-api.integration.test.js +164 -0
  27. package/dist/__tests__/integration/data-api.integration.test.js.map +1 -0
  28. package/dist/__tests__/integration/gamma-api.integration.test.d.ts +9 -0
  29. package/dist/__tests__/integration/gamma-api.integration.test.d.ts.map +1 -0
  30. package/dist/__tests__/integration/gamma-api.integration.test.js +170 -0
  31. package/dist/__tests__/integration/gamma-api.integration.test.js.map +1 -0
  32. package/dist/__tests__/integration/market-service.integration.test.d.ts +10 -0
  33. package/dist/__tests__/integration/market-service.integration.test.d.ts.map +1 -0
  34. package/dist/__tests__/integration/market-service.integration.test.js +173 -0
  35. package/dist/__tests__/integration/market-service.integration.test.js.map +1 -0
  36. package/dist/__tests__/integration/realtime-service-v2.integration.test.d.ts +10 -0
  37. package/dist/__tests__/integration/realtime-service-v2.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/integration/realtime-service-v2.integration.test.js +307 -0
  39. package/dist/__tests__/integration/realtime-service-v2.integration.test.js.map +1 -0
  40. package/dist/__tests__/integration/trading-service.integration.test.d.ts +10 -0
  41. package/dist/__tests__/integration/trading-service.integration.test.d.ts.map +1 -0
  42. package/dist/__tests__/integration/trading-service.integration.test.js +58 -0
  43. package/dist/__tests__/integration/trading-service.integration.test.js.map +1 -0
  44. package/dist/__tests__/test-utils.d.ts +92 -0
  45. package/dist/__tests__/test-utils.d.ts.map +1 -0
  46. package/dist/__tests__/test-utils.js +143 -0
  47. package/dist/__tests__/test-utils.js.map +1 -0
  48. package/dist/clients/bridge-client.d.ts +388 -0
  49. package/dist/clients/bridge-client.d.ts.map +1 -0
  50. package/dist/clients/bridge-client.js +587 -0
  51. package/dist/clients/bridge-client.js.map +1 -0
  52. package/dist/clients/clob-api.d.ts +391 -0
  53. package/dist/clients/clob-api.d.ts.map +1 -0
  54. package/dist/clients/clob-api.js +448 -0
  55. package/dist/clients/clob-api.js.map +1 -0
  56. package/dist/clients/ctf-client.d.ts +473 -0
  57. package/dist/clients/ctf-client.d.ts.map +1 -0
  58. package/dist/clients/ctf-client.js +915 -0
  59. package/dist/clients/ctf-client.js.map +1 -0
  60. package/dist/clients/data-api.d.ts +439 -0
  61. package/dist/clients/data-api.d.ts.map +1 -0
  62. package/dist/clients/data-api.js +592 -0
  63. package/dist/clients/data-api.js.map +1 -0
  64. package/dist/clients/gamma-api.d.ts +401 -0
  65. package/dist/clients/gamma-api.d.ts.map +1 -0
  66. package/dist/clients/gamma-api.js +352 -0
  67. package/dist/clients/gamma-api.js.map +1 -0
  68. package/dist/clients/subgraph.d.ts +196 -0
  69. package/dist/clients/subgraph.d.ts.map +1 -0
  70. package/dist/clients/subgraph.js +332 -0
  71. package/dist/clients/subgraph.js.map +1 -0
  72. package/dist/clients/trading-client.d.ts +252 -0
  73. package/dist/clients/trading-client.d.ts.map +1 -0
  74. package/dist/clients/trading-client.js +543 -0
  75. package/dist/clients/trading-client.js.map +1 -0
  76. package/dist/clients/websocket-manager.d.ts +103 -0
  77. package/dist/clients/websocket-manager.d.ts.map +1 -0
  78. package/dist/clients/websocket-manager.js +200 -0
  79. package/dist/clients/websocket-manager.js.map +1 -0
  80. package/dist/core/cache-adapter-bridge.d.ts +36 -0
  81. package/dist/core/cache-adapter-bridge.d.ts.map +1 -0
  82. package/dist/core/cache-adapter-bridge.js +81 -0
  83. package/dist/core/cache-adapter-bridge.js.map +1 -0
  84. package/dist/core/cache.d.ts +41 -0
  85. package/dist/core/cache.d.ts.map +1 -0
  86. package/dist/core/cache.js +72 -0
  87. package/dist/core/cache.js.map +1 -0
  88. package/dist/core/errors.d.ts +39 -0
  89. package/dist/core/errors.d.ts.map +1 -0
  90. package/dist/core/errors.js +86 -0
  91. package/dist/core/errors.js.map +1 -0
  92. package/dist/core/rate-limiter.d.ts +32 -0
  93. package/dist/core/rate-limiter.d.ts.map +1 -0
  94. package/dist/core/rate-limiter.js +75 -0
  95. package/dist/core/rate-limiter.js.map +1 -0
  96. package/dist/core/types.d.ts +402 -0
  97. package/dist/core/types.d.ts.map +1 -0
  98. package/dist/core/types.js +19 -0
  99. package/dist/core/types.js.map +1 -0
  100. package/dist/core/types.test.d.ts +7 -0
  101. package/dist/core/types.test.d.ts.map +1 -0
  102. package/dist/core/types.test.js +122 -0
  103. package/dist/core/types.test.js.map +1 -0
  104. package/dist/core/unified-cache.d.ts +63 -0
  105. package/dist/core/unified-cache.d.ts.map +1 -0
  106. package/dist/core/unified-cache.js +114 -0
  107. package/dist/core/unified-cache.js.map +1 -0
  108. package/dist/index.d.ts +151 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +248 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/services/arbitrage-service.d.ts +409 -0
  113. package/dist/services/arbitrage-service.d.ts.map +1 -0
  114. package/dist/services/arbitrage-service.js +1440 -0
  115. package/dist/services/arbitrage-service.js.map +1 -0
  116. package/dist/services/authorization-service.d.ts +97 -0
  117. package/dist/services/authorization-service.d.ts.map +1 -0
  118. package/dist/services/authorization-service.js +279 -0
  119. package/dist/services/authorization-service.js.map +1 -0
  120. package/dist/services/market-service.d.ts +208 -0
  121. package/dist/services/market-service.d.ts.map +1 -0
  122. package/dist/services/market-service.js +774 -0
  123. package/dist/services/market-service.js.map +1 -0
  124. package/dist/services/onchain-service.d.ts +309 -0
  125. package/dist/services/onchain-service.d.ts.map +1 -0
  126. package/dist/services/onchain-service.js +417 -0
  127. package/dist/services/onchain-service.js.map +1 -0
  128. package/dist/services/realtime-service-v2.d.ts +361 -0
  129. package/dist/services/realtime-service-v2.d.ts.map +1 -0
  130. package/dist/services/realtime-service-v2.js +840 -0
  131. package/dist/services/realtime-service-v2.js.map +1 -0
  132. package/dist/services/realtime-service.d.ts +82 -0
  133. package/dist/services/realtime-service.d.ts.map +1 -0
  134. package/dist/services/realtime-service.js +182 -0
  135. package/dist/services/realtime-service.js.map +1 -0
  136. package/dist/services/smart-money-service.d.ts +196 -0
  137. package/dist/services/smart-money-service.d.ts.map +1 -0
  138. package/dist/services/smart-money-service.js +358 -0
  139. package/dist/services/smart-money-service.js.map +1 -0
  140. package/dist/services/swap-service.d.ts +217 -0
  141. package/dist/services/swap-service.d.ts.map +1 -0
  142. package/dist/services/swap-service.js +695 -0
  143. package/dist/services/swap-service.js.map +1 -0
  144. package/dist/services/trading-service.d.ts +156 -0
  145. package/dist/services/trading-service.d.ts.map +1 -0
  146. package/dist/services/trading-service.js +356 -0
  147. package/dist/services/trading-service.js.map +1 -0
  148. package/dist/services/wallet-service.d.ts +275 -0
  149. package/dist/services/wallet-service.d.ts.map +1 -0
  150. package/dist/services/wallet-service.js +630 -0
  151. package/dist/services/wallet-service.js.map +1 -0
  152. package/dist/utils/price-utils.d.ts +153 -0
  153. package/dist/utils/price-utils.d.ts.map +1 -0
  154. package/dist/utils/price-utils.js +236 -0
  155. package/dist/utils/price-utils.js.map +1 -0
  156. package/dist/utils/price-utils.test.d.ts +5 -0
  157. package/dist/utils/price-utils.test.d.ts.map +1 -0
  158. package/dist/utils/price-utils.test.js +192 -0
  159. package/dist/utils/price-utils.test.js.map +1 -0
  160. package/package.json +10 -4
  161. package/README.en.md +0 -538
  162. package/docs/00-design.md +0 -760
  163. package/docs/02-API.md +0 -1148
  164. package/docs/arb/test-plan.md +0 -387
  165. package/docs/arb/test-results.md +0 -336
  166. package/docs/arbitrage.md +0 -754
  167. package/docs/reports/smart-money-analysis-2025-12-23-cn.md +0 -840
  168. package/examples/01-basic-usage.ts +0 -68
  169. package/examples/02-smart-money.ts +0 -95
  170. package/examples/03-market-analysis.ts +0 -108
  171. package/examples/04-kline-aggregation.ts +0 -158
  172. package/examples/05-follow-wallet-strategy.ts +0 -156
  173. package/examples/06-services-demo.ts +0 -124
  174. package/examples/07-realtime-websocket.ts +0 -117
  175. package/examples/08-trading-orders.ts +0 -278
  176. package/examples/09-rewards-tracking.ts +0 -187
  177. package/examples/10-ctf-operations.ts +0 -336
  178. package/examples/11-live-arbitrage-scan.ts +0 -221
  179. package/examples/12-trending-arb-monitor.ts +0 -406
  180. package/examples/13-arbitrage-service.ts +0 -211
  181. package/examples/README.md +0 -179
  182. package/scripts/README.md +0 -163
  183. package/scripts/approvals/approve-erc1155.ts +0 -129
  184. package/scripts/approvals/approve-neg-risk-erc1155.ts +0 -149
  185. package/scripts/approvals/approve-neg-risk.ts +0 -102
  186. package/scripts/approvals/check-all-allowances.ts +0 -150
  187. package/scripts/approvals/check-allowance.ts +0 -129
  188. package/scripts/approvals/check-ctf-approval.ts +0 -158
  189. package/scripts/arb/faze-bo3-arb.ts +0 -385
  190. package/scripts/arb/settle-position.ts +0 -190
  191. package/scripts/arb/token-rebalancer.ts +0 -420
  192. package/scripts/arb-tests/01-unit-tests.ts +0 -495
  193. package/scripts/arb-tests/02-integration-tests.ts +0 -412
  194. package/scripts/arb-tests/03-e2e-tests.ts +0 -503
  195. package/scripts/arb-tests/README.md +0 -109
  196. package/scripts/datas/001-report.md +0 -486
  197. package/scripts/datas/clone-modal-screenshot.png +0 -0
  198. package/scripts/deposit/deposit-native-usdc.ts +0 -179
  199. package/scripts/deposit/deposit-usdc.ts +0 -155
  200. package/scripts/deposit/swap-usdc-to-usdce.ts +0 -375
  201. package/scripts/research/research-markets.ts +0 -166
  202. package/scripts/trading/check-orders.ts +0 -50
  203. package/scripts/trading/sell-nvidia-positions.ts +0 -206
  204. package/scripts/trading/test-order.ts +0 -172
  205. package/scripts/verify/test-approve-trading.ts +0 -98
  206. package/scripts/verify/test-provider-fix.ts +0 -43
  207. package/scripts/verify/test-search-mcp.ts +0 -113
  208. package/scripts/verify/verify-all-apis.ts +0 -160
  209. package/scripts/wallet/check-wallet-balances.ts +0 -75
  210. package/scripts/wallet/test-wallet-operations.ts +0 -191
  211. package/scripts/wallet/verify-wallet-tools.ts +0 -124
  212. package/src/__tests__/clob-api.test.ts +0 -301
  213. package/src/__tests__/integration/bridge-client.integration.test.ts +0 -314
  214. package/src/__tests__/integration/clob-api.integration.test.ts +0 -218
  215. package/src/__tests__/integration/ctf-client.integration.test.ts +0 -331
  216. package/src/__tests__/integration/data-api.integration.test.ts +0 -194
  217. package/src/__tests__/integration/gamma-api.integration.test.ts +0 -206
  218. package/src/__tests__/test-utils.ts +0 -170
  219. package/src/clients/bridge-client.ts +0 -841
  220. package/src/clients/clob-api.ts +0 -629
  221. package/src/clients/ctf-client.ts +0 -1216
  222. package/src/clients/data-api.ts +0 -469
  223. package/src/clients/gamma-api.ts +0 -597
  224. package/src/clients/trading-client.ts +0 -749
  225. package/src/clients/websocket-manager.ts +0 -267
  226. package/src/core/cache-adapter-bridge.ts +0 -94
  227. package/src/core/cache.ts +0 -85
  228. package/src/core/errors.ts +0 -117
  229. package/src/core/rate-limiter.ts +0 -74
  230. package/src/core/types.ts +0 -360
  231. package/src/core/unified-cache.ts +0 -153
  232. package/src/index.ts +0 -461
  233. package/src/services/arbitrage-service.ts +0 -1807
  234. package/src/services/authorization-service.ts +0 -357
  235. package/src/services/market-service.ts +0 -544
  236. package/src/services/realtime-service.ts +0 -196
  237. package/src/services/swap-service.ts +0 -896
  238. package/src/services/wallet-service.ts +0 -259
  239. package/src/utils/price-utils.ts +0 -307
  240. package/tsconfig.json +0 -8
  241. package/vitest.config.ts +0 -19
  242. package/vitest.integration.config.ts +0 -18
@@ -0,0 +1,1440 @@
1
+ /**
2
+ * ArbitrageService - Real-time Arbitrage Detection and Execution
3
+ *
4
+ * Uses WebSocket for real-time orderbook monitoring and automatically
5
+ * detects arbitrage opportunities in Polymarket binary markets.
6
+ *
7
+ * Strategy:
8
+ * - Long Arb: Buy YES + NO (effective cost < $1) → Merge → $1 USDC
9
+ * - Short Arb: Sell pre-held YES + NO tokens (effective revenue > $1)
10
+ *
11
+ * Features:
12
+ * - Real-time orderbook monitoring via WebSocket
13
+ * - Automatic arbitrage detection using effective prices
14
+ * - Configurable profit threshold and trade sizes
15
+ * - Auto-execute mode or event-based manual mode
16
+ * - Balance tracking and position management
17
+ *
18
+ * Based on: scripts/arb/faze-bo3-arb.ts
19
+ * Docs: docs/arbitrage.md
20
+ */
21
+ import { EventEmitter } from 'events';
22
+ import { RealtimeServiceV2, } from './realtime-service-v2.js';
23
+ import { TradingService } from './trading-service.js';
24
+ import { MarketService } from './market-service.js';
25
+ import { CTFClient } from '../clients/ctf-client.js';
26
+ import { GammaApiClient } from '../clients/gamma-api.js';
27
+ import { RateLimiter } from '../core/rate-limiter.js';
28
+ import { createUnifiedCache } from '../core/unified-cache.js';
29
+ import { getEffectivePrices } from '../utils/price-utils.js';
30
+ // ===== ArbitrageService =====
31
+ export class ArbitrageService extends EventEmitter {
32
+ realtimeService;
33
+ marketSubscription = null;
34
+ ctf = null;
35
+ tradingService = null;
36
+ rateLimiter;
37
+ market = null;
38
+ config;
39
+ orderbook = {
40
+ yesBids: [],
41
+ yesAsks: [],
42
+ noBids: [],
43
+ noAsks: [],
44
+ lastUpdate: 0,
45
+ };
46
+ balance = {
47
+ usdc: 0,
48
+ yesTokens: 0,
49
+ noTokens: 0,
50
+ lastUpdate: 0,
51
+ };
52
+ isExecuting = false;
53
+ lastExecutionTime = 0;
54
+ lastRebalanceTime = 0;
55
+ balanceUpdateInterval = null;
56
+ rebalanceInterval = null;
57
+ isRunning = false;
58
+ totalCapital = 0;
59
+ // Statistics
60
+ stats = {
61
+ opportunitiesDetected: 0,
62
+ executionsAttempted: 0,
63
+ executionsSucceeded: 0,
64
+ totalProfit: 0,
65
+ startTime: 0,
66
+ };
67
+ constructor(config = {}) {
68
+ super();
69
+ this.config = {
70
+ privateKey: config.privateKey,
71
+ rpcUrl: config.rpcUrl || 'https://polygon-rpc.com',
72
+ profitThreshold: config.profitThreshold ?? 0.005,
73
+ minTradeSize: config.minTradeSize ?? 5,
74
+ maxTradeSize: config.maxTradeSize ?? 100,
75
+ minTokenReserve: config.minTokenReserve ?? 10,
76
+ autoExecute: config.autoExecute ?? false,
77
+ enableLogging: config.enableLogging ?? true,
78
+ executionCooldown: config.executionCooldown ?? 5000,
79
+ // Rebalancer config
80
+ enableRebalancer: config.enableRebalancer ?? false,
81
+ minUsdcRatio: config.minUsdcRatio ?? 0.2,
82
+ maxUsdcRatio: config.maxUsdcRatio ?? 0.8,
83
+ targetUsdcRatio: config.targetUsdcRatio ?? 0.5,
84
+ imbalanceThreshold: config.imbalanceThreshold ?? 5,
85
+ rebalanceIntervalMs: config.rebalanceInterval ?? 10000,
86
+ rebalanceCooldown: config.rebalanceCooldown ?? 30000,
87
+ // Execution safety
88
+ sizeSafetyFactor: config.sizeSafetyFactor ?? 0.8,
89
+ autoFixImbalance: config.autoFixImbalance ?? true,
90
+ };
91
+ this.rateLimiter = new RateLimiter();
92
+ this.realtimeService = new RealtimeServiceV2({ debug: false });
93
+ // Initialize trading clients if private key provided
94
+ if (this.config.privateKey) {
95
+ this.ctf = new CTFClient({
96
+ privateKey: this.config.privateKey,
97
+ rpcUrl: this.config.rpcUrl,
98
+ });
99
+ const cache = createUnifiedCache();
100
+ this.tradingService = new TradingService(this.rateLimiter, cache, {
101
+ privateKey: this.config.privateKey,
102
+ chainId: 137,
103
+ });
104
+ }
105
+ // RealtimeServiceV2 event handlers are set up during subscription
106
+ }
107
+ // ===== Public API =====
108
+ /**
109
+ * Start monitoring a market for arbitrage opportunities
110
+ */
111
+ async start(market) {
112
+ if (this.isRunning) {
113
+ throw new Error('ArbitrageService is already running. Call stop() first.');
114
+ }
115
+ this.market = market;
116
+ this.isRunning = true;
117
+ this.stats.startTime = Date.now();
118
+ this.log(`Starting arbitrage monitor for: ${market.name}`);
119
+ this.log(`Condition ID: ${market.conditionId.slice(0, 20)}...`);
120
+ this.log(`Profit Threshold: ${(this.config.profitThreshold * 100).toFixed(2)}%`);
121
+ this.log(`Auto Execute: ${this.config.autoExecute ? 'YES' : 'NO'}`);
122
+ // Initialize trading service
123
+ if (this.tradingService) {
124
+ await this.tradingService.initialize();
125
+ this.log(`Wallet: ${this.ctf?.getAddress()}`);
126
+ await this.updateBalance();
127
+ this.log(`USDC Balance: ${this.balance.usdc.toFixed(2)}`);
128
+ this.log(`YES Tokens: ${this.balance.yesTokens.toFixed(2)}`);
129
+ this.log(`NO Tokens: ${this.balance.noTokens.toFixed(2)}`);
130
+ // Calculate total capital (USDC + paired tokens)
131
+ const pairedTokens = Math.min(this.balance.yesTokens, this.balance.noTokens);
132
+ this.totalCapital = this.balance.usdc + pairedTokens;
133
+ this.log(`Total Capital: ${this.totalCapital.toFixed(2)}`);
134
+ // Start balance update interval
135
+ this.balanceUpdateInterval = setInterval(() => this.updateBalance(), 30000);
136
+ // Start rebalancer if enabled
137
+ if (this.config.enableRebalancer) {
138
+ this.log(`Rebalancer: ENABLED (USDC range: ${(this.config.minUsdcRatio * 100).toFixed(0)}%-${(this.config.maxUsdcRatio * 100).toFixed(0)}%, target: ${(this.config.targetUsdcRatio * 100).toFixed(0)}%)`);
139
+ this.rebalanceInterval = setInterval(() => this.checkAndRebalance(), this.config.rebalanceIntervalMs);
140
+ }
141
+ }
142
+ else {
143
+ this.log('No wallet configured - monitoring only');
144
+ }
145
+ // Connect and subscribe to WebSocket
146
+ this.realtimeService.connect();
147
+ this.marketSubscription = this.realtimeService.subscribeMarkets([market.yesTokenId, market.noTokenId], {
148
+ onOrderbook: (book) => {
149
+ // Convert OrderbookSnapshot to BookUpdate format
150
+ const bookUpdate = {
151
+ assetId: book.assetId,
152
+ bids: book.bids,
153
+ asks: book.asks,
154
+ timestamp: book.timestamp,
155
+ };
156
+ this.handleBookUpdate(bookUpdate);
157
+ },
158
+ onError: (error) => this.emit('error', error),
159
+ });
160
+ this.emit('started', market);
161
+ this.log('Monitoring for arbitrage opportunities...');
162
+ }
163
+ /**
164
+ * Stop monitoring
165
+ */
166
+ async stop() {
167
+ if (!this.isRunning)
168
+ return;
169
+ this.isRunning = false;
170
+ if (this.balanceUpdateInterval) {
171
+ clearInterval(this.balanceUpdateInterval);
172
+ this.balanceUpdateInterval = null;
173
+ }
174
+ if (this.rebalanceInterval) {
175
+ clearInterval(this.rebalanceInterval);
176
+ this.rebalanceInterval = null;
177
+ }
178
+ // Unsubscribe and disconnect
179
+ if (this.marketSubscription) {
180
+ this.marketSubscription.unsubscribe();
181
+ this.marketSubscription = null;
182
+ }
183
+ this.realtimeService.disconnect();
184
+ this.log('Stopped');
185
+ this.log(`Total opportunities: ${this.stats.opportunitiesDetected}`);
186
+ this.log(`Executions: ${this.stats.executionsSucceeded}/${this.stats.executionsAttempted}`);
187
+ this.log(`Total profit: $${this.stats.totalProfit.toFixed(2)}`);
188
+ this.emit('stopped');
189
+ }
190
+ /**
191
+ * Get current orderbook state
192
+ */
193
+ getOrderbook() {
194
+ return { ...this.orderbook };
195
+ }
196
+ /**
197
+ * Get current balance state
198
+ */
199
+ getBalance() {
200
+ return { ...this.balance };
201
+ }
202
+ /**
203
+ * Get statistics
204
+ */
205
+ getStats() {
206
+ return {
207
+ ...this.stats,
208
+ runningTimeMs: this.isRunning ? Date.now() - this.stats.startTime : 0,
209
+ };
210
+ }
211
+ /**
212
+ * Check for arbitrage opportunity based on current orderbook
213
+ */
214
+ checkOpportunity() {
215
+ if (!this.market)
216
+ return null;
217
+ const { yesBids, yesAsks, noBids, noAsks } = this.orderbook;
218
+ if (yesBids.length === 0 || yesAsks.length === 0 || noBids.length === 0 || noAsks.length === 0) {
219
+ return null;
220
+ }
221
+ const yesBestBid = yesBids[0]?.price || 0;
222
+ const yesBestAsk = yesAsks[0]?.price || 1;
223
+ const noBestBid = noBids[0]?.price || 0;
224
+ const noBestAsk = noAsks[0]?.price || 1;
225
+ // Calculate effective prices
226
+ const effective = getEffectivePrices(yesBestAsk, yesBestBid, noBestAsk, noBestBid);
227
+ // Check for arbitrage
228
+ const longCost = effective.effectiveBuyYes + effective.effectiveBuyNo;
229
+ const longProfit = 1 - longCost;
230
+ const shortRevenue = effective.effectiveSellYes + effective.effectiveSellNo;
231
+ const shortProfit = shortRevenue - 1;
232
+ // Calculate sizes with safety factor to prevent partial fills
233
+ // Use min of both sides * safety factor to ensure both orders can fill
234
+ const safetyFactor = this.config.sizeSafetyFactor;
235
+ const orderbookLongSize = Math.min(yesAsks[0]?.size || 0, noAsks[0]?.size || 0) * safetyFactor;
236
+ const orderbookShortSize = Math.min(yesBids[0]?.size || 0, noBids[0]?.size || 0) * safetyFactor;
237
+ const heldPairs = Math.min(this.balance.yesTokens, this.balance.noTokens);
238
+ const balanceLongSize = longCost > 0 ? this.balance.usdc / longCost : 0;
239
+ // Check long arb
240
+ if (longProfit > this.config.profitThreshold) {
241
+ const maxSize = Math.min(orderbookLongSize, balanceLongSize * safetyFactor, this.config.maxTradeSize);
242
+ if (maxSize >= this.config.minTradeSize) {
243
+ return {
244
+ type: 'long',
245
+ profitRate: longProfit,
246
+ profitPercent: longProfit * 100,
247
+ effectivePrices: {
248
+ buyYes: effective.effectiveBuyYes,
249
+ buyNo: effective.effectiveBuyNo,
250
+ sellYes: effective.effectiveSellYes,
251
+ sellNo: effective.effectiveSellNo,
252
+ },
253
+ maxOrderbookSize: orderbookLongSize,
254
+ maxBalanceSize: balanceLongSize,
255
+ recommendedSize: maxSize,
256
+ estimatedProfit: longProfit * maxSize,
257
+ description: `Buy YES @ ${effective.effectiveBuyYes.toFixed(4)} + NO @ ${effective.effectiveBuyNo.toFixed(4)}, Merge for $1`,
258
+ timestamp: Date.now(),
259
+ };
260
+ }
261
+ }
262
+ // Check short arb
263
+ if (shortProfit > this.config.profitThreshold) {
264
+ const maxSize = Math.min(orderbookShortSize, heldPairs, this.config.maxTradeSize);
265
+ if (maxSize >= this.config.minTradeSize && heldPairs >= this.config.minTokenReserve) {
266
+ return {
267
+ type: 'short',
268
+ profitRate: shortProfit,
269
+ profitPercent: shortProfit * 100,
270
+ effectivePrices: {
271
+ buyYes: effective.effectiveBuyYes,
272
+ buyNo: effective.effectiveBuyNo,
273
+ sellYes: effective.effectiveSellYes,
274
+ sellNo: effective.effectiveSellNo,
275
+ },
276
+ maxOrderbookSize: orderbookShortSize,
277
+ maxBalanceSize: heldPairs,
278
+ recommendedSize: maxSize,
279
+ estimatedProfit: shortProfit * maxSize,
280
+ description: `Sell YES @ ${effective.effectiveSellYes.toFixed(4)} + NO @ ${effective.effectiveSellNo.toFixed(4)}`,
281
+ timestamp: Date.now(),
282
+ };
283
+ }
284
+ }
285
+ return null;
286
+ }
287
+ /**
288
+ * Manually execute an arbitrage opportunity
289
+ */
290
+ async execute(opportunity) {
291
+ if (!this.ctf || !this.tradingService || !this.market) {
292
+ return {
293
+ success: false,
294
+ type: opportunity.type,
295
+ size: 0,
296
+ profit: 0,
297
+ txHashes: [],
298
+ error: 'Trading not configured (no private key)',
299
+ executionTimeMs: 0,
300
+ };
301
+ }
302
+ if (this.isExecuting) {
303
+ return {
304
+ success: false,
305
+ type: opportunity.type,
306
+ size: 0,
307
+ profit: 0,
308
+ txHashes: [],
309
+ error: 'Another execution in progress',
310
+ executionTimeMs: 0,
311
+ };
312
+ }
313
+ this.isExecuting = true;
314
+ this.stats.executionsAttempted++;
315
+ const startTime = Date.now();
316
+ try {
317
+ const result = opportunity.type === 'long'
318
+ ? await this.executeLongArb(opportunity)
319
+ : await this.executeShortArb(opportunity);
320
+ if (result.success) {
321
+ this.stats.executionsSucceeded++;
322
+ this.stats.totalProfit += result.profit;
323
+ this.lastExecutionTime = Date.now();
324
+ }
325
+ this.emit('execution', result);
326
+ return result;
327
+ }
328
+ finally {
329
+ this.isExecuting = false;
330
+ // Update balance after execution
331
+ await this.updateBalance();
332
+ }
333
+ }
334
+ // ===== Rebalancer Methods =====
335
+ /**
336
+ * Calculate recommended rebalance action based on current state
337
+ */
338
+ calculateRebalanceAction() {
339
+ if (!this.market || this.totalCapital === 0) {
340
+ return { type: 'none', amount: 0, reason: 'No market or capital', priority: 0 };
341
+ }
342
+ const { usdc, yesTokens, noTokens } = this.balance;
343
+ const pairedTokens = Math.min(yesTokens, noTokens);
344
+ const currentTotal = usdc + pairedTokens;
345
+ const usdcRatio = usdc / currentTotal;
346
+ const tokenImbalance = yesTokens - noTokens;
347
+ // Priority 1: Fix YES/NO imbalance (highest priority - risk control)
348
+ if (Math.abs(tokenImbalance) > this.config.imbalanceThreshold) {
349
+ if (tokenImbalance > 0) {
350
+ const sellAmount = Math.min(tokenImbalance, yesTokens * 0.5);
351
+ if (sellAmount >= this.config.minTradeSize) {
352
+ return {
353
+ type: 'sell_yes',
354
+ amount: Math.floor(sellAmount * 1e6) / 1e6,
355
+ reason: `Risk: YES > NO by ${tokenImbalance.toFixed(2)}`,
356
+ priority: 100,
357
+ };
358
+ }
359
+ }
360
+ else {
361
+ const sellAmount = Math.min(-tokenImbalance, noTokens * 0.5);
362
+ if (sellAmount >= this.config.minTradeSize) {
363
+ return {
364
+ type: 'sell_no',
365
+ amount: Math.floor(sellAmount * 1e6) / 1e6,
366
+ reason: `Risk: NO > YES by ${(-tokenImbalance).toFixed(2)}`,
367
+ priority: 100,
368
+ };
369
+ }
370
+ }
371
+ }
372
+ // Priority 2: USDC ratio too high (> maxUsdcRatio) → Split to create tokens
373
+ if (usdcRatio > this.config.maxUsdcRatio) {
374
+ const targetUsdc = this.totalCapital * this.config.targetUsdcRatio;
375
+ const excessUsdc = usdc - targetUsdc;
376
+ const splitAmount = Math.min(excessUsdc * 0.5, usdc * 0.3);
377
+ if (splitAmount >= this.config.minTradeSize) {
378
+ return {
379
+ type: 'split',
380
+ amount: Math.floor(splitAmount * 100) / 100,
381
+ reason: `USDC ${(usdcRatio * 100).toFixed(0)}% > ${(this.config.maxUsdcRatio * 100).toFixed(0)}% max`,
382
+ priority: 50,
383
+ };
384
+ }
385
+ }
386
+ // Priority 3: USDC ratio too low (< minUsdcRatio) → Merge tokens to recover USDC
387
+ if (usdcRatio < this.config.minUsdcRatio && pairedTokens >= this.config.minTradeSize) {
388
+ const targetUsdc = this.totalCapital * this.config.targetUsdcRatio;
389
+ const neededUsdc = targetUsdc - usdc;
390
+ const mergeAmount = Math.min(neededUsdc * 0.5, pairedTokens * 0.5);
391
+ if (mergeAmount >= this.config.minTradeSize) {
392
+ return {
393
+ type: 'merge',
394
+ amount: Math.floor(mergeAmount * 100) / 100,
395
+ reason: `USDC ${(usdcRatio * 100).toFixed(0)}% < ${(this.config.minUsdcRatio * 100).toFixed(0)}% min`,
396
+ priority: 50,
397
+ };
398
+ }
399
+ }
400
+ return { type: 'none', amount: 0, reason: 'Balanced', priority: 0 };
401
+ }
402
+ /**
403
+ * Execute a rebalance action
404
+ */
405
+ async rebalance(action) {
406
+ if (!this.ctf || !this.tradingService || !this.market) {
407
+ return {
408
+ success: false,
409
+ action: action || { type: 'none', amount: 0, reason: 'No trading config', priority: 0 },
410
+ error: 'Trading not configured',
411
+ };
412
+ }
413
+ const rebalanceAction = action || this.calculateRebalanceAction();
414
+ if (rebalanceAction.type === 'none') {
415
+ return { success: true, action: rebalanceAction };
416
+ }
417
+ this.log(`\n🔄 Rebalance: ${rebalanceAction.type.toUpperCase()} ${rebalanceAction.amount.toFixed(2)}`);
418
+ this.log(` Reason: ${rebalanceAction.reason}`);
419
+ try {
420
+ let txHash;
421
+ switch (rebalanceAction.type) {
422
+ case 'split': {
423
+ const result = await this.ctf.split(this.market.conditionId, rebalanceAction.amount.toString());
424
+ txHash = result.txHash;
425
+ this.log(` ✅ Split TX: ${txHash}`);
426
+ break;
427
+ }
428
+ case 'merge': {
429
+ const tokenIds = {
430
+ yesTokenId: this.market.yesTokenId,
431
+ noTokenId: this.market.noTokenId,
432
+ };
433
+ const result = await this.ctf.mergeByTokenIds(this.market.conditionId, tokenIds, rebalanceAction.amount.toString());
434
+ txHash = result.txHash;
435
+ this.log(` ✅ Merge TX: ${txHash}`);
436
+ break;
437
+ }
438
+ case 'sell_yes': {
439
+ const result = await this.tradingService.createMarketOrder({
440
+ tokenId: this.market.yesTokenId,
441
+ side: 'SELL',
442
+ amount: rebalanceAction.amount,
443
+ orderType: 'FOK',
444
+ });
445
+ if (!result.success) {
446
+ throw new Error(result.errorMsg || 'Sell YES failed');
447
+ }
448
+ this.log(` ✅ Sold ${rebalanceAction.amount.toFixed(2)} YES tokens`);
449
+ break;
450
+ }
451
+ case 'sell_no': {
452
+ const result = await this.tradingService.createMarketOrder({
453
+ tokenId: this.market.noTokenId,
454
+ side: 'SELL',
455
+ amount: rebalanceAction.amount,
456
+ orderType: 'FOK',
457
+ });
458
+ if (!result.success) {
459
+ throw new Error(result.errorMsg || 'Sell NO failed');
460
+ }
461
+ this.log(` ✅ Sold ${rebalanceAction.amount.toFixed(2)} NO tokens`);
462
+ break;
463
+ }
464
+ }
465
+ await this.updateBalance();
466
+ const rebalanceResult = { success: true, action: rebalanceAction, txHash };
467
+ this.emit('rebalance', rebalanceResult);
468
+ return rebalanceResult;
469
+ }
470
+ catch (error) {
471
+ this.log(` ❌ Failed: ${error.message}`);
472
+ const rebalanceResult = {
473
+ success: false,
474
+ action: rebalanceAction,
475
+ error: error.message,
476
+ };
477
+ this.emit('rebalance', rebalanceResult);
478
+ return rebalanceResult;
479
+ }
480
+ }
481
+ // ===== Settle Position Methods =====
482
+ /**
483
+ * Settle a market position - merge paired tokens to recover USDC
484
+ * @param market Market to settle (defaults to current market)
485
+ * @param execute If true, execute the merge. If false, just return info.
486
+ */
487
+ async settlePosition(market, execute = false) {
488
+ const targetMarket = market || this.market;
489
+ if (!targetMarket) {
490
+ throw new Error('No market specified');
491
+ }
492
+ if (!this.ctf) {
493
+ return {
494
+ market: targetMarket,
495
+ yesBalance: 0,
496
+ noBalance: 0,
497
+ pairedTokens: 0,
498
+ unpairedYes: 0,
499
+ unpairedNo: 0,
500
+ merged: false,
501
+ error: 'CTF client not configured',
502
+ };
503
+ }
504
+ const tokenIds = {
505
+ yesTokenId: targetMarket.yesTokenId,
506
+ noTokenId: targetMarket.noTokenId,
507
+ };
508
+ // Get token balances
509
+ const positions = await this.ctf.getPositionBalanceByTokenIds(targetMarket.conditionId, tokenIds);
510
+ const yesBalance = parseFloat(positions.yesBalance);
511
+ const noBalance = parseFloat(positions.noBalance);
512
+ const pairedTokens = Math.min(yesBalance, noBalance);
513
+ const unpairedYes = yesBalance - pairedTokens;
514
+ const unpairedNo = noBalance - pairedTokens;
515
+ this.log(`\n📊 Position: ${targetMarket.name}`);
516
+ this.log(` YES: ${yesBalance.toFixed(6)}`);
517
+ this.log(` NO: ${noBalance.toFixed(6)}`);
518
+ this.log(` Paired: ${pairedTokens.toFixed(6)} (can merge → $${pairedTokens.toFixed(2)} USDC)`);
519
+ if (unpairedYes > 0.001) {
520
+ this.log(` ⚠️ Unpaired YES: ${unpairedYes.toFixed(6)}`);
521
+ }
522
+ if (unpairedNo > 0.001) {
523
+ this.log(` ⚠️ Unpaired NO: ${unpairedNo.toFixed(6)}`);
524
+ }
525
+ const result = {
526
+ market: targetMarket,
527
+ yesBalance,
528
+ noBalance,
529
+ pairedTokens,
530
+ unpairedYes,
531
+ unpairedNo,
532
+ merged: false,
533
+ };
534
+ // Execute merge if requested and we have enough pairs
535
+ if (execute && pairedTokens >= 1) {
536
+ const mergeAmount = Math.floor(pairedTokens * 1e6) / 1e6;
537
+ this.log(`\n🔄 Merging ${mergeAmount.toFixed(6)} token pairs...`);
538
+ try {
539
+ const mergeResult = await this.ctf.mergeByTokenIds(targetMarket.conditionId, tokenIds, mergeAmount.toString());
540
+ result.merged = true;
541
+ result.mergeAmount = mergeAmount;
542
+ result.mergeTxHash = mergeResult.txHash;
543
+ result.usdcRecovered = mergeAmount;
544
+ this.log(` ✅ Merge TX: ${mergeResult.txHash}`);
545
+ this.log(` ✅ Recovered: $${mergeAmount.toFixed(2)} USDC`);
546
+ }
547
+ catch (error) {
548
+ result.error = error.message;
549
+ this.log(` ❌ Merge failed: ${error.message}`);
550
+ }
551
+ }
552
+ else if (pairedTokens >= 1) {
553
+ this.log(` 💡 Run settlePosition(market, true) to recover $${pairedTokens.toFixed(2)} USDC`);
554
+ }
555
+ this.emit('settle', result);
556
+ return result;
557
+ }
558
+ /**
559
+ * Settle multiple markets at once
560
+ */
561
+ async settleMultiple(markets, execute = false) {
562
+ const results = [];
563
+ let totalMerged = 0;
564
+ let totalUnpairedYes = 0;
565
+ let totalUnpairedNo = 0;
566
+ for (const market of markets) {
567
+ const result = await this.settlePosition(market, execute);
568
+ results.push(result);
569
+ if (result.usdcRecovered)
570
+ totalMerged += result.usdcRecovered;
571
+ totalUnpairedYes += result.unpairedYes;
572
+ totalUnpairedNo += result.unpairedNo;
573
+ }
574
+ this.log(`\n═══════════════════════════════════════`);
575
+ this.log(`SUMMARY: ${markets.length} markets`);
576
+ if (execute) {
577
+ this.log(`Total Merged: $${totalMerged.toFixed(2)} USDC`);
578
+ }
579
+ if (totalUnpairedYes > 0.001 || totalUnpairedNo > 0.001) {
580
+ this.log(`Unpaired YES: ${totalUnpairedYes.toFixed(6)}`);
581
+ this.log(`Unpaired NO: ${totalUnpairedNo.toFixed(6)}`);
582
+ }
583
+ return results;
584
+ }
585
+ /**
586
+ * Clear all positions in a market using the best strategy
587
+ *
588
+ * Strategy:
589
+ * - Active market: Merge paired tokens → Sell remaining unpaired tokens
590
+ * - Resolved market: Redeem winning tokens
591
+ *
592
+ * @param market Market to clear positions from
593
+ * @param execute If true, execute the clearing. If false, just return info.
594
+ * @returns Result with all actions taken
595
+ *
596
+ * @example
597
+ * ```typescript
598
+ * const service = new ArbitrageService({ privateKey: '0x...' });
599
+ *
600
+ * // View clearing plan
601
+ * const plan = await service.clearPositions(market, false);
602
+ * console.log(`Will recover: $${plan.totalUsdcRecovered}`);
603
+ *
604
+ * // Execute clearing
605
+ * const result = await service.clearPositions(market, true);
606
+ * ```
607
+ */
608
+ async clearPositions(market, execute = false) {
609
+ if (!this.ctf) {
610
+ return {
611
+ market,
612
+ marketStatus: 'unknown',
613
+ yesBalance: 0,
614
+ noBalance: 0,
615
+ actions: [],
616
+ totalUsdcRecovered: 0,
617
+ success: false,
618
+ error: 'CTF client not configured',
619
+ };
620
+ }
621
+ const tokenIds = {
622
+ yesTokenId: market.yesTokenId,
623
+ noTokenId: market.noTokenId,
624
+ };
625
+ // Get token balances
626
+ const positions = await this.ctf.getPositionBalanceByTokenIds(market.conditionId, tokenIds);
627
+ const yesBalance = parseFloat(positions.yesBalance);
628
+ const noBalance = parseFloat(positions.noBalance);
629
+ if (yesBalance < 0.001 && noBalance < 0.001) {
630
+ this.log(`No positions to clear for ${market.name}`);
631
+ return {
632
+ market,
633
+ marketStatus: 'unknown',
634
+ yesBalance,
635
+ noBalance,
636
+ actions: [],
637
+ totalUsdcRecovered: 0,
638
+ success: true,
639
+ };
640
+ }
641
+ this.log(`\n🧹 Clearing positions: ${market.name}`);
642
+ this.log(` YES: ${yesBalance.toFixed(6)}, NO: ${noBalance.toFixed(6)}`);
643
+ // Check if market is resolved
644
+ let marketStatus = 'unknown';
645
+ let winningOutcome;
646
+ try {
647
+ const resolution = await this.ctf.getMarketResolution(market.conditionId);
648
+ marketStatus = resolution.isResolved ? 'resolved' : 'active';
649
+ winningOutcome = resolution.winningOutcome;
650
+ this.log(` Status: ${marketStatus}${resolution.isResolved ? ` (Winner: ${winningOutcome})` : ''}`);
651
+ }
652
+ catch {
653
+ // If we can't determine resolution, try to get market status from MarketService
654
+ try {
655
+ const cache = createUnifiedCache();
656
+ const tempMarketService = new MarketService(undefined, undefined, this.rateLimiter, cache);
657
+ const clobMarket = await tempMarketService.getClobMarket(market.conditionId);
658
+ marketStatus = clobMarket.closed ? 'resolved' : 'active';
659
+ this.log(` Status: ${marketStatus} (from MarketService)`);
660
+ }
661
+ catch {
662
+ this.log(` Status: unknown (assuming active)`);
663
+ marketStatus = 'active';
664
+ }
665
+ }
666
+ const actions = [];
667
+ let totalUsdcRecovered = 0;
668
+ if (!execute) {
669
+ // Dry run - calculate expected actions
670
+ if (marketStatus === 'resolved' && winningOutcome) {
671
+ // Resolved market: redeem winning tokens
672
+ const winningBalance = winningOutcome === 'YES' ? yesBalance : noBalance;
673
+ if (winningBalance >= 0.001) {
674
+ actions.push({
675
+ type: 'redeem',
676
+ amount: winningBalance,
677
+ usdcResult: winningBalance, // 1 USDC per winning token
678
+ success: true,
679
+ });
680
+ totalUsdcRecovered = winningBalance;
681
+ }
682
+ }
683
+ else {
684
+ // Active market: merge + sell
685
+ const pairedTokens = Math.min(yesBalance, noBalance);
686
+ const unpairedYes = yesBalance - pairedTokens;
687
+ const unpairedNo = noBalance - pairedTokens;
688
+ if (pairedTokens >= 1) {
689
+ actions.push({
690
+ type: 'merge',
691
+ amount: pairedTokens,
692
+ usdcResult: pairedTokens,
693
+ success: true,
694
+ });
695
+ totalUsdcRecovered += pairedTokens;
696
+ }
697
+ // For unpaired tokens, estimate sell price (assume ~0.5 if unknown)
698
+ if (unpairedYes >= this.config.minTradeSize) {
699
+ const estimatedPrice = 0.5; // Conservative estimate
700
+ actions.push({
701
+ type: 'sell_yes',
702
+ amount: unpairedYes,
703
+ usdcResult: unpairedYes * estimatedPrice,
704
+ success: true,
705
+ });
706
+ totalUsdcRecovered += unpairedYes * estimatedPrice;
707
+ }
708
+ if (unpairedNo >= this.config.minTradeSize) {
709
+ const estimatedPrice = 0.5;
710
+ actions.push({
711
+ type: 'sell_no',
712
+ amount: unpairedNo,
713
+ usdcResult: unpairedNo * estimatedPrice,
714
+ success: true,
715
+ });
716
+ totalUsdcRecovered += unpairedNo * estimatedPrice;
717
+ }
718
+ }
719
+ this.log(` 📋 Plan: ${actions.length} actions, ~$${totalUsdcRecovered.toFixed(2)} USDC`);
720
+ for (const action of actions) {
721
+ this.log(` - ${action.type}: ${action.amount.toFixed(4)} → ~$${action.usdcResult.toFixed(2)}`);
722
+ }
723
+ return {
724
+ market,
725
+ marketStatus,
726
+ yesBalance,
727
+ noBalance,
728
+ actions,
729
+ totalUsdcRecovered,
730
+ success: true,
731
+ };
732
+ }
733
+ // Execute clearing
734
+ this.log(` 🔄 Executing...`);
735
+ if (marketStatus === 'resolved' && winningOutcome) {
736
+ // Resolved market: redeem
737
+ const winningBalance = winningOutcome === 'YES' ? yesBalance : noBalance;
738
+ if (winningBalance >= 0.001) {
739
+ try {
740
+ const redeemResult = await this.ctf.redeem(market.conditionId);
741
+ actions.push({
742
+ type: 'redeem',
743
+ amount: winningBalance,
744
+ usdcResult: winningBalance,
745
+ txHash: redeemResult.txHash,
746
+ success: true,
747
+ });
748
+ totalUsdcRecovered = winningBalance;
749
+ this.log(` ✅ Redeemed: ${winningBalance.toFixed(4)} tokens → $${winningBalance.toFixed(2)} USDC`);
750
+ }
751
+ catch (error) {
752
+ actions.push({
753
+ type: 'redeem',
754
+ amount: winningBalance,
755
+ usdcResult: 0,
756
+ success: false,
757
+ error: error.message,
758
+ });
759
+ this.log(` ❌ Redeem failed: ${error.message}`);
760
+ }
761
+ }
762
+ }
763
+ else {
764
+ // Active market: merge + sell
765
+ const pairedTokens = Math.min(yesBalance, noBalance);
766
+ let unpairedYes = yesBalance - pairedTokens;
767
+ let unpairedNo = noBalance - pairedTokens;
768
+ // Step 1: Merge paired tokens
769
+ if (pairedTokens >= 1) {
770
+ const mergeAmount = Math.floor(pairedTokens * 1e6) / 1e6;
771
+ try {
772
+ const mergeResult = await this.ctf.mergeByTokenIds(market.conditionId, tokenIds, mergeAmount.toString());
773
+ actions.push({
774
+ type: 'merge',
775
+ amount: mergeAmount,
776
+ usdcResult: mergeAmount,
777
+ txHash: mergeResult.txHash,
778
+ success: true,
779
+ });
780
+ totalUsdcRecovered += mergeAmount;
781
+ this.log(` ✅ Merged: ${mergeAmount.toFixed(4)} pairs → $${mergeAmount.toFixed(2)} USDC`);
782
+ }
783
+ catch (error) {
784
+ actions.push({
785
+ type: 'merge',
786
+ amount: mergeAmount,
787
+ usdcResult: 0,
788
+ success: false,
789
+ error: error.message,
790
+ });
791
+ this.log(` ❌ Merge failed: ${error.message}`);
792
+ // Update unpaired amounts since merge failed
793
+ unpairedYes = yesBalance;
794
+ unpairedNo = noBalance;
795
+ }
796
+ }
797
+ // Step 2: Sell unpaired tokens
798
+ if (this.tradingService && unpairedYes >= this.config.minTradeSize) {
799
+ try {
800
+ const sellAmount = Math.floor(unpairedYes * 1e6) / 1e6;
801
+ const result = await this.tradingService.createMarketOrder({
802
+ tokenId: market.yesTokenId,
803
+ side: 'SELL',
804
+ amount: sellAmount,
805
+ orderType: 'FOK',
806
+ });
807
+ if (result.success) {
808
+ // Estimate USDC received (conservative estimate since we don't have exact trade info)
809
+ const usdcReceived = sellAmount * 0.5; // Assume ~0.5 average price
810
+ actions.push({
811
+ type: 'sell_yes',
812
+ amount: sellAmount,
813
+ usdcResult: usdcReceived,
814
+ success: true,
815
+ });
816
+ totalUsdcRecovered += usdcReceived;
817
+ this.log(` ✅ Sold YES: ${sellAmount.toFixed(4)} → ~$${usdcReceived.toFixed(2)} USDC`);
818
+ }
819
+ else {
820
+ throw new Error(result.errorMsg || 'Sell failed');
821
+ }
822
+ }
823
+ catch (error) {
824
+ actions.push({
825
+ type: 'sell_yes',
826
+ amount: unpairedYes,
827
+ usdcResult: 0,
828
+ success: false,
829
+ error: error.message,
830
+ });
831
+ this.log(` ❌ Sell YES failed: ${error.message}`);
832
+ }
833
+ }
834
+ if (this.tradingService && unpairedNo >= this.config.minTradeSize) {
835
+ try {
836
+ const sellAmount = Math.floor(unpairedNo * 1e6) / 1e6;
837
+ const result = await this.tradingService.createMarketOrder({
838
+ tokenId: market.noTokenId,
839
+ side: 'SELL',
840
+ amount: sellAmount,
841
+ orderType: 'FOK',
842
+ });
843
+ if (result.success) {
844
+ // Estimate USDC received (conservative estimate since we don't have exact trade info)
845
+ const usdcReceived = sellAmount * 0.5; // Assume ~0.5 average price
846
+ actions.push({
847
+ type: 'sell_no',
848
+ amount: sellAmount,
849
+ usdcResult: usdcReceived,
850
+ success: true,
851
+ });
852
+ totalUsdcRecovered += usdcReceived;
853
+ this.log(` ✅ Sold NO: ${sellAmount.toFixed(4)} → ~$${usdcReceived.toFixed(2)} USDC`);
854
+ }
855
+ else {
856
+ throw new Error(result.errorMsg || 'Sell failed');
857
+ }
858
+ }
859
+ catch (error) {
860
+ actions.push({
861
+ type: 'sell_no',
862
+ amount: unpairedNo,
863
+ usdcResult: 0,
864
+ success: false,
865
+ error: error.message,
866
+ });
867
+ this.log(` ❌ Sell NO failed: ${error.message}`);
868
+ }
869
+ }
870
+ }
871
+ const allSuccess = actions.every((a) => a.success);
872
+ this.log(` 📊 Result: ${actions.filter((a) => a.success).length}/${actions.length} succeeded, $${totalUsdcRecovered.toFixed(2)} recovered`);
873
+ const result = {
874
+ market,
875
+ marketStatus,
876
+ yesBalance,
877
+ noBalance,
878
+ actions,
879
+ totalUsdcRecovered,
880
+ success: allSuccess,
881
+ };
882
+ this.emit('settle', result);
883
+ return result;
884
+ }
885
+ /**
886
+ * Clear positions from multiple markets
887
+ *
888
+ * @param markets Markets to clear
889
+ * @param execute If true, execute clearing
890
+ * @returns Results for all markets
891
+ */
892
+ async clearAllPositions(markets, execute = false) {
893
+ const results = [];
894
+ let totalRecovered = 0;
895
+ this.log(`\n🧹 Clearing positions from ${markets.length} markets...`);
896
+ for (const market of markets) {
897
+ const result = await this.clearPositions(market, execute);
898
+ results.push(result);
899
+ totalRecovered += result.totalUsdcRecovered;
900
+ }
901
+ this.log(`\n═══════════════════════════════════════`);
902
+ this.log(`TOTAL: $${totalRecovered.toFixed(2)} USDC ${execute ? 'recovered' : 'expected'}`);
903
+ return results;
904
+ }
905
+ // ===== Private Methods =====
906
+ handleBookUpdate(update) {
907
+ if (!this.market)
908
+ return;
909
+ const { assetId, bids, asks } = update;
910
+ if (assetId === this.market.yesTokenId) {
911
+ this.orderbook.yesBids = bids.sort((a, b) => b.price - a.price);
912
+ this.orderbook.yesAsks = asks.sort((a, b) => a.price - b.price);
913
+ }
914
+ else if (assetId === this.market.noTokenId) {
915
+ this.orderbook.noBids = bids.sort((a, b) => b.price - a.price);
916
+ this.orderbook.noAsks = asks.sort((a, b) => a.price - b.price);
917
+ }
918
+ this.orderbook.lastUpdate = Date.now();
919
+ this.emit('orderbookUpdate', this.orderbook);
920
+ // Check for arbitrage opportunity
921
+ this.checkAndHandleOpportunity();
922
+ }
923
+ checkAndHandleOpportunity() {
924
+ const opportunity = this.checkOpportunity();
925
+ if (opportunity) {
926
+ this.stats.opportunitiesDetected++;
927
+ this.emit('opportunity', opportunity);
928
+ this.log(`\n${'!'.repeat(60)}`);
929
+ this.log(`${opportunity.type.toUpperCase()} ARB: ${opportunity.description}`);
930
+ this.log(`Profit: ${opportunity.profitPercent.toFixed(2)}%, Size: ${opportunity.recommendedSize.toFixed(2)}, Est: $${opportunity.estimatedProfit.toFixed(2)}`);
931
+ this.log('!'.repeat(60));
932
+ // Auto-execute if enabled and cooldown has passed
933
+ if (this.config.autoExecute && !this.isExecuting) {
934
+ const timeSinceLastExecution = Date.now() - this.lastExecutionTime;
935
+ if (timeSinceLastExecution >= this.config.executionCooldown) {
936
+ this.execute(opportunity).catch((error) => {
937
+ this.emit('error', error);
938
+ });
939
+ }
940
+ }
941
+ }
942
+ }
943
+ async checkAndRebalance() {
944
+ if (!this.isRunning || this.isExecuting)
945
+ return;
946
+ // Check cooldown
947
+ const timeSinceLastRebalance = Date.now() - this.lastRebalanceTime;
948
+ if (timeSinceLastRebalance < this.config.rebalanceCooldown) {
949
+ return;
950
+ }
951
+ await this.updateBalance();
952
+ const action = this.calculateRebalanceAction();
953
+ if (action.type !== 'none' && action.amount >= this.config.minTradeSize) {
954
+ await this.rebalance(action);
955
+ this.lastRebalanceTime = Date.now();
956
+ }
957
+ }
958
+ /**
959
+ * Fix YES/NO imbalance immediately after partial execution
960
+ * This is critical when one side of a parallel order fails
961
+ */
962
+ async fixImbalanceIfNeeded() {
963
+ if (!this.config.autoFixImbalance || !this.ctf || !this.tradingService || !this.market)
964
+ return;
965
+ await this.updateBalance();
966
+ const imbalance = this.balance.yesTokens - this.balance.noTokens;
967
+ if (Math.abs(imbalance) <= this.config.imbalanceThreshold)
968
+ return;
969
+ this.log(`\n⚠️ Imbalance detected after execution: ${imbalance > 0 ? 'YES' : 'NO'} excess = ${Math.abs(imbalance).toFixed(2)}`);
970
+ // Sell the excess tokens to restore balance
971
+ const sellAmount = Math.floor(Math.abs(imbalance) * 0.9 * 1e6) / 1e6; // Sell 90% to be safe
972
+ if (sellAmount < this.config.minTradeSize)
973
+ return;
974
+ try {
975
+ if (imbalance > 0) {
976
+ // Sell excess YES
977
+ const result = await this.tradingService.createMarketOrder({
978
+ tokenId: this.market.yesTokenId,
979
+ side: 'SELL',
980
+ amount: sellAmount,
981
+ orderType: 'FOK',
982
+ });
983
+ if (result.success) {
984
+ this.log(` ✅ Sold ${sellAmount.toFixed(2)} excess YES to restore balance`);
985
+ }
986
+ }
987
+ else {
988
+ // Sell excess NO
989
+ const result = await this.tradingService.createMarketOrder({
990
+ tokenId: this.market.noTokenId,
991
+ side: 'SELL',
992
+ amount: sellAmount,
993
+ orderType: 'FOK',
994
+ });
995
+ if (result.success) {
996
+ this.log(` ✅ Sold ${sellAmount.toFixed(2)} excess NO to restore balance`);
997
+ }
998
+ }
999
+ }
1000
+ catch (error) {
1001
+ this.log(` ❌ Failed to fix imbalance: ${error.message}`);
1002
+ }
1003
+ }
1004
+ async updateBalance() {
1005
+ if (!this.ctf || !this.market)
1006
+ return;
1007
+ try {
1008
+ const tokenIds = {
1009
+ yesTokenId: this.market.yesTokenId,
1010
+ noTokenId: this.market.noTokenId,
1011
+ };
1012
+ const [usdcBalance, positions] = await Promise.all([
1013
+ this.ctf.getUsdcBalance(),
1014
+ this.ctf.getPositionBalanceByTokenIds(this.market.conditionId, tokenIds),
1015
+ ]);
1016
+ this.balance = {
1017
+ usdc: parseFloat(usdcBalance),
1018
+ yesTokens: parseFloat(positions.yesBalance),
1019
+ noTokens: parseFloat(positions.noBalance),
1020
+ lastUpdate: Date.now(),
1021
+ };
1022
+ this.emit('balanceUpdate', this.balance);
1023
+ }
1024
+ catch (error) {
1025
+ this.emit('error', error);
1026
+ }
1027
+ }
1028
+ async executeLongArb(opportunity) {
1029
+ const startTime = Date.now();
1030
+ const txHashes = [];
1031
+ const size = opportunity.recommendedSize;
1032
+ this.log(`\nExecuting Long Arb (Buy → Merge)...`);
1033
+ try {
1034
+ const { buyYes, buyNo } = opportunity.effectivePrices;
1035
+ const requiredUsdc = (buyYes + buyNo) * size;
1036
+ if (this.balance.usdc < requiredUsdc) {
1037
+ return {
1038
+ success: false,
1039
+ type: 'long',
1040
+ size,
1041
+ profit: 0,
1042
+ txHashes,
1043
+ error: `Insufficient USDC.e: have ${this.balance.usdc.toFixed(2)}, need ${requiredUsdc.toFixed(2)}`,
1044
+ executionTimeMs: Date.now() - startTime,
1045
+ };
1046
+ }
1047
+ // Buy both tokens in parallel
1048
+ this.log(` 1. Buying tokens in parallel...`);
1049
+ const [buyYesResult, buyNoResult] = await Promise.all([
1050
+ this.tradingService.createMarketOrder({
1051
+ tokenId: this.market.yesTokenId,
1052
+ side: 'BUY',
1053
+ amount: size * buyYes,
1054
+ orderType: 'FOK',
1055
+ }),
1056
+ this.tradingService.createMarketOrder({
1057
+ tokenId: this.market.noTokenId,
1058
+ side: 'BUY',
1059
+ amount: size * buyNo,
1060
+ orderType: 'FOK',
1061
+ }),
1062
+ ]);
1063
+ const outcomes = this.market.outcomes || ['YES', 'NO'];
1064
+ this.log(` ${outcomes[0]}: ${buyYesResult.success ? '✓' : '✗'}, ${outcomes[1]}: ${buyNoResult.success ? '✓' : '✗'}`);
1065
+ // If one succeeded and the other failed, we have an imbalance - fix it
1066
+ if (!buyYesResult.success || !buyNoResult.success) {
1067
+ // Check if partial execution created imbalance
1068
+ if (buyYesResult.success !== buyNoResult.success) {
1069
+ this.log(` ⚠️ Partial execution detected - attempting to fix imbalance...`);
1070
+ await this.fixImbalanceIfNeeded();
1071
+ }
1072
+ return {
1073
+ success: false,
1074
+ type: 'long',
1075
+ size,
1076
+ profit: 0,
1077
+ txHashes,
1078
+ error: `Order(s) failed: YES=${buyYesResult.errorMsg}, NO=${buyNoResult.errorMsg}`,
1079
+ executionTimeMs: Date.now() - startTime,
1080
+ };
1081
+ }
1082
+ // Merge tokens
1083
+ const tokenIds = {
1084
+ yesTokenId: this.market.yesTokenId,
1085
+ noTokenId: this.market.noTokenId,
1086
+ };
1087
+ // Update balance to get accurate token counts
1088
+ await this.updateBalance();
1089
+ const heldPairs = Math.min(this.balance.yesTokens, this.balance.noTokens);
1090
+ const mergeSize = Math.floor(Math.min(size, heldPairs) * 1e6) / 1e6;
1091
+ if (mergeSize >= this.config.minTradeSize) {
1092
+ this.log(` 2. Merging ${mergeSize.toFixed(2)} pairs...`);
1093
+ try {
1094
+ const mergeResult = await this.ctf.mergeByTokenIds(this.market.conditionId, tokenIds, mergeSize.toString());
1095
+ txHashes.push(mergeResult.txHash);
1096
+ this.log(` TX: ${mergeResult.txHash}`);
1097
+ const profit = opportunity.profitRate * mergeSize;
1098
+ this.log(` ✅ Long Arb completed! Profit: ~$${profit.toFixed(2)}`);
1099
+ return {
1100
+ success: true,
1101
+ type: 'long',
1102
+ size: mergeSize,
1103
+ profit,
1104
+ txHashes,
1105
+ executionTimeMs: Date.now() - startTime,
1106
+ };
1107
+ }
1108
+ catch (mergeError) {
1109
+ this.log(` ⚠️ Merge failed: ${mergeError.message}`);
1110
+ return {
1111
+ success: false,
1112
+ type: 'long',
1113
+ size,
1114
+ profit: 0,
1115
+ txHashes,
1116
+ error: `Merge failed: ${mergeError.message}`,
1117
+ executionTimeMs: Date.now() - startTime,
1118
+ };
1119
+ }
1120
+ }
1121
+ return {
1122
+ success: false,
1123
+ type: 'long',
1124
+ size,
1125
+ profit: 0,
1126
+ txHashes,
1127
+ error: `Insufficient pairs for merge: ${heldPairs.toFixed(2)}`,
1128
+ executionTimeMs: Date.now() - startTime,
1129
+ };
1130
+ }
1131
+ catch (error) {
1132
+ return {
1133
+ success: false,
1134
+ type: 'long',
1135
+ size,
1136
+ profit: 0,
1137
+ txHashes,
1138
+ error: error.message,
1139
+ executionTimeMs: Date.now() - startTime,
1140
+ };
1141
+ }
1142
+ }
1143
+ async executeShortArb(opportunity) {
1144
+ const startTime = Date.now();
1145
+ const txHashes = [];
1146
+ const size = opportunity.recommendedSize;
1147
+ this.log(`\nExecuting Short Arb (Sell Pre-held Tokens)...`);
1148
+ try {
1149
+ const heldPairs = Math.min(this.balance.yesTokens, this.balance.noTokens);
1150
+ if (heldPairs < size) {
1151
+ return {
1152
+ success: false,
1153
+ type: 'short',
1154
+ size,
1155
+ profit: 0,
1156
+ txHashes,
1157
+ error: `Insufficient held tokens: have ${heldPairs.toFixed(2)}, need ${size.toFixed(2)}`,
1158
+ executionTimeMs: Date.now() - startTime,
1159
+ };
1160
+ }
1161
+ // Sell both tokens in parallel
1162
+ this.log(` 1. Selling pre-held tokens in parallel...`);
1163
+ const [sellYesResult, sellNoResult] = await Promise.all([
1164
+ this.tradingService.createMarketOrder({
1165
+ tokenId: this.market.yesTokenId,
1166
+ side: 'SELL',
1167
+ amount: size,
1168
+ orderType: 'FOK',
1169
+ }),
1170
+ this.tradingService.createMarketOrder({
1171
+ tokenId: this.market.noTokenId,
1172
+ side: 'SELL',
1173
+ amount: size,
1174
+ orderType: 'FOK',
1175
+ }),
1176
+ ]);
1177
+ const outcomes = this.market.outcomes || ['YES', 'NO'];
1178
+ this.log(` ${outcomes[0]}: ${sellYesResult.success ? '✓' : '✗'}, ${outcomes[1]}: ${sellNoResult.success ? '✓' : '✗'}`);
1179
+ // If one succeeded and the other failed, we have an imbalance
1180
+ if (!sellYesResult.success || !sellNoResult.success) {
1181
+ // Check if partial execution created imbalance
1182
+ if (sellYesResult.success !== sellNoResult.success) {
1183
+ this.log(` ⚠️ Partial execution detected - imbalance created`);
1184
+ // Note: For short arb, we just sold one side, creating imbalance
1185
+ // The rebalancer will fix this on next cycle
1186
+ await this.fixImbalanceIfNeeded();
1187
+ }
1188
+ return {
1189
+ success: false,
1190
+ type: 'short',
1191
+ size,
1192
+ profit: 0,
1193
+ txHashes,
1194
+ error: `Order(s) failed: YES=${sellYesResult.errorMsg}, NO=${sellNoResult.errorMsg}`,
1195
+ executionTimeMs: Date.now() - startTime,
1196
+ };
1197
+ }
1198
+ const profit = opportunity.profitRate * size;
1199
+ this.log(` ✅ Short Arb completed! Profit: ~$${profit.toFixed(2)}`);
1200
+ return {
1201
+ success: true,
1202
+ type: 'short',
1203
+ size,
1204
+ profit,
1205
+ txHashes,
1206
+ executionTimeMs: Date.now() - startTime,
1207
+ };
1208
+ }
1209
+ catch (error) {
1210
+ return {
1211
+ success: false,
1212
+ type: 'short',
1213
+ size,
1214
+ profit: 0,
1215
+ txHashes,
1216
+ error: error.message,
1217
+ executionTimeMs: Date.now() - startTime,
1218
+ };
1219
+ }
1220
+ }
1221
+ log(message) {
1222
+ if (this.config.enableLogging) {
1223
+ console.log(`[ArbitrageService] ${message}`);
1224
+ }
1225
+ }
1226
+ // ===== Market Scanning Methods =====
1227
+ /**
1228
+ * Scan markets for arbitrage opportunities
1229
+ *
1230
+ * @param criteria Filter criteria for markets
1231
+ * @param minProfit Minimum profit threshold (default: 0.005 = 0.5%)
1232
+ * @returns Array of scan results sorted by profit
1233
+ *
1234
+ * @example
1235
+ * ```typescript
1236
+ * const service = new ArbitrageService({ privateKey: '0x...' });
1237
+ *
1238
+ * // Scan markets with at least $5000 volume
1239
+ * const results = await service.scanMarkets({ minVolume24h: 5000 }, 0.005);
1240
+ *
1241
+ * // Start arbitraging the best opportunity
1242
+ * if (results.length > 0 && results[0].arbType !== 'none') {
1243
+ * await service.start(results[0].market);
1244
+ * }
1245
+ * ```
1246
+ */
1247
+ async scanMarkets(criteria = {}, minProfit = 0.005) {
1248
+ const { minVolume24h = 1000, maxVolume24h, keywords = [], limit = 100, } = criteria;
1249
+ this.log(`Scanning markets (minVolume: $${minVolume24h}, minProfit: ${(minProfit * 100).toFixed(2)}%)...`);
1250
+ // Create temporary API clients for scanning
1251
+ const cache = createUnifiedCache();
1252
+ const gammaApi = new GammaApiClient(this.rateLimiter, cache);
1253
+ const tempMarketService = new MarketService(gammaApi, undefined, this.rateLimiter, cache);
1254
+ // Fetch active markets from Gamma API
1255
+ const markets = await gammaApi.getMarkets({
1256
+ active: true,
1257
+ closed: false,
1258
+ limit,
1259
+ });
1260
+ this.log(`Found ${markets.length} active markets`);
1261
+ const results = [];
1262
+ for (const gammaMarket of markets) {
1263
+ try {
1264
+ // Filter by volume
1265
+ const volume24h = gammaMarket.volume24hr || 0;
1266
+ if (volume24h < minVolume24h)
1267
+ continue;
1268
+ if (maxVolume24h && volume24h > maxVolume24h)
1269
+ continue;
1270
+ // Filter by keywords
1271
+ if (keywords.length > 0) {
1272
+ const marketText = `${gammaMarket.question} ${gammaMarket.description || ''}`.toLowerCase();
1273
+ const hasKeyword = keywords.some((kw) => marketText.includes(kw.toLowerCase()));
1274
+ if (!hasKeyword)
1275
+ continue;
1276
+ }
1277
+ // Skip non-binary markets
1278
+ if (!gammaMarket.conditionId || gammaMarket.outcomes?.length !== 2)
1279
+ continue;
1280
+ // Get market data for token IDs
1281
+ let clobMarket;
1282
+ try {
1283
+ clobMarket = await tempMarketService.getClobMarket(gammaMarket.conditionId);
1284
+ }
1285
+ catch {
1286
+ continue; // Skip if market data not available
1287
+ }
1288
+ const yesToken = clobMarket.tokens.find((t) => t.outcome === 'Yes');
1289
+ const noToken = clobMarket.tokens.find((t) => t.outcome === 'No');
1290
+ if (!yesToken || !noToken)
1291
+ continue;
1292
+ // Get orderbook data
1293
+ let orderbook;
1294
+ try {
1295
+ orderbook = await tempMarketService.getProcessedOrderbook(gammaMarket.conditionId);
1296
+ }
1297
+ catch {
1298
+ continue; // Skip if orderbook not available
1299
+ }
1300
+ const { effectivePrices, longArbProfit, shortArbProfit } = orderbook.summary;
1301
+ // Determine best arbitrage type
1302
+ let arbType = 'none';
1303
+ let profitRate = 0;
1304
+ if (longArbProfit > minProfit && longArbProfit >= shortArbProfit) {
1305
+ arbType = 'long';
1306
+ profitRate = longArbProfit;
1307
+ }
1308
+ else if (shortArbProfit > minProfit) {
1309
+ arbType = 'short';
1310
+ profitRate = shortArbProfit;
1311
+ }
1312
+ // Calculate available size (min of both sides)
1313
+ const yesAskSize = orderbook.yes.askSize || 0;
1314
+ const noAskSize = orderbook.no.askSize || 0;
1315
+ const yesBidSize = orderbook.yes.bidSize || 0;
1316
+ const noBidSize = orderbook.no.bidSize || 0;
1317
+ const availableSize = arbType === 'long'
1318
+ ? Math.min(yesAskSize, noAskSize)
1319
+ : Math.min(yesBidSize, noBidSize);
1320
+ // Calculate score (profit * volume * available_size)
1321
+ const score = profitRate * 100 * Math.log10(volume24h + 1) * Math.min(availableSize, 100) / 100;
1322
+ // Create market config
1323
+ const marketConfig = {
1324
+ name: gammaMarket.question.slice(0, 60) + (gammaMarket.question.length > 60 ? '...' : ''),
1325
+ conditionId: gammaMarket.conditionId,
1326
+ yesTokenId: yesToken.tokenId,
1327
+ noTokenId: noToken.tokenId,
1328
+ outcomes: gammaMarket.outcomes,
1329
+ };
1330
+ const longCost = effectivePrices.effectiveBuyYes + effectivePrices.effectiveBuyNo;
1331
+ const shortRevenue = effectivePrices.effectiveSellYes + effectivePrices.effectiveSellNo;
1332
+ let description;
1333
+ if (arbType === 'long') {
1334
+ description = `Buy YES@${effectivePrices.effectiveBuyYes.toFixed(4)} + NO@${effectivePrices.effectiveBuyNo.toFixed(4)} = ${longCost.toFixed(4)} → Merge for $1`;
1335
+ }
1336
+ else if (arbType === 'short') {
1337
+ description = `Sell YES@${effectivePrices.effectiveSellYes.toFixed(4)} + NO@${effectivePrices.effectiveSellNo.toFixed(4)} = ${shortRevenue.toFixed(4)}`;
1338
+ }
1339
+ else {
1340
+ description = `No opportunity (Long cost: ${longCost.toFixed(4)}, Short rev: ${shortRevenue.toFixed(4)})`;
1341
+ }
1342
+ results.push({
1343
+ market: marketConfig,
1344
+ arbType,
1345
+ profitRate,
1346
+ profitPercent: profitRate * 100,
1347
+ effectivePrices: {
1348
+ buyYes: effectivePrices.effectiveBuyYes,
1349
+ buyNo: effectivePrices.effectiveBuyNo,
1350
+ sellYes: effectivePrices.effectiveSellYes,
1351
+ sellNo: effectivePrices.effectiveSellNo,
1352
+ longCost,
1353
+ shortRevenue,
1354
+ },
1355
+ volume24h,
1356
+ availableSize,
1357
+ score,
1358
+ description,
1359
+ });
1360
+ }
1361
+ catch (error) {
1362
+ // Skip markets with errors
1363
+ continue;
1364
+ }
1365
+ }
1366
+ // Sort by profit (descending), then by score
1367
+ results.sort((a, b) => {
1368
+ if (b.profitRate !== a.profitRate)
1369
+ return b.profitRate - a.profitRate;
1370
+ return b.score - a.score;
1371
+ });
1372
+ this.log(`Found ${results.filter((r) => r.arbType !== 'none').length} markets with arbitrage opportunities`);
1373
+ return results;
1374
+ }
1375
+ /**
1376
+ * Quick scan for best arbitrage opportunities
1377
+ *
1378
+ * @param minProfit Minimum profit threshold (default: 0.005 = 0.5%)
1379
+ * @param limit Maximum number of results to return (default: 10)
1380
+ * @returns Top arbitrage opportunities
1381
+ *
1382
+ * @example
1383
+ * ```typescript
1384
+ * const service = new ArbitrageService({ privateKey: '0x...' });
1385
+ *
1386
+ * // Find best arbitrage opportunities
1387
+ * const top = await service.quickScan(0.005, 5);
1388
+ *
1389
+ * // Print results
1390
+ * for (const r of top) {
1391
+ * console.log(`${r.market.name}: ${r.arbType} +${r.profitPercent.toFixed(2)}%`);
1392
+ * }
1393
+ *
1394
+ * // Start the best one
1395
+ * if (top.length > 0) {
1396
+ * await service.start(top[0].market);
1397
+ * }
1398
+ * ```
1399
+ */
1400
+ async quickScan(minProfit = 0.005, limit = 10) {
1401
+ const results = await this.scanMarkets({ minVolume24h: 5000, limit: 100 }, minProfit);
1402
+ // Return only markets with opportunities, limited to requested count
1403
+ return results
1404
+ .filter((r) => r.arbType !== 'none')
1405
+ .slice(0, limit);
1406
+ }
1407
+ /**
1408
+ * Find and start arbitraging the best opportunity
1409
+ *
1410
+ * @param minProfit Minimum profit threshold (default: 0.005 = 0.5%)
1411
+ * @returns The scan result that was started, or null if none found
1412
+ *
1413
+ * @example
1414
+ * ```typescript
1415
+ * const service = new ArbitrageService({
1416
+ * privateKey: '0x...',
1417
+ * autoExecute: true,
1418
+ * profitThreshold: 0.005,
1419
+ * });
1420
+ *
1421
+ * // Find and start the best opportunity
1422
+ * const started = await service.findAndStart(0.005);
1423
+ * if (started) {
1424
+ * console.log(`Started: ${started.market.name} (+${started.profitPercent.toFixed(2)}%)`);
1425
+ * }
1426
+ * ```
1427
+ */
1428
+ async findAndStart(minProfit = 0.005) {
1429
+ const results = await this.quickScan(minProfit, 1);
1430
+ if (results.length === 0) {
1431
+ this.log('No arbitrage opportunities found');
1432
+ return null;
1433
+ }
1434
+ const best = results[0];
1435
+ this.log(`Best opportunity: ${best.market.name} (${best.arbType} +${best.profitPercent.toFixed(2)}%)`);
1436
+ await this.start(best.market);
1437
+ return best;
1438
+ }
1439
+ }
1440
+ //# sourceMappingURL=arbitrage-service.js.map