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