perp-cli 0.14.3 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +138 -405
- package/README.zh.md +243 -0
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +85 -15
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +12 -5
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
- package/dist/bot/strategies/funding-arb-utils.js +53 -20
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +3 -3
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +126 -6
- package/dist/commands/account.js +168 -24
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +23 -81
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +175 -97
- package/dist/commands/arb.d.ts +20 -0
- package/dist/commands/arb.js +107 -163
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +395 -100
- package/dist/commands/options.d.ts +14 -0
- package/dist/commands/options.js +151 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +455 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +83 -55
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +67 -62
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +3 -3
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +35 -3
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +16 -12
- package/dist/exchanges/arcus/adapter.js +45 -18
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +30 -10
- package/dist/exchanges/aster.js +153 -53
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +8 -2
- package/dist/exchanges/canonical-symbols.js +46 -2
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +73 -0
- package/dist/exchanges/derive/options.js +379 -0
- package/dist/exchanges/extended/adapter.d.ts +96 -0
- package/dist/exchanges/extended/adapter.js +241 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +133 -0
- package/dist/exchanges/extended/public.js +436 -0
- package/dist/exchanges/grvt/adapter.d.ts +100 -0
- package/dist/exchanges/grvt/adapter.js +283 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
- package/dist/exchanges/hyperliquid-spot.js +24 -16
- package/dist/exchanges/hyperliquid.d.ts +199 -22
- package/dist/exchanges/hyperliquid.js +550 -202
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +157 -13
- package/dist/exchanges/interface.js +47 -0
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +67 -12
- package/dist/exchanges/lighter-public.js +250 -82
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +10 -5
- package/dist/exchanges/lighter-spot.js +105 -97
- package/dist/exchanges/lighter.d.ts +80 -21
- package/dist/exchanges/lighter.js +312 -105
- package/dist/exchanges/ondo/adapter.d.ts +38 -13
- package/dist/exchanges/ondo/adapter.js +86 -27
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +35 -3
- package/dist/exchanges/ondo/public.js +75 -32
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +8 -2
- package/dist/exchanges/pacifica-spot.js +26 -18
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +130 -0
- package/dist/exchanges/pacifica-vaults.js +277 -0
- package/dist/exchanges/pacifica.d.ts +108 -10
- package/dist/exchanges/pacifica.js +428 -171
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +69 -13
- package/dist/exchanges/qfex/adapter.js +102 -17
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +65 -12
- package/dist/exchanges/qfex/public.js +131 -25
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +160 -0
- package/dist/exchanges/risex/adapter.js +842 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +8 -8
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +61 -0
- package/dist/exchanges/txflow/adapter.js +150 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +155 -0
- package/dist/exchanges/txflow/public.d.ts +87 -0
- package/dist/exchanges/txflow/public.js +355 -0
- package/dist/exchanges/unknown-symbol.d.ts +34 -0
- package/dist/exchanges/unknown-symbol.js +56 -0
- package/dist/exchanges/variational/adapter.d.ts +67 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +52 -10
- package/dist/exchanges/venue-types.js +82 -11
- package/dist/exchanges/venue.d.ts +150 -11
- package/dist/exchanges/venue.js +126 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +19 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +20 -8
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +18 -6
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +15 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +131 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +6 -4
- package/dist/funding/legacy-public.js +42 -41
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +95 -6
- package/dist/funding/rates.js +99 -19
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +64 -0
- package/dist/mcp-server.js +211 -68
- package/dist/pacifica/client.d.ts +69 -5
- package/dist/pacifica/client.js +181 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +36 -63
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +22 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +980 -0
- package/dist/ws/index.js +4460 -0
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +996 -0
- package/dist/ws/node.js +4554 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +71 -10
- package/skills/perp-cli/SKILL.md +153 -36
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +22 -12
- package/skills/perp-cli/references/commands.md +143 -17
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +523 -0
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public market data for the legacy venues without a native `getVenueMarkets`
|
|
3
|
-
* (pacifica, hyperliquid core, aster)
|
|
3
|
+
* (pacifica, hyperliquid core, aster). Lighter uses
|
|
4
4
|
* exchanges/lighter-public.ts.
|
|
5
5
|
*
|
|
6
6
|
* Network-aware: the REST base comes from `legacyPublicBaseUrl(venue, net)`,
|
|
@@ -19,23 +19,26 @@
|
|
|
19
19
|
* pacifica: crypto-centric; PACIFICA_NON_CRYPTO lists every non-crypto perp
|
|
20
20
|
* listed on 2026-09-30.
|
|
21
21
|
* aster: exchangeInfo `underlyingSubType` (STOCK / ETF / Commodities / pre-launch).
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* canonical
|
|
22
|
+
* Ambiguous tickers (UNCLASSIFIED_TICKERS, src/ws/unclassified.ts — SPX = the SPX6900
|
|
23
|
+
* token, whose ticker equals the S&P 500 canonical key) are listed as "unclassified"
|
|
24
|
+
* (canonical null) so they cannot produce a false cross-venue match.
|
|
25
25
|
*
|
|
26
26
|
* Fields a venue does not publish on this path are null (HL has no fixed tick
|
|
27
27
|
* size; Aster leverage brackets need a signed call; Aster OI is per-symbol only).
|
|
28
28
|
*/
|
|
29
29
|
import { HL_MIN_ORDER_VALUE_USD } from "../constants.js";
|
|
30
|
+
import { isPacificaPerpetual } from "../ws/pacifica-instrument.js";
|
|
31
|
+
import { asterCliSymbol, asterMatchable, isAsterListedPerpetual } from "../ws/aster-market.js";
|
|
32
|
+
import { isUnclassifiedTicker } from "../ws/unclassified.js";
|
|
30
33
|
import { PerpError } from "../errors.js";
|
|
31
34
|
import { venueRateString } from "../utils/numeric.js";
|
|
32
35
|
import { assertOk } from "../api/public/_http.js";
|
|
33
36
|
import { legacyPublicBaseUrl } from "../api/public/urls.js";
|
|
34
37
|
import { canonicalFor } from "../exchanges/canonical-symbols.js";
|
|
35
38
|
import { fundingBasisOf, settlementIntervalOf } from "../exchanges/venue.js";
|
|
39
|
+
import { nextTopOfHourMs } from "../exchanges/venue-types.js";
|
|
36
40
|
// ── Classification ──
|
|
37
|
-
|
|
38
|
-
/** Pacifica non-crypto perps (live /info 2026-09-30). BP / CHIP: ticker collides with a listed equity/ETF. */
|
|
41
|
+
/** Pacifica non-crypto perps (live /info 2026-09-30). Ambiguous BP / CHIP / SPX: UNCLASSIFIED_TICKERS. */
|
|
39
42
|
const PACIFICA_NON_CRYPTO = {
|
|
40
43
|
NVDA: "equity", TSLA: "equity", GOOGL: "equity", PLTR: "equity", HOOD: "equity", CRCL: "equity",
|
|
41
44
|
SPCX: "equity", SKHYNIX: "equity", SAMSUNG: "equity", MSTR: "equity", MU: "equity", SNDK: "equity",
|
|
@@ -46,19 +49,20 @@ const PACIFICA_NON_CRYPTO = {
|
|
|
46
49
|
USDJPY: "fx", EURUSD: "fx",
|
|
47
50
|
SP500: "index",
|
|
48
51
|
};
|
|
49
|
-
|
|
52
|
+
// Ambiguous tickers are listed as "unclassified" (canonical null: never matched across venues),
|
|
53
|
+
// so every listing path (CLI, MCP, public directory) shows the same market set.
|
|
50
54
|
function classifyPacifica(symbol) {
|
|
51
55
|
const up = symbol.toUpperCase();
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
56
|
+
if (isUnclassifiedTicker("pacifica", up))
|
|
57
|
+
return "unclassified";
|
|
54
58
|
return PACIFICA_NON_CRYPTO[up] ?? "crypto"; // crypto-centric venue: non-crypto is listed explicitly
|
|
55
59
|
}
|
|
56
60
|
function classifyHyperliquid(symbol) {
|
|
57
|
-
return
|
|
61
|
+
return isUnclassifiedTicker("hyperliquid", symbol) ? "unclassified" : "crypto";
|
|
58
62
|
}
|
|
59
63
|
function classifyAster(symbol, subTypes) {
|
|
60
|
-
if (
|
|
61
|
-
return
|
|
64
|
+
if (isUnclassifiedTicker("aster", symbol))
|
|
65
|
+
return "unclassified";
|
|
62
66
|
if (subTypes.includes("pre-launch"))
|
|
63
67
|
return "preIpo";
|
|
64
68
|
if (subTypes.includes("ETF"))
|
|
@@ -75,6 +79,10 @@ function warnExcluded(venue, what, symbols) {
|
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
// ── HTTP ──
|
|
82
|
+
/** One public JSON read with the venue error mapping (rate limits, HTTP status, invalid JSON). */
|
|
83
|
+
export async function legacyPublicJson(venue, url, label, init) {
|
|
84
|
+
return getJson(venue, url, label, init);
|
|
85
|
+
}
|
|
78
86
|
async function getJson(venue, url, label, init) {
|
|
79
87
|
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(15_000) });
|
|
80
88
|
await assertOk(res, `${venue} ${label}`);
|
|
@@ -124,21 +132,16 @@ async function pacificaRows(net) {
|
|
|
124
132
|
const basis = fundingBasisOf("pacifica");
|
|
125
133
|
const settlement = settlementIntervalOf("pacifica");
|
|
126
134
|
const out = [];
|
|
127
|
-
const unclassified = [];
|
|
128
135
|
const unpriced = [];
|
|
129
136
|
for (const m of info) {
|
|
130
|
-
// /info lists spot markets too (e.g. SOL-USDC). A row without the field cannot be classified.
|
|
137
|
+
// /info lists spot markets too (e.g. SOL-USDC): perps only. A row without the field cannot be classified.
|
|
131
138
|
if (typeof m.instrument_type !== "string") {
|
|
132
139
|
throw new PerpError("EXCHANGE_ERROR", `Pacifica /info row ${String(m.symbol)} has no instrument_type`, { exchange: "pacifica" });
|
|
133
140
|
}
|
|
134
|
-
if (m
|
|
141
|
+
if (!isPacificaPerpetual(m))
|
|
135
142
|
continue;
|
|
136
143
|
const symbol = String(m.symbol ?? "");
|
|
137
144
|
const assetClass = classifyPacifica(symbol);
|
|
138
|
-
if (assetClass === null) {
|
|
139
|
-
unclassified.push(symbol);
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
145
|
const p = priceBySymbol.get(symbol);
|
|
143
146
|
const mark = p ? positiveNumStr(p.mark) : null;
|
|
144
147
|
if (!p || mark === null) {
|
|
@@ -157,7 +160,9 @@ async function pacificaRows(net) {
|
|
|
157
160
|
quoteAsset: null,
|
|
158
161
|
markPrice: mark,
|
|
159
162
|
indexPrice: positiveNumStr(p.oracle),
|
|
160
|
-
fundingRate
|
|
163
|
+
// Last settled hourly rate (WS prices `funding`, the CLI row's fundingRate); next_funding
|
|
164
|
+
// is the predicted one.
|
|
165
|
+
fundingRate: rateStr(p.funding),
|
|
161
166
|
fundingRateBasis: basis,
|
|
162
167
|
settlementIntervalHours: settlement,
|
|
163
168
|
openInterest: numStr(p.open_interest),
|
|
@@ -171,10 +176,9 @@ async function pacificaRows(net) {
|
|
|
171
176
|
session: null,
|
|
172
177
|
tradable: true,
|
|
173
178
|
},
|
|
174
|
-
nextFundingTime:
|
|
179
|
+
nextFundingTime: nextTopOfHourMs(), // not published; docs: settled at the end of each 1 h interval (on the hour)
|
|
175
180
|
});
|
|
176
181
|
}
|
|
177
|
-
warnExcluded("pacifica", "unclassified/ambiguous market(s)", unclassified);
|
|
178
182
|
warnExcluded("pacifica", "market(s) without a mark price", unpriced);
|
|
179
183
|
return out;
|
|
180
184
|
}
|
|
@@ -197,16 +201,15 @@ async function hyperliquidRows(net) {
|
|
|
197
201
|
const basis = fundingBasisOf("hyperliquid");
|
|
198
202
|
const settlement = settlementIntervalOf("hyperliquid");
|
|
199
203
|
const out = [];
|
|
200
|
-
const unclassified = [];
|
|
201
204
|
const unpriced = [];
|
|
202
205
|
universe.forEach((a, i) => {
|
|
206
|
+
// Delisted assets stay in meta (isDelisted: true) with zeroed funding / volume / OI
|
|
207
|
+
// (live 2026-10-14: 56 of 234) — not a market: excluded from listings, prices and scans.
|
|
208
|
+
if (a.isDelisted === true)
|
|
209
|
+
return;
|
|
203
210
|
const ctx = ctxs[i];
|
|
204
211
|
const symbol = String(a.name ?? "");
|
|
205
212
|
const assetClass = classifyHyperliquid(symbol);
|
|
206
|
-
if (assetClass === null) {
|
|
207
|
-
unclassified.push(symbol);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
213
|
const mark = positiveNumStr(ctx.markPx);
|
|
211
214
|
if (mark === null) {
|
|
212
215
|
unpriced.push(symbol);
|
|
@@ -237,12 +240,11 @@ async function hyperliquidRows(net) {
|
|
|
237
240
|
maxLeverage: Number.isFinite(maxLeverage) && maxLeverage > 0 ? maxLeverage : null,
|
|
238
241
|
marginTiers: null,
|
|
239
242
|
session: null,
|
|
240
|
-
tradable:
|
|
243
|
+
tradable: true, // delisted assets are skipped above
|
|
241
244
|
},
|
|
242
|
-
nextFundingTime:
|
|
245
|
+
nextFundingTime: nextTopOfHourMs(), // not in metaAndAssetCtxs; docs: paid every hour (on the hour)
|
|
243
246
|
});
|
|
244
247
|
});
|
|
245
|
-
warnExcluded("hyperliquid", "unclassified/ambiguous market(s)", unclassified);
|
|
246
248
|
warnExcluded("hyperliquid", "market(s) without a mark price", unpriced);
|
|
247
249
|
return out;
|
|
248
250
|
}
|
|
@@ -271,20 +273,20 @@ async function asterRows(net) {
|
|
|
271
273
|
intervalBySymbol.set(String(f.symbol), h);
|
|
272
274
|
}
|
|
273
275
|
const out = [];
|
|
274
|
-
const unclassified = [];
|
|
275
276
|
const unpriced = [];
|
|
276
277
|
const noInterval = [];
|
|
277
278
|
for (const s of symbols) {
|
|
278
|
-
|
|
279
|
+
// Shared eligibility (SETTLING / PENDING_TRADING rows were listed with tradable false); every
|
|
280
|
+
// quote, like the CLI adapter and the WS directory.
|
|
281
|
+
if (!isAsterListedPerpetual(s))
|
|
279
282
|
continue;
|
|
280
283
|
const venueSymbol = String(s.symbol);
|
|
281
|
-
const symbol = String(s.baseAsset);
|
|
284
|
+
const symbol = asterCliSymbol({ symbol: venueSymbol, baseAsset: String(s.baseAsset), quoteAsset: s.quoteAsset });
|
|
285
|
+
const base = String(s.baseAsset);
|
|
282
286
|
const subTypes = Array.isArray(s.underlyingSubType) ? s.underlyingSubType.map(String) : [];
|
|
283
|
-
const assetClass = classifyAster(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
287
|
+
const assetClass = classifyAster(base, subTypes);
|
|
288
|
+
// A second listing of a base (BTCUSD1 beside BTCUSDT) or a non-USD quote (U): canonical null.
|
|
289
|
+
const matchable = asterMatchable(s);
|
|
288
290
|
const p = premiumBySymbol.get(venueSymbol);
|
|
289
291
|
const mark = p ? positiveNumStr(p.markPrice) : null;
|
|
290
292
|
if (!p || mark === null) {
|
|
@@ -306,10 +308,10 @@ async function asterRows(net) {
|
|
|
306
308
|
venue: "aster",
|
|
307
309
|
venueSymbol,
|
|
308
310
|
symbol,
|
|
309
|
-
canonical: canonicalFor("aster",
|
|
311
|
+
canonical: matchable ? canonicalFor("aster", base, assetClass) : null,
|
|
310
312
|
assetClass,
|
|
311
313
|
settle: s.marginAsset,
|
|
312
|
-
quoteAsset: s.quoteAsset,
|
|
314
|
+
quoteAsset: typeof s.quoteAsset === "string" && s.quoteAsset !== "" ? s.quoteAsset : null,
|
|
313
315
|
markPrice: mark,
|
|
314
316
|
indexPrice: positiveNumStr(p.indexPrice),
|
|
315
317
|
fundingRate: rateStr(p.lastFundingRate),
|
|
@@ -329,7 +331,6 @@ async function asterRows(net) {
|
|
|
329
331
|
nextFundingTime: Number.isFinite(next) && next > 0 ? next : null,
|
|
330
332
|
});
|
|
331
333
|
}
|
|
332
|
-
warnExcluded("aster", "unclassified/ambiguous market(s)", unclassified);
|
|
333
334
|
warnExcluded("aster", "market(s) without a mark price", unpriced);
|
|
334
335
|
warnExcluded("aster", "market(s) missing from fundingInfo", noInterval);
|
|
335
336
|
return out;
|
package/dist/funding/rates.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Real-time cross-venue funding rate comparison.
|
|
3
3
|
*
|
|
4
|
-
* Fans out over every venue with the "marketData" role
|
|
4
|
+
* Fans out over every venue with the "marketData" role using
|
|
5
5
|
* each venue's `publicData.fetchFundingRates(net)`, normalizes every row to a
|
|
6
6
|
* per-hour rate with the row's own basis (Lighter
|
|
7
7
|
* family 8h, Aster per-symbol), and matches markets across venues by
|
|
8
|
-
* `canonical` (null → excluded
|
|
8
|
+
* `canonical` (null → excluded).
|
|
9
9
|
*
|
|
10
10
|
* A venue that fails is reported in `exchangeStatus` ("error") and
|
|
11
11
|
* `exchangeErrors` (message) — its rows are absent, never silently empty.
|
|
12
12
|
*/
|
|
13
13
|
import { type HistoricalAverages } from "./history.js";
|
|
14
14
|
import { type VenueDefinition } from "../exchanges/venue.js";
|
|
15
|
+
import type { FundingRateKind } from "../exchanges/interface.js";
|
|
15
16
|
import { type FundingRateBasis, type Network, type VenueId } from "../exchanges/venue-types.js";
|
|
16
17
|
export interface ExchangeFundingRate {
|
|
17
18
|
exchange: VenueId;
|
|
@@ -28,22 +29,65 @@ export interface ExchangeFundingRate {
|
|
|
28
29
|
historicalAvg: HistoricalAverages | null;
|
|
29
30
|
/** false = the venue's funding rate is unverified (VenueDefinition.fundingVerified) — shown only on explicit request. */
|
|
30
31
|
fundingVerified: boolean;
|
|
32
|
+
/** How the row was read: "unofficial" = an undocumented venue API (VenueDefinition.apiStatus) — may change without notice. */
|
|
33
|
+
apiStatus: "official" | "unofficial";
|
|
34
|
+
/**
|
|
35
|
+
* Which rate `fundingRate` is (the venue market row's rate — arb compares those, never a
|
|
36
|
+
* predicted next-period rate unless the venue only publishes that): "lastSettled" | "accruing" |
|
|
37
|
+
* "predicted", null = not verified (fundingRateKindOf).
|
|
38
|
+
*/
|
|
39
|
+
rateKind: FundingRateKind | null;
|
|
31
40
|
}
|
|
32
41
|
export interface SymbolFundingComparison {
|
|
33
42
|
/** Canonical symbol shared by every row. */
|
|
34
43
|
symbol: string;
|
|
35
44
|
rates: ExchangeFundingRate[];
|
|
45
|
+
/**
|
|
46
|
+
* Ranking is over VERIFIED rows only (VenueDefinition.fundingVerified): an unverified row
|
|
47
|
+
* (`--include-unverified`) is shown in `rates` but is never the long or short leg nor part of
|
|
48
|
+
* the spread. Fewer than two verified rows → no ranked pair: longExchange / shortExchange /
|
|
49
|
+
* maxSpreadAnnual / estHourlyIncomeUsd are null (a display-only row).
|
|
50
|
+
*/
|
|
51
|
+
maxSpreadAnnual: number | null;
|
|
52
|
+
longExchange: string | null;
|
|
53
|
+
shortExchange: string | null;
|
|
54
|
+
bestMarkPrice: number;
|
|
55
|
+
estHourlyIncomeUsd: number | null;
|
|
56
|
+
/**
|
|
57
|
+
* false = a leg of the long/short pair sits on a venue without the trading role (RISEx while its
|
|
58
|
+
* trading is gated in this release): the spread is data only — `arb exec` refuses it.
|
|
59
|
+
*/
|
|
60
|
+
executable: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* true = the row's rates are of different kinds (e.g. Lighter lastSettled vs Hyperliquid
|
|
63
|
+
* accruing) — the perp-ws comparator's `mixedKinds`: the spread compares unlike quantities.
|
|
64
|
+
*/
|
|
65
|
+
mixedKinds: boolean;
|
|
66
|
+
/** Venues of the long/short pair that perp-cli cannot trade (read-only); [] when executable. */
|
|
67
|
+
readOnlyLegs: string[];
|
|
68
|
+
/**
|
|
69
|
+
* Venues of the long/short pair whose funding is unverified (VenueDefinition.fundingVerified
|
|
70
|
+
* false, e.g. QFEX — shown only with --include-unverified, display only); such a pair is never
|
|
71
|
+
* executable. [] when both legs are verified.
|
|
72
|
+
*/
|
|
73
|
+
unverifiedLegs: string[];
|
|
74
|
+
}
|
|
75
|
+
/** A comparison with a ranked pair (≥ 2 verified rows): long / short / spread / income are set. */
|
|
76
|
+
export type RankedFundingComparison = SymbolFundingComparison & {
|
|
36
77
|
maxSpreadAnnual: number;
|
|
37
78
|
longExchange: string;
|
|
38
79
|
shortExchange: string;
|
|
39
|
-
bestMarkPrice: number;
|
|
40
80
|
estHourlyIncomeUsd: number;
|
|
41
|
-
}
|
|
81
|
+
};
|
|
82
|
+
export declare function isRankedComparison(c: SymbolFundingComparison): c is RankedFundingComparison;
|
|
42
83
|
export interface FundingRateSnapshot {
|
|
43
84
|
timestamp: string;
|
|
44
85
|
symbols: SymbolFundingComparison[];
|
|
86
|
+
/** The minSpread applied (annualized %) and how many ranked pairs it hid (spread below it). */
|
|
87
|
+
minSpread: number;
|
|
88
|
+
belowMinSpread: number;
|
|
45
89
|
exchangeStatus: Record<string, "ok" | "error">;
|
|
46
|
-
/** Error message per venue whose status is "error"
|
|
90
|
+
/** Error message per venue whose status is "error". */
|
|
47
91
|
exchangeErrors: Record<string, string>;
|
|
48
92
|
}
|
|
49
93
|
export declare const TOP_SYMBOLS: string[];
|
|
@@ -59,6 +103,51 @@ export declare function fetchVenuesRates(venues: VenueDefinition[], net: Network
|
|
|
59
103
|
exchangeStatus: Record<string, "ok" | "error">;
|
|
60
104
|
exchangeErrors: Record<string, string>;
|
|
61
105
|
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Same canonical ticker on two venues is not proof of the same asset (e.g. two
|
|
108
|
+
* unrelated tokens both listed as "PRL"). Within a canonical group, drop any
|
|
109
|
+
* venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
|
|
110
|
+
* group median, with a one-time stderr warning per (symbol, venue).
|
|
111
|
+
*/
|
|
112
|
+
export declare function dropPriceOutliers<T extends {
|
|
113
|
+
exchange: VenueId;
|
|
114
|
+
markPrice: number;
|
|
115
|
+
}>(canonical: string, list: T[], tag?: string): T[];
|
|
116
|
+
export interface VenuePrice {
|
|
117
|
+
exchange: VenueId;
|
|
118
|
+
/** Venue display symbol. */
|
|
119
|
+
symbol: string;
|
|
120
|
+
canonical: string;
|
|
121
|
+
markPrice: number;
|
|
122
|
+
apiStatus: "official" | "unofficial";
|
|
123
|
+
}
|
|
124
|
+
export interface PriceGroupSnapshot {
|
|
125
|
+
/** canonical → venue prices, after the median price-outlier drop (same guard as the funding scan). */
|
|
126
|
+
groups: Map<string, VenuePrice[]>;
|
|
127
|
+
exchangeStatus: Record<string, "ok" | "error">;
|
|
128
|
+
exchangeErrors: Record<string, string>;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Mark prices of `venues`, grouped by canonical symbol — the grouping and price-outlier guard of
|
|
132
|
+
* the funding comparison, WITHOUT any funding dependency: read from each venue's market directory
|
|
133
|
+
* (publicData.fetchMarkets), so a market with a null funding rate or a venue whose funding is
|
|
134
|
+
* unverified is still priced. Rows without a canonical symbol, not tradable, or without a
|
|
135
|
+
* positive mark are excluded. A venue that fails is reported in exchangeStatus / exchangeErrors.
|
|
136
|
+
*/
|
|
137
|
+
export declare function fetchCanonicalPriceGroups(venues: VenueDefinition[], opts?: {
|
|
138
|
+
symbols?: string[];
|
|
139
|
+
network?: Network;
|
|
140
|
+
tag?: string;
|
|
141
|
+
}): Promise<PriceGroupSnapshot>;
|
|
142
|
+
/**
|
|
143
|
+
* Whether a long/short pair can be traded as a funding arb: both legs need the trading role AND
|
|
144
|
+
* verified funding (an unverified-funding venue is display only — never ranked for trading).
|
|
145
|
+
*/
|
|
146
|
+
export declare function pairExecutability(longExchange: string, shortExchange: string): {
|
|
147
|
+
executable: boolean;
|
|
148
|
+
readOnlyLegs: string[];
|
|
149
|
+
unverifiedLegs: string[];
|
|
150
|
+
};
|
|
62
151
|
/**
|
|
63
152
|
* Fetch funding rates from every market-data venue in parallel, normalize, and compare.
|
|
64
153
|
* Returns rates sorted by max spread (descending).
|
|
@@ -97,7 +186,7 @@ export interface SpotPerpSpread {
|
|
|
97
186
|
/**
|
|
98
187
|
* Fetch spot+perp funding rate spreads.
|
|
99
188
|
* Since spot has 0 funding cost, the spread is simply |perp funding rate|.
|
|
100
|
-
* Only includes symbols available on at least one bot spot venue (def.spot
|
|
189
|
+
* Only includes symbols available on at least one bot spot venue (def.spot).
|
|
101
190
|
*
|
|
102
191
|
* Key safety checks:
|
|
103
192
|
* - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
|
package/dist/funding/rates.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Real-time cross-venue funding rate comparison.
|
|
3
3
|
*
|
|
4
|
-
* Fans out over every venue with the "marketData" role
|
|
4
|
+
* Fans out over every venue with the "marketData" role using
|
|
5
5
|
* each venue's `publicData.fetchFundingRates(net)`, normalizes every row to a
|
|
6
6
|
* per-hour rate with the row's own basis (Lighter
|
|
7
7
|
* family 8h, Aster per-symbol), and matches markets across venues by
|
|
8
|
-
* `canonical` (null → excluded
|
|
8
|
+
* `canonical` (null → excluded).
|
|
9
9
|
*
|
|
10
10
|
* A venue that fails is reported in `exchangeStatus` ("error") and
|
|
11
11
|
* `exchangeErrors` (message) — its rows are absent, never silently empty.
|
|
@@ -14,10 +14,15 @@ import { annualizeHourlyRate, estimateHourlyFundingFromHourly } from "./normaliz
|
|
|
14
14
|
import { getHistoricalAverages } from "./history.js";
|
|
15
15
|
import { SPOT_PERP_TOKEN_MAP, MAX_PRICE_DEVIATION_PCT } from "../exchanges/index.js";
|
|
16
16
|
import { getVenue, listVenuesWithRole, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
|
|
17
|
-
import { rateToHourly } from "../exchanges/venue-types.js";
|
|
17
|
+
import { fundingRateKindOf, rateToHourly } from "../exchanges/venue-types.js";
|
|
18
18
|
import { ensureVenuesRegistered } from "../exchanges/venues/index.js";
|
|
19
19
|
import { sharedApiNetwork } from "../api/public/urls.js";
|
|
20
20
|
import { PerpError } from "../errors.js";
|
|
21
|
+
import { canonicalQuerySymbol } from "../exchanges/canonical-symbols.js";
|
|
22
|
+
export function isRankedComparison(c) {
|
|
23
|
+
// estHourlyIncomeUsd is set together with the pair (null exactly when the pair is).
|
|
24
|
+
return c.longExchange !== null && c.shortExchange !== null && c.maxSpreadAnnual !== null;
|
|
25
|
+
}
|
|
21
26
|
// ── Default top symbols to track ──
|
|
22
27
|
export const TOP_SYMBOLS = [
|
|
23
28
|
"BTC", "ETH", "SOL", "DOGE", "SUI", "AVAX", "LINK", "ARB",
|
|
@@ -26,7 +31,11 @@ export const TOP_SYMBOLS = [
|
|
|
26
31
|
];
|
|
27
32
|
/** Mark-price source preference for `bestMarkPrice` (most liquid first). */
|
|
28
33
|
const MARK_PRICE_PREFERENCE = [
|
|
29
|
-
"hyperliquid", "pacifica", "lighter", "aster", "lighter-rh", "ondo", "arcus", "qfex",
|
|
34
|
+
"hyperliquid", "pacifica", "lighter", "aster", "lighter-rh", "ondo", "arcus", "risex", "qfex", "grvt", "extended",
|
|
35
|
+
// RFQ venue: the mark is the single maker's (OLP) price.
|
|
36
|
+
"variational",
|
|
37
|
+
// Unofficial API: last resort as a mark-price source.
|
|
38
|
+
"txflow",
|
|
30
39
|
];
|
|
31
40
|
// ── Per-venue fetch ──
|
|
32
41
|
function errorMessage(e) {
|
|
@@ -51,20 +60,24 @@ async function fetchVenueRates(def, net) {
|
|
|
51
60
|
for (const r of rows) {
|
|
52
61
|
if (r.canonical === null)
|
|
53
62
|
continue; // not matchable across venues
|
|
63
|
+
// Outside its funding hours (QFEX market_hours) a market has no rate: never ranked, snapshotted
|
|
64
|
+
// (saveFundingSnapshot) or averaged as a 0.
|
|
65
|
+
if (r.rate === null || r.marketClosed === true)
|
|
66
|
+
continue;
|
|
54
67
|
const markRaw = r.markPrice !== null ? r.markPrice : markBySymbol?.get(r.venueSymbol);
|
|
55
68
|
const markPrice = markRaw === undefined ? NaN : Number(markRaw);
|
|
56
69
|
if (!Number.isFinite(markPrice) || markPrice <= 0) {
|
|
57
70
|
unpriced.push(r.symbol);
|
|
58
71
|
continue;
|
|
59
72
|
}
|
|
60
|
-
out.push(toExchangeRate(r, r.canonical, markPrice, def.fundingVerified));
|
|
73
|
+
out.push(toExchangeRate({ ...r, rate: r.rate }, r.canonical, markPrice, def.fundingVerified, def.apiStatus.kind));
|
|
61
74
|
}
|
|
62
75
|
if (unpriced.length > 0) {
|
|
63
76
|
console.warn(`[${def.id}] excluded ${unpriced.length} funding row(s) without a mark price: ${unpriced.join(", ")}`);
|
|
64
77
|
}
|
|
65
78
|
return out;
|
|
66
79
|
}
|
|
67
|
-
function toExchangeRate(r, canonical, markPrice, fundingVerified) {
|
|
80
|
+
function toExchangeRate(r, canonical, markPrice, fundingVerified, apiStatus) {
|
|
68
81
|
const fundingRate = Number(r.rate);
|
|
69
82
|
if (!Number.isFinite(fundingRate)) {
|
|
70
83
|
throw new PerpError("EXCHANGE_ERROR", `${r.venue} ${r.symbol}: non-numeric funding rate "${r.rate}"`);
|
|
@@ -82,6 +95,8 @@ function toExchangeRate(r, canonical, markPrice, fundingVerified) {
|
|
|
82
95
|
nextFundingTime: r.nextFundingTime,
|
|
83
96
|
historicalAvg: null,
|
|
84
97
|
fundingVerified,
|
|
98
|
+
apiStatus,
|
|
99
|
+
rateKind: fundingRateKindOf(r.venue, r.rate),
|
|
85
100
|
};
|
|
86
101
|
}
|
|
87
102
|
/**
|
|
@@ -117,7 +132,7 @@ const _warnedPriceMismatch = new Set();
|
|
|
117
132
|
* venue whose mark price deviates more than MAX_PRICE_DEVIATION_PCT from the
|
|
118
133
|
* group median, with a one-time stderr warning per (symbol, venue).
|
|
119
134
|
*/
|
|
120
|
-
function dropPriceOutliers(canonical, list) {
|
|
135
|
+
export function dropPriceOutliers(canonical, list, tag = "funding") {
|
|
121
136
|
if (list.length < 2)
|
|
122
137
|
return list;
|
|
123
138
|
const sorted = list.map(r => r.markPrice).sort((x, y) => x - y);
|
|
@@ -133,12 +148,12 @@ function dropPriceOutliers(canonical, list) {
|
|
|
133
148
|
const key = `${canonical}:${r.exchange}`;
|
|
134
149
|
if (!_warnedPriceMismatch.has(key)) {
|
|
135
150
|
_warnedPriceMismatch.add(key);
|
|
136
|
-
process.stderr.write(`[
|
|
151
|
+
process.stderr.write(`[${tag}] 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
152
|
}
|
|
138
153
|
return false;
|
|
139
154
|
});
|
|
140
155
|
}
|
|
141
|
-
function groupByCanonical(rates, symbols) {
|
|
156
|
+
function groupByCanonical(rates, symbols, tag = "funding") {
|
|
142
157
|
const out = new Map();
|
|
143
158
|
for (const r of rates) {
|
|
144
159
|
if (symbols && !symbols.includes(r.canonical))
|
|
@@ -150,7 +165,7 @@ function groupByCanonical(rates, symbols) {
|
|
|
150
165
|
out.set(r.canonical, [r]);
|
|
151
166
|
}
|
|
152
167
|
for (const [canonical, list] of [...out]) {
|
|
153
|
-
const kept = dropPriceOutliers(canonical, list);
|
|
168
|
+
const kept = dropPriceOutliers(canonical, list, tag);
|
|
154
169
|
if (kept.length === 0)
|
|
155
170
|
out.delete(canonical); // every venue disagreed — not comparable
|
|
156
171
|
else
|
|
@@ -158,6 +173,49 @@ function groupByCanonical(rates, symbols) {
|
|
|
158
173
|
}
|
|
159
174
|
return out;
|
|
160
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Mark prices of `venues`, grouped by canonical symbol — the grouping and price-outlier guard of
|
|
178
|
+
* the funding comparison, WITHOUT any funding dependency: read from each venue's market directory
|
|
179
|
+
* (publicData.fetchMarkets), so a market with a null funding rate or a venue whose funding is
|
|
180
|
+
* unverified is still priced. Rows without a canonical symbol, not tradable, or without a
|
|
181
|
+
* positive mark are excluded. A venue that fails is reported in exchangeStatus / exchangeErrors.
|
|
182
|
+
*/
|
|
183
|
+
export async function fetchCanonicalPriceGroups(venues, opts) {
|
|
184
|
+
ensureVenuesRegistered();
|
|
185
|
+
const net = opts?.network ?? sharedApiNetwork();
|
|
186
|
+
const settled = await Promise.allSettled(venues.map(v => v.publicData.fetchMarkets(net, { funding: false })));
|
|
187
|
+
const exchangeStatus = {};
|
|
188
|
+
const exchangeErrors = {};
|
|
189
|
+
const prices = [];
|
|
190
|
+
settled.forEach((res, i) => {
|
|
191
|
+
const def = venues[i];
|
|
192
|
+
if (res.status === "rejected") {
|
|
193
|
+
exchangeStatus[def.id] = "error";
|
|
194
|
+
exchangeErrors[def.id] = errorMessage(res.reason);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
exchangeStatus[def.id] = "ok";
|
|
198
|
+
for (const m of res.value) {
|
|
199
|
+
if (m.canonical === null || !m.tradable)
|
|
200
|
+
continue;
|
|
201
|
+
const markPrice = Number(m.markPrice);
|
|
202
|
+
if (!Number.isFinite(markPrice) || markPrice <= 0)
|
|
203
|
+
continue;
|
|
204
|
+
prices.push({ exchange: def.id, symbol: m.symbol, canonical: m.canonical, markPrice, apiStatus: def.apiStatus.kind });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return { groups: groupByCanonical(prices, opts?.symbols, opts?.tag ?? "prices"), exchangeStatus, exchangeErrors };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Whether a long/short pair can be traded as a funding arb: both legs need the trading role AND
|
|
211
|
+
* verified funding (an unverified-funding venue is display only — never ranked for trading).
|
|
212
|
+
*/
|
|
213
|
+
export function pairExecutability(longExchange, shortExchange) {
|
|
214
|
+
const legs = [...new Set([longExchange, shortExchange])];
|
|
215
|
+
const readOnlyLegs = legs.filter(v => !getVenue(v).roles.includes("trading"));
|
|
216
|
+
const unverifiedLegs = legs.filter(v => !getVenue(v).fundingVerified);
|
|
217
|
+
return { executable: readOnlyLegs.length === 0 && unverifiedLegs.length === 0, readOnlyLegs, unverifiedLegs };
|
|
218
|
+
}
|
|
161
219
|
function bestMarkPrice(rates) {
|
|
162
220
|
const rank = (id) => MARK_PRICE_PREFERENCE.indexOf(id);
|
|
163
221
|
return [...rates].sort((a, b) => rank(a.exchange) - rank(b.exchange))[0].markPrice;
|
|
@@ -183,14 +241,28 @@ export async function fetchAllFundingRates(opts) {
|
|
|
183
241
|
}
|
|
184
242
|
const comparisons = [];
|
|
185
243
|
const minSpread = opts?.minSpread ?? 0;
|
|
244
|
+
let belowMinSpread = 0;
|
|
186
245
|
for (const [symbol, rates] of rateMap) {
|
|
187
246
|
// Need at least 2 exchanges to compare
|
|
188
247
|
if (rates.length < 2)
|
|
189
248
|
continue;
|
|
190
249
|
// Sort by hourly rate (ascending)
|
|
191
250
|
rates.sort((a, b) => a.hourlyRate - b.hourlyRate);
|
|
192
|
-
|
|
193
|
-
|
|
251
|
+
// The long / short legs and the spread come from verified rows only: an unverified rate is
|
|
252
|
+
// display data (--include-unverified), never ranked. < 2 verified rows → a display-only row.
|
|
253
|
+
const ranked = rates.filter(r => r.fundingVerified);
|
|
254
|
+
if (ranked.length < 2) {
|
|
255
|
+
const unverifiedLegs = [...new Set(rates.filter(r => !r.fundingVerified).map(r => r.exchange))];
|
|
256
|
+
comparisons.push({
|
|
257
|
+
symbol, rates, maxSpreadAnnual: null, longExchange: null, shortExchange: null,
|
|
258
|
+
bestMarkPrice: bestMarkPrice(rates), estHourlyIncomeUsd: null,
|
|
259
|
+
executable: false, readOnlyLegs: [], unverifiedLegs,
|
|
260
|
+
mixedKinds: new Set(rates.map(r => r.rateKind)).size > 1,
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const lowest = ranked[0];
|
|
265
|
+
const highest = ranked[ranked.length - 1];
|
|
194
266
|
// The median filter alone lets a 2-venue group through at up to 2 × MAX_PRICE_DEVIATION_PCT
|
|
195
267
|
// apart (each side deviates half the gap): the CHOSEN pair must itself agree on price.
|
|
196
268
|
const pairGapPct = Math.abs(lowest.markPrice - highest.markPrice) / ((lowest.markPrice + highest.markPrice) / 2) * 100;
|
|
@@ -203,8 +275,10 @@ export async function fetchAllFundingRates(opts) {
|
|
|
203
275
|
continue;
|
|
204
276
|
}
|
|
205
277
|
const maxSpreadAnnual = annualizeHourlyRate(highest.hourlyRate - lowest.hourlyRate);
|
|
206
|
-
if (maxSpreadAnnual < minSpread)
|
|
278
|
+
if (maxSpreadAnnual < minSpread) {
|
|
279
|
+
belowMinSpread++;
|
|
207
280
|
continue;
|
|
281
|
+
}
|
|
208
282
|
// Estimate hourly income for $1000 notional arb
|
|
209
283
|
const notional = 1000;
|
|
210
284
|
const longIncome = estimateHourlyFundingFromHourly(lowest.hourlyRate, notional, "long");
|
|
@@ -218,13 +292,17 @@ export async function fetchAllFundingRates(opts) {
|
|
|
218
292
|
shortExchange: highest.exchange, // short where funding is highest
|
|
219
293
|
bestMarkPrice: bestMarkPrice(rates),
|
|
220
294
|
estHourlyIncomeUsd,
|
|
295
|
+
...pairExecutability(lowest.exchange, highest.exchange),
|
|
296
|
+
mixedKinds: new Set(rates.map(r => r.rateKind)).size > 1,
|
|
221
297
|
});
|
|
222
298
|
}
|
|
223
|
-
// Sort by spread descending
|
|
224
|
-
comparisons.sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual);
|
|
299
|
+
// Sort by spread descending; display-only rows (no ranked pair) last
|
|
300
|
+
comparisons.sort((a, b) => (b.maxSpreadAnnual ?? -Infinity) - (a.maxSpreadAnnual ?? -Infinity));
|
|
225
301
|
return {
|
|
226
302
|
timestamp: new Date().toISOString(),
|
|
227
303
|
symbols: comparisons,
|
|
304
|
+
minSpread,
|
|
305
|
+
belowMinSpread,
|
|
228
306
|
exchangeStatus,
|
|
229
307
|
exchangeErrors,
|
|
230
308
|
};
|
|
@@ -233,13 +311,15 @@ export async function fetchAllFundingRates(opts) {
|
|
|
233
311
|
* Fetch rates for a single (canonical) symbol across all exchanges.
|
|
234
312
|
*/
|
|
235
313
|
export async function fetchSymbolFundingRates(symbol, includeUnverified = false) {
|
|
236
|
-
|
|
314
|
+
// The comparison groups by canonical key: a venue ticker / denomination alias (kPEPE) is
|
|
315
|
+
// looked up under its canonical symbol (1000PEPE), never as-is.
|
|
316
|
+
const snapshot = await fetchAllFundingRates({ symbols: [canonicalQuerySymbol(symbol)], includeUnverified });
|
|
237
317
|
return snapshot.symbols[0] ?? null;
|
|
238
318
|
}
|
|
239
319
|
/**
|
|
240
320
|
* Fetch spot+perp funding rate spreads.
|
|
241
321
|
* Since spot has 0 funding cost, the spread is simply |perp funding rate|.
|
|
242
|
-
* Only includes symbols available on at least one bot spot venue (def.spot
|
|
322
|
+
* Only includes symbols available on at least one bot spot venue (def.spot).
|
|
243
323
|
*
|
|
244
324
|
* Key safety checks:
|
|
245
325
|
* - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
|
|
@@ -247,8 +327,8 @@ export async function fetchSymbolFundingRates(symbol, includeUnverified = false)
|
|
|
247
327
|
* (filters out same-ticker-different-token issues like HIP-1 TRUMP ≠ perp TRUMP)
|
|
248
328
|
*/
|
|
249
329
|
export async function fetchSpotPerpSpreads(opts) {
|
|
250
|
-
// Spot legs come from the venue definitions (def.spot
|
|
251
|
-
// (separate/runtime account + transfer
|
|
330
|
+
// Spot legs come from the venue definitions (def.spot): only bot/arb spot venues
|
|
331
|
+
// (separate/runtime account + transfer). Perp legs: those venues + pacifica.
|
|
252
332
|
ensureVenuesRegistered();
|
|
253
333
|
const net = opts?.network ?? sharedApiNetwork();
|
|
254
334
|
const { listBotSpotVenues, requireSpotAdapter, defaultSpotQuote } = await import("../exchanges/spot-factory.js");
|