@catalyst-team/poly-sdk 0.2.0 → 0.2.1

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