perp-cli 0.12.17 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +303 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +357 -459
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +240 -6
- package/dist/exchanges/aster.js +1139 -195
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +95 -0
- package/dist/exchanges/ondo/funds.js +198 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1386 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +29 -0
- package/dist/exchanges/venues/ondo.js +136 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +39 -0
- package/dist/exchanges/venues/qfex.js +140 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
package/dist/funding/rates.js
CHANGED
|
@@ -1,198 +1,185 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Real-time
|
|
2
|
+
* Real-time cross-venue funding rate comparison.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Fans out over every venue with the "marketData" role (design §10.6) using
|
|
5
|
+
* each venue's `publicData.fetchFundingRates(net)`, normalizes every row to a
|
|
6
|
+
* per-hour rate with the row's own basis (Lighter
|
|
7
|
+
* family 8h, Aster per-symbol), and matches markets across venues by
|
|
8
|
+
* `canonical` (null → excluded, §10.9).
|
|
9
|
+
*
|
|
10
|
+
* A venue that fails is reported in `exchangeStatus` ("error") and
|
|
11
|
+
* `exchangeErrors` (message) — its rows are absent, never silently empty.
|
|
7
12
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates as fetchLtFundingRates, } from "../shared-api.js";
|
|
13
|
+
import { annualizeHourlyRate, estimateHourlyFundingFromHourly } from "./normalize.js";
|
|
10
14
|
import { getHistoricalAverages } from "./history.js";
|
|
11
15
|
import { SPOT_PERP_TOKEN_MAP, MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
|
|
16
|
+
import { getVenue, listVenuesWithRole, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
|
|
17
|
+
import { rateToHourly } from "../exchanges/venue-types.js";
|
|
18
|
+
import { ensureVenuesRegistered } from "../exchanges/venues/index.js";
|
|
19
|
+
import { sharedApiNetwork } from "../api/public/urls.js";
|
|
20
|
+
import { PerpError } from "../errors.js";
|
|
12
21
|
// ── Default top symbols to track ──
|
|
13
22
|
export const TOP_SYMBOLS = [
|
|
14
23
|
"BTC", "ETH", "SOL", "DOGE", "SUI", "AVAX", "LINK", "ARB",
|
|
15
24
|
"WIF", "PEPE", "ONDO", "SEI", "TIA", "INJ", "NEAR",
|
|
16
25
|
"APT", "OP", "FIL", "AAVE", "MKR",
|
|
17
26
|
];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
fundingRate: p.funding,
|
|
28
|
-
hourlyRate: hourly,
|
|
29
|
-
annualizedPct: annualizeRate(p.funding, "pacifica"),
|
|
30
|
-
markPrice: p.mark,
|
|
31
|
-
nextFundingTime: p.nextFunding,
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
27
|
+
/** Mark-price source preference for `bestMarkPrice` (most liquid first). */
|
|
28
|
+
const MARK_PRICE_PREFERENCE = [
|
|
29
|
+
"hyperliquid", "pacifica", "lighter", "aster", "lighter-rh", "ondo", "arcus", "qfex",
|
|
30
|
+
];
|
|
31
|
+
// ── Per-venue fetch ──
|
|
32
|
+
function errorMessage(e) {
|
|
33
|
+
if (e instanceof PerpError)
|
|
34
|
+
return e.structured.message;
|
|
35
|
+
return e instanceof Error ? e.message : String(e);
|
|
38
36
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
markPrice: a.markPx,
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return [];
|
|
37
|
+
/**
|
|
38
|
+
* Funding rows for one venue, normalized. Rows the venue publishes without a
|
|
39
|
+
* mark price are priced from the same venue's markets (by venueSymbol); rows
|
|
40
|
+
* still unpriced, or without a canonical symbol, are excluded (never 0).
|
|
41
|
+
*/
|
|
42
|
+
async function fetchVenueRates(def, net) {
|
|
43
|
+
const rows = await def.publicData.fetchFundingRates(net);
|
|
44
|
+
let markBySymbol = null;
|
|
45
|
+
if (rows.some(r => r.markPrice === null)) {
|
|
46
|
+
const markets = await def.publicData.fetchMarkets(net);
|
|
47
|
+
markBySymbol = new Map(markets.map(m => [m.venueSymbol, m.markPrice]));
|
|
56
48
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
|
|
68
|
-
const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
|
|
69
|
-
const out = [];
|
|
70
|
-
for (const fr of funding) {
|
|
71
|
-
const symbol = fr.symbol || symMap.get(fr.marketId) || "";
|
|
72
|
-
if (!symbol)
|
|
73
|
-
continue;
|
|
74
|
-
const rate = fr.rate;
|
|
75
|
-
// Prefer fr.markPrice; fall back to orderBook last-trade as a documented
|
|
76
|
-
// price-source preference (NOT an error fallback).
|
|
77
|
-
const directMark = fr.markPrice;
|
|
78
|
-
const fallbackMark = priceMap.get(fr.marketId);
|
|
79
|
-
let markPrice;
|
|
80
|
-
if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
|
|
81
|
-
markPrice = directMark;
|
|
82
|
-
}
|
|
83
|
-
else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
|
|
84
|
-
markPrice = fallbackMark;
|
|
85
|
-
}
|
|
86
|
-
if (!Number.isFinite(rate) || markPrice === undefined)
|
|
87
|
-
continue;
|
|
88
|
-
out.push({
|
|
89
|
-
exchange: "lighter",
|
|
90
|
-
symbol,
|
|
91
|
-
fundingRate: rate,
|
|
92
|
-
hourlyRate: toHourlyRate(rate, "lighter"),
|
|
93
|
-
annualizedPct: annualizeRate(rate, "lighter"),
|
|
94
|
-
markPrice,
|
|
95
|
-
});
|
|
49
|
+
const out = [];
|
|
50
|
+
const unpriced = [];
|
|
51
|
+
for (const r of rows) {
|
|
52
|
+
if (r.canonical === null)
|
|
53
|
+
continue; // not matchable across venues
|
|
54
|
+
const markRaw = r.markPrice !== null ? r.markPrice : markBySymbol?.get(r.venueSymbol);
|
|
55
|
+
const markPrice = markRaw === undefined ? NaN : Number(markRaw);
|
|
56
|
+
if (!Number.isFinite(markPrice) || markPrice <= 0) {
|
|
57
|
+
unpriced.push(r.symbol);
|
|
58
|
+
continue;
|
|
96
59
|
}
|
|
97
|
-
|
|
60
|
+
out.push(toExchangeRate(r, r.canonical, markPrice, def.fundingVerified));
|
|
98
61
|
}
|
|
99
|
-
|
|
100
|
-
|
|
62
|
+
if (unpriced.length > 0) {
|
|
63
|
+
console.warn(`[${def.id}] excluded ${unpriced.length} funding row(s) without a mark price: ${unpriced.join(", ")}`);
|
|
101
64
|
}
|
|
65
|
+
return out;
|
|
102
66
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
67
|
+
function toExchangeRate(r, canonical, markPrice, fundingVerified) {
|
|
68
|
+
const fundingRate = Number(r.rate);
|
|
69
|
+
if (!Number.isFinite(fundingRate)) {
|
|
70
|
+
throw new PerpError("EXCHANGE_ERROR", `${r.venue} ${r.symbol}: non-numeric funding rate "${r.rate}"`);
|
|
71
|
+
}
|
|
72
|
+
const hourlyRate = rateToHourly(fundingRate, r.basis);
|
|
73
|
+
return {
|
|
74
|
+
exchange: r.venue,
|
|
75
|
+
symbol: r.symbol,
|
|
76
|
+
canonical,
|
|
77
|
+
fundingRate,
|
|
78
|
+
basis: r.basis,
|
|
79
|
+
hourlyRate,
|
|
80
|
+
annualizedPct: annualizeHourlyRate(hourlyRate),
|
|
81
|
+
markPrice,
|
|
82
|
+
nextFundingTime: r.nextFundingTime,
|
|
83
|
+
historicalAvg: null,
|
|
84
|
+
fundingVerified,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fetch every venue in `venues` concurrently; failures land in status/errors.
|
|
89
|
+
* Venues whose funding is unverified (VenueDefinition.fundingVerified false — QFEX until its
|
|
90
|
+
* rate unit is confirmed) are DROPPED unless `includeUnverified`: every consumer that ranks,
|
|
91
|
+
* alerts on or trades funding spreads (arb scan, MCP, alerts, dashboard, portfolio) leaves it
|
|
92
|
+
* false. Only explicit, labelled reads pass true (`arb scan --include-unverified`).
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchVenuesRates(venues, net, includeUnverified) {
|
|
95
|
+
venues = venues.filter(v => v.fundingVerified || includeUnverified);
|
|
96
|
+
const settled = await Promise.allSettled(venues.map(v => fetchVenueRates(v, net)));
|
|
97
|
+
const exchangeStatus = {};
|
|
98
|
+
const exchangeErrors = {};
|
|
99
|
+
const rates = [];
|
|
100
|
+
settled.forEach((res, i) => {
|
|
101
|
+
const id = venues[i].id;
|
|
102
|
+
if (res.status === "fulfilled") {
|
|
103
|
+
exchangeStatus[id] = "ok";
|
|
104
|
+
rates.push(...res.value);
|
|
120
105
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!rawSymbol.endsWith("USDT"))
|
|
125
|
-
continue;
|
|
126
|
-
const fundingRate = Number(p.lastFundingRate);
|
|
127
|
-
// Prefer premiumIndex.markPrice; fall back to ticker.lastPrice as a
|
|
128
|
-
// documented price-source preference (NOT an error fallback).
|
|
129
|
-
const markFromPremium = Number(p.markPrice);
|
|
130
|
-
const markPrice = Number.isFinite(markFromPremium) && markFromPremium > 0
|
|
131
|
-
? markFromPremium
|
|
132
|
-
: priceMap.get(rawSymbol);
|
|
133
|
-
if (!Number.isFinite(fundingRate) || markPrice === undefined || !Number.isFinite(markPrice) || markPrice <= 0) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const symbol = rawSymbol.replace(/USDT$/, "");
|
|
137
|
-
const nextFundingTimeRaw = Number(p.nextFundingTime);
|
|
138
|
-
out.push({
|
|
139
|
-
exchange: "aster",
|
|
140
|
-
symbol,
|
|
141
|
-
fundingRate,
|
|
142
|
-
hourlyRate: toHourlyRate(fundingRate, "aster"),
|
|
143
|
-
annualizedPct: annualizeRate(fundingRate, "aster"),
|
|
144
|
-
markPrice,
|
|
145
|
-
nextFundingTime: Number.isFinite(nextFundingTimeRaw) ? nextFundingTimeRaw : 0,
|
|
146
|
-
});
|
|
106
|
+
else {
|
|
107
|
+
exchangeStatus[id] = "error";
|
|
108
|
+
exchangeErrors[id] = errorMessage(res.reason);
|
|
147
109
|
}
|
|
148
|
-
|
|
110
|
+
});
|
|
111
|
+
return { rates, exchangeStatus, exchangeErrors };
|
|
112
|
+
}
|
|
113
|
+
const _warnedPriceMismatch = new Set();
|
|
114
|
+
/**
|
|
115
|
+
* Same canonical ticker on two venues is not proof of the same asset (e.g. two
|
|
116
|
+
* unrelated tokens both listed as "PRL"). Within a canonical group, drop any
|
|
117
|
+
* venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
|
|
118
|
+
* group median, with a one-time stderr warning per (symbol, venue).
|
|
119
|
+
*/
|
|
120
|
+
function dropPriceOutliers(canonical, list) {
|
|
121
|
+
if (list.length < 2)
|
|
122
|
+
return list;
|
|
123
|
+
const sorted = list.map(r => r.markPrice).sort((x, y) => x - y);
|
|
124
|
+
const mid = sorted.length % 2 === 1
|
|
125
|
+
? sorted[(sorted.length - 1) / 2]
|
|
126
|
+
: (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2;
|
|
127
|
+
if (!(mid > 0))
|
|
128
|
+
return list;
|
|
129
|
+
return list.filter(r => {
|
|
130
|
+
const devPct = Math.abs(r.markPrice - mid) / mid * 100;
|
|
131
|
+
if (devPct <= MAX_PRICE_DEVIATION_PCT)
|
|
132
|
+
return true;
|
|
133
|
+
const key = `${canonical}:${r.exchange}`;
|
|
134
|
+
if (!_warnedPriceMismatch.has(key)) {
|
|
135
|
+
_warnedPriceMismatch.add(key);
|
|
136
|
+
process.stderr.write(`[funding] excluded ${r.exchange} ${canonical}: mark ${r.markPrice} deviates ${devPct.toFixed(1)}% from cross-venue median ${mid} (likely a different asset with the same ticker)\n`);
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function groupByCanonical(rates, symbols) {
|
|
142
|
+
const out = new Map();
|
|
143
|
+
for (const r of rates) {
|
|
144
|
+
if (symbols && !symbols.includes(r.canonical))
|
|
145
|
+
continue;
|
|
146
|
+
const list = out.get(r.canonical);
|
|
147
|
+
if (list)
|
|
148
|
+
list.push(r);
|
|
149
|
+
else
|
|
150
|
+
out.set(r.canonical, [r]);
|
|
149
151
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
for (const [canonical, list] of [...out]) {
|
|
153
|
+
const kept = dropPriceOutliers(canonical, list);
|
|
154
|
+
if (kept.length === 0)
|
|
155
|
+
out.delete(canonical); // every venue disagreed — not comparable
|
|
156
|
+
else
|
|
157
|
+
out.set(canonical, kept);
|
|
152
158
|
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function bestMarkPrice(rates) {
|
|
162
|
+
const rank = (id) => MARK_PRICE_PREFERENCE.indexOf(id);
|
|
163
|
+
return [...rates].sort((a, b) => rank(a.exchange) - rank(b.exchange))[0].markPrice;
|
|
153
164
|
}
|
|
154
165
|
// ── Core comparison logic ──
|
|
155
166
|
/**
|
|
156
|
-
* Fetch funding rates from
|
|
167
|
+
* Fetch funding rates from every market-data venue in parallel, normalize, and compare.
|
|
157
168
|
* Returns rates sorted by max spread (descending).
|
|
169
|
+
* @param opts.network defaults to the network the CLI selected (setSharedApiNetwork)
|
|
158
170
|
*/
|
|
159
171
|
export async function fetchAllFundingRates(opts) {
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
]);
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (!r.symbol)
|
|
172
|
-
continue;
|
|
173
|
-
if (opts?.symbols && !opts.symbols.includes(r.symbol.toUpperCase()))
|
|
174
|
-
continue;
|
|
175
|
-
const key = r.symbol.toUpperCase();
|
|
176
|
-
if (!rateMap.has(key))
|
|
177
|
-
rateMap.set(key, []);
|
|
178
|
-
rateMap.get(key).push(r);
|
|
179
|
-
}
|
|
180
|
-
// Attach historical averages when available
|
|
181
|
-
try {
|
|
182
|
-
const symbols = Array.from(rateMap.keys());
|
|
183
|
-
const exchanges = ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
184
|
-
const historicals = getHistoricalAverages(symbols, exchanges);
|
|
185
|
-
for (const [, rates] of rateMap) {
|
|
186
|
-
for (const r of rates) {
|
|
187
|
-
const key = `${r.symbol.toUpperCase()}:${r.exchange}`;
|
|
188
|
-
const avg = historicals.get(key);
|
|
189
|
-
if (avg)
|
|
190
|
-
r.historicalAvg = avg;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
// Non-critical
|
|
172
|
+
ensureVenuesRegistered();
|
|
173
|
+
const net = opts?.network ?? sharedApiNetwork();
|
|
174
|
+
const { rates: allRates, exchangeStatus, exchangeErrors } = await fetchVenuesRates(listVenuesWithRole("marketData"), net, opts?.includeUnverified === true);
|
|
175
|
+
const rateMap = groupByCanonical(allRates, opts?.symbols);
|
|
176
|
+
// Attach historical averages (keyed by venue symbol + venue, as saved by saveFundingSnapshot)
|
|
177
|
+
const grouped = [...rateMap.values()].flat();
|
|
178
|
+
const historicals = getHistoricalAverages([...new Set(grouped.map(r => r.symbol.toUpperCase()))], [...new Set(grouped.map(r => r.exchange))]);
|
|
179
|
+
for (const r of grouped) {
|
|
180
|
+
const avg = historicals.get(`${r.symbol.toUpperCase()}:${r.exchange}`);
|
|
181
|
+
if (avg)
|
|
182
|
+
r.historicalAvg = avg;
|
|
196
183
|
}
|
|
197
184
|
const comparisons = [];
|
|
198
185
|
const minSpread = opts?.minSpread ?? 0;
|
|
@@ -204,18 +191,24 @@ export async function fetchAllFundingRates(opts) {
|
|
|
204
191
|
rates.sort((a, b) => a.hourlyRate - b.hourlyRate);
|
|
205
192
|
const lowest = rates[0];
|
|
206
193
|
const highest = rates[rates.length - 1];
|
|
207
|
-
|
|
194
|
+
// The median filter alone lets a 2-venue group through at up to 2 × MAX_PRICE_DEVIATION_PCT
|
|
195
|
+
// apart (each side deviates half the gap): the CHOSEN pair must itself agree on price.
|
|
196
|
+
const pairGapPct = Math.abs(lowest.markPrice - highest.markPrice) / ((lowest.markPrice + highest.markPrice) / 2) * 100;
|
|
197
|
+
if (!(pairGapPct <= MAX_PRICE_DEVIATION_PCT)) {
|
|
198
|
+
const key = `${symbol}:${lowest.exchange}:${highest.exchange}`;
|
|
199
|
+
if (!_warnedPriceMismatch.has(key)) {
|
|
200
|
+
_warnedPriceMismatch.add(key);
|
|
201
|
+
process.stderr.write(`[funding] excluded ${symbol} ${lowest.exchange}↔${highest.exchange}: pair mark gap ${pairGapPct.toFixed(1)}% > ${MAX_PRICE_DEVIATION_PCT}%\n`);
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const maxSpreadAnnual = annualizeHourlyRate(highest.hourlyRate - lowest.hourlyRate);
|
|
208
206
|
if (maxSpreadAnnual < minSpread)
|
|
209
207
|
continue;
|
|
210
|
-
// Best mark price: prefer HL (most liquid), then PAC, then LT
|
|
211
|
-
const hlRate = rates.find(r => r.exchange === "hyperliquid");
|
|
212
|
-
const pacRate = rates.find(r => r.exchange === "pacifica");
|
|
213
|
-
const ltRate = rates.find(r => r.exchange === "lighter");
|
|
214
|
-
const bestMarkPrice = hlRate?.markPrice || pacRate?.markPrice || ltRate?.markPrice || 0;
|
|
215
208
|
// Estimate hourly income for $1000 notional arb
|
|
216
209
|
const notional = 1000;
|
|
217
|
-
const longIncome =
|
|
218
|
-
const shortIncome =
|
|
210
|
+
const longIncome = estimateHourlyFundingFromHourly(lowest.hourlyRate, notional, "long");
|
|
211
|
+
const shortIncome = estimateHourlyFundingFromHourly(highest.hourlyRate, notional, "short");
|
|
219
212
|
const estHourlyIncomeUsd = -(longIncome + shortIncome); // negate because income = -cost
|
|
220
213
|
comparisons.push({
|
|
221
214
|
symbol,
|
|
@@ -223,7 +216,7 @@ export async function fetchAllFundingRates(opts) {
|
|
|
223
216
|
maxSpreadAnnual,
|
|
224
217
|
longExchange: lowest.exchange, // long where funding is lowest
|
|
225
218
|
shortExchange: highest.exchange, // short where funding is highest
|
|
226
|
-
bestMarkPrice,
|
|
219
|
+
bestMarkPrice: bestMarkPrice(rates),
|
|
227
220
|
estHourlyIncomeUsd,
|
|
228
221
|
});
|
|
229
222
|
}
|
|
@@ -233,19 +226,20 @@ export async function fetchAllFundingRates(opts) {
|
|
|
233
226
|
timestamp: new Date().toISOString(),
|
|
234
227
|
symbols: comparisons,
|
|
235
228
|
exchangeStatus,
|
|
229
|
+
exchangeErrors,
|
|
236
230
|
};
|
|
237
231
|
}
|
|
238
232
|
/**
|
|
239
|
-
* Fetch rates for a single symbol across all exchanges.
|
|
233
|
+
* Fetch rates for a single (canonical) symbol across all exchanges.
|
|
240
234
|
*/
|
|
241
|
-
export async function fetchSymbolFundingRates(symbol) {
|
|
242
|
-
const snapshot = await fetchAllFundingRates({ symbols: [symbol.toUpperCase()] });
|
|
235
|
+
export async function fetchSymbolFundingRates(symbol, includeUnverified = false) {
|
|
236
|
+
const snapshot = await fetchAllFundingRates({ symbols: [symbol.toUpperCase()], includeUnverified });
|
|
243
237
|
return snapshot.symbols[0] ?? null;
|
|
244
238
|
}
|
|
245
239
|
/**
|
|
246
240
|
* Fetch spot+perp funding rate spreads.
|
|
247
241
|
* Since spot has 0 funding cost, the spread is simply |perp funding rate|.
|
|
248
|
-
* Only includes symbols available on at least one spot
|
|
242
|
+
* Only includes symbols available on at least one bot spot venue (def.spot, §12.4 #3).
|
|
249
243
|
*
|
|
250
244
|
* Key safety checks:
|
|
251
245
|
* - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
|
|
@@ -253,112 +247,67 @@ export async function fetchSymbolFundingRates(symbol) {
|
|
|
253
247
|
* (filters out same-ticker-different-token issues like HIP-1 TRUMP ≠ perp TRUMP)
|
|
254
248
|
*/
|
|
255
249
|
export async function fetchSpotPerpSpreads(opts) {
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const ctx = ctxs[i];
|
|
290
|
-
const spotPrice = Number(ctx?.midPx ?? ctx?.markPx ?? 0);
|
|
291
|
-
// Map to perp symbol: UBTC→BTC, or direct if no mapping
|
|
292
|
-
const perpSymbol = SPOT_PERP_TOKEN_MAP[base] ?? base;
|
|
293
|
-
// Only keep the best-priced entry per perp symbol (prefer higher-volume)
|
|
294
|
-
if (!hlSpotByPerp.has(perpSymbol) || spotPrice > 0) {
|
|
295
|
-
hlSpotByPerp.set(perpSymbol, base);
|
|
296
|
-
if (spotPrice > 0)
|
|
297
|
-
hlSpotPrices.set(perpSymbol, spotPrice);
|
|
250
|
+
// Spot legs come from the venue definitions (def.spot, §12): only bot/arb spot venues
|
|
251
|
+
// (separate/runtime account + transfer, §12.4 #3). Perp legs: those venues + pacifica.
|
|
252
|
+
ensureVenuesRegistered();
|
|
253
|
+
const net = opts?.network ?? sharedApiNetwork();
|
|
254
|
+
const { listBotSpotVenues, requireSpotAdapter, defaultSpotQuote } = await import("../exchanges/spot-factory.js");
|
|
255
|
+
const spotVenues = listBotSpotVenues();
|
|
256
|
+
const perpVenueIds = new Set(["hyperliquid", "lighter", "pacifica", ...spotVenues.map(v => v.id)]);
|
|
257
|
+
const { rates: perpRates, exchangeStatus, exchangeErrors } = await fetchVenuesRates([...perpVenueIds].map(id => getVenue(id)), net, false);
|
|
258
|
+
// Spot availability + prices per venue: perpSymbol → candidate spot prices (U-tokens map
|
|
259
|
+
// UBTC→BTC; several spot tokens may claim one perp symbol). A failure is reported as
|
|
260
|
+
// "<venue>-spot" in status/errors.
|
|
261
|
+
const spotPricesByVenue = new Map();
|
|
262
|
+
await Promise.all(spotVenues.map(async (def) => {
|
|
263
|
+
try {
|
|
264
|
+
if (def.create === null)
|
|
265
|
+
throw new Error(`${def.id} has no adapter`);
|
|
266
|
+
const perp = await def.create({ network: net, credentials: READ_ONLY_CREDENTIALS, noAgent: true, hlDex: null, readOnly: true, noBuilder: true });
|
|
267
|
+
const spot = await requireSpotAdapter(def.id, perp);
|
|
268
|
+
const quote = defaultSpotQuote(def, net);
|
|
269
|
+
const byPerp = new Map();
|
|
270
|
+
for (const m of await spot.getSpotMarkets()) {
|
|
271
|
+
if (m.quoteToken.toUpperCase() !== quote || m.markPrice === null)
|
|
272
|
+
continue; // null = no price → cannot be validated
|
|
273
|
+
const px = Number(m.markPrice);
|
|
274
|
+
if (!Number.isFinite(px) || px <= 0)
|
|
275
|
+
continue; // no price → cannot be validated → not a candidate
|
|
276
|
+
const base = m.baseToken.toUpperCase();
|
|
277
|
+
const perpSymbol = SPOT_PERP_TOKEN_MAP[base] ?? base;
|
|
278
|
+
const list = byPerp.get(perpSymbol);
|
|
279
|
+
if (list)
|
|
280
|
+
list.push(px);
|
|
281
|
+
else
|
|
282
|
+
byPerp.set(perpSymbol, [px]);
|
|
298
283
|
}
|
|
284
|
+
spotPricesByVenue.set(def.id, byPerp);
|
|
285
|
+
exchangeStatus[`${def.id}-spot`] = "ok";
|
|
299
286
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// LT: explorer API → symbols with "/"
|
|
304
|
-
const ltMarkets = await fetch("https://explorer.elliot.ai/api/markets")
|
|
305
|
-
.then(r => r.json());
|
|
306
|
-
for (const m of ltMarkets) {
|
|
307
|
-
if (m.symbol.includes("/")) {
|
|
308
|
-
ltSpotSymbols.add(m.symbol.split("/")[0].toUpperCase());
|
|
309
|
-
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
exchangeStatus[`${def.id}-spot`] = "error";
|
|
289
|
+
exchangeErrors[`${def.id}-spot`] = errorMessage(e);
|
|
310
290
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const book = await fetch(`https://mainnet.zklighter.elliot.ai/api/v1/orderBookOrders?market_id=${
|
|
315
|
-
// We don't have market_id here, skip detailed price fetch for LT
|
|
316
|
-
// LT tokens (ETH, LINK, etc.) are direct names, low risk of mismatch
|
|
317
|
-
""}`);
|
|
318
|
-
// Skip — LT uses direct token names, not indices
|
|
319
|
-
}
|
|
320
|
-
catch { /* skip */ }
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
catch { /* non-critical */ }
|
|
324
|
-
// Merge all perp rates by symbol
|
|
325
|
-
const perpRateMap = new Map();
|
|
326
|
-
for (const r of [...hlRates, ...ltRates, ...pacRates]) {
|
|
327
|
-
if (!r.symbol)
|
|
328
|
-
continue;
|
|
329
|
-
if (opts?.symbols && !opts.symbols.includes(r.symbol.toUpperCase()))
|
|
330
|
-
continue;
|
|
331
|
-
const key = r.symbol.toUpperCase();
|
|
332
|
-
if (!perpRateMap.has(key))
|
|
333
|
-
perpRateMap.set(key, []);
|
|
334
|
-
perpRateMap.get(key).push(r);
|
|
335
|
-
}
|
|
291
|
+
}));
|
|
292
|
+
// Merge all perp rates by canonical symbol
|
|
293
|
+
const perpRateMap = groupByCanonical(perpRates, opts?.symbols);
|
|
336
294
|
const spreads = [];
|
|
337
295
|
const minSpread = opts?.minSpread ?? 0;
|
|
338
296
|
for (const [symbol, rates] of perpRateMap) {
|
|
339
297
|
rates.sort((a, b) => Math.abs(b.hourlyRate) - Math.abs(a.hourlyRate));
|
|
340
298
|
const bestPerp = rates[0];
|
|
341
|
-
// Spot exchanges: only include if symbol has a
|
|
299
|
+
// Spot exchanges: only include if the symbol has a spot market whose price is within
|
|
300
|
+
// MAX_PRICE_DEVIATION_PCT of the perp mark (same ticker, different token — e.g. HIP-1
|
|
301
|
+
// TRUMP ≠ perp TRUMP — is skipped).
|
|
342
302
|
const uniqueSpot = [];
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
if (deviation <= MAX_PRICE_DEVIATION_PCT) {
|
|
351
|
-
uniqueSpot.push("hyperliquid");
|
|
352
|
-
}
|
|
353
|
-
// else: same ticker, different token (e.g., HIP-1 TRUMP ≠ perp TRUMP) → skip
|
|
354
|
-
}
|
|
355
|
-
else if (spotPrice === undefined) {
|
|
356
|
-
// No price data (empty orderbook) → skip, can't verify
|
|
357
|
-
}
|
|
303
|
+
const perpPrice = bestPerp.markPrice;
|
|
304
|
+
for (const [venueId, byPerp] of spotPricesByVenue) {
|
|
305
|
+
const prices = byPerp.get(symbol);
|
|
306
|
+
if (!prices || !(perpPrice > 0))
|
|
307
|
+
continue;
|
|
308
|
+
if (prices.some(px => Math.abs(px - perpPrice) / perpPrice * 100 <= MAX_PRICE_DEVIATION_PCT))
|
|
309
|
+
uniqueSpot.push(venueId);
|
|
358
310
|
}
|
|
359
|
-
// LT spot: direct token names, low mismatch risk
|
|
360
|
-
if (ltSpotSymbols.has(symbol))
|
|
361
|
-
uniqueSpot.push("lighter");
|
|
362
311
|
if (uniqueSpot.length === 0)
|
|
363
312
|
continue;
|
|
364
313
|
const annualSpreadPct = Math.abs(bestPerp.annualizedPct);
|
|
@@ -385,5 +334,7 @@ export async function fetchSpotPerpSpreads(opts) {
|
|
|
385
334
|
return {
|
|
386
335
|
timestamp: new Date().toISOString(),
|
|
387
336
|
spreads,
|
|
337
|
+
exchangeStatus,
|
|
338
|
+
exchangeErrors,
|
|
388
339
|
};
|
|
389
340
|
}
|
|
File without changes
|