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