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,5 +1,6 @@
|
|
|
1
|
+
import { assertCandleInterval } from "./interface.js";
|
|
1
2
|
import { LocalEvmSigner } from "../signer/index.js";
|
|
2
|
-
import { DEFINITE_ORDER_FAILURE_CODES, PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure } from "../errors.js";
|
|
3
|
+
import { DEFINITE_ORDER_FAILURE_CODES, PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure, noAccountKeyError } from "../errors.js";
|
|
3
4
|
import { parseFiniteVenueNumber, venueRateField } from "../utils/numeric.js";
|
|
4
5
|
import { alignProtectionPrice, floorSize, formatComputedPrice, slippagePrice, subDecimal } from "./precision.js";
|
|
5
6
|
import { unsupported } from "./trading-v2.js";
|
|
@@ -9,9 +10,9 @@ import { rateLimitedError } from "./rate-limiter.js";
|
|
|
9
10
|
import { fetchAndCache, withCache, TTL_ACCOUNT } from "../cache.js";
|
|
10
11
|
import { approvalFromMax, builderGateOf, hlFeeTenthsBps, NO_BUILDER, requireFeeBps } from "./builder-fee.js";
|
|
11
12
|
/**
|
|
12
|
-
* Map a TIF input to HL's native value
|
|
13
|
+
* Map a TIF input to HL's native value. Accepts the common names
|
|
13
14
|
* GTC/IOC/ALO and the native spellings case-insensitively. FOK is not offered by
|
|
14
|
-
* HL (no FOK emulation
|
|
15
|
+
* HL (no FOK emulation) → CAPABILITY_UNSUPPORTED; anything else likewise.
|
|
15
16
|
*/
|
|
16
17
|
export function hlTif(raw) {
|
|
17
18
|
const t = raw.trim().toUpperCase();
|
|
@@ -31,7 +32,7 @@ export function hlSizeStep(szDecimals) {
|
|
|
31
32
|
return szDecimals <= 0 ? "1" : `0.${"0".repeat(szDecimals - 1)}1`;
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
* Slippage-bounded IOC protection price
|
|
35
|
+
* Slippage-bounded IOC protection price.
|
|
35
36
|
*
|
|
36
37
|
* `reference` × (1 ± bps/10000), then aligned TOWARD CROSSING (buy → up, sell →
|
|
37
38
|
* down) on HL's price grid: at most 5 significant figures and at most
|
|
@@ -56,8 +57,8 @@ export function hlProtectionPrice(reference, side, bps, szDecimals, maxDecimals)
|
|
|
56
57
|
return { protectionPrice, appliedBps };
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
|
-
* A USER-SUPPLIED limit / trigger / modify price checked against HL's price grid
|
|
60
|
-
* an off-grid limit price is refused, never rounded — rounding to nearest can move a buy price UP
|
|
60
|
+
* A USER-SUPPLIED limit / trigger / modify price checked against HL's price grid:
|
|
61
|
+
* an off-grid limit price is refused, never rounded — rounding to nearest can move a buy price UP.
|
|
61
62
|
* HL docs (trading/tick-and-lot-size): "Prices can have up to 5 significant figures, but no more
|
|
62
63
|
* than MAX_DECIMALS - szDecimals decimal places where MAX_DECIMALS is 6 for perps and 8 for spot.
|
|
63
64
|
* Integer prices are always allowed, regardless of the number of significant figures."
|
|
@@ -140,7 +141,7 @@ export async function assertHlAppliedBps(appliedBps, what) {
|
|
|
140
141
|
}
|
|
141
142
|
/**
|
|
142
143
|
* Definite IOC outcome of a raw HL `order` response (shared contract with the
|
|
143
|
-
* perp-order lane
|
|
144
|
+
* perp-order lane). Reads `response.data.statuses[0]`:
|
|
144
145
|
* - `{ filled: { totalSz } }` with a plain decimal totalSz → that size, final;
|
|
145
146
|
* - `{ error }` that is HL's IOC no-match cancel ("could not immediately match")
|
|
146
147
|
* → the venue explicitly cancelled the IOC with nothing filled → "0", final;
|
|
@@ -270,6 +271,18 @@ export const HL_ABSTRACTION_TTL_MS = 60_000;
|
|
|
270
271
|
export function hlAbstractionCachePrefix(network) {
|
|
271
272
|
return `mode:hl:${network}:abstraction:`;
|
|
272
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Margin tiers of a universe entry. Docs "Margin tiers": "For IDs less than 50, there is a single
|
|
276
|
+
* tier with max leverage equal to the ID" — those tables are not in `marginTables`, so the
|
|
277
|
+
* implicit tier is emitted instead of null. Unknown ids ≥ 50 → null.
|
|
278
|
+
*/
|
|
279
|
+
export function hlMarginTiersOf(marginTableId, tierMap) {
|
|
280
|
+
const id = Number(marginTableId);
|
|
281
|
+
const tiers = tierMap.get(id);
|
|
282
|
+
if (tiers !== undefined)
|
|
283
|
+
return tiers;
|
|
284
|
+
return Number.isInteger(id) && id > 0 && id < 50 ? [{ lowerBound: 0, maxLeverage: id }] : null;
|
|
285
|
+
}
|
|
273
286
|
/** Venue margin restriction of a universe entry: strictIsolated (no margin removal) / noCross; null = unrestricted. */
|
|
274
287
|
export function hlMarginModeOf(asset) {
|
|
275
288
|
if (asset.marginMode === "strictIsolated")
|
|
@@ -343,6 +356,32 @@ export function hlL1ActionRejection(type, e) {
|
|
|
343
356
|
const HL_FUNDING_PAGE = 500;
|
|
344
357
|
/** 7 days of hourly funding = 168 rows per coin; 20 pages = 10 000 rows (~60 coins). */
|
|
345
358
|
const HL_FUNDING_MAX_PAGES = 20;
|
|
359
|
+
/**
|
|
360
|
+
* candleSnapshot intervals (docs "Candle snapshot": "Supported intervals") with their nominal
|
|
361
|
+
* length. Anything else is answered 422 "Failed to deserialize" (live 2026-10-19, "7m"), so it is
|
|
362
|
+
* refused as INVALID_PARAMS before sending. "1M" buckets are 30 days, not calendar months
|
|
363
|
+
* (live: consecutive 1M opens 30 d apart); "1w" buckets open on Thursday 00:00 UTC.
|
|
364
|
+
*/
|
|
365
|
+
export const HL_CANDLE_INTERVAL_MS = {
|
|
366
|
+
"1m": 60_000, "3m": 180_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000,
|
|
367
|
+
"1h": 3_600_000, "2h": 7_200_000, "4h": 14_400_000, "8h": 28_800_000, "12h": 43_200_000,
|
|
368
|
+
"1d": 86_400_000, "3d": 259_200_000, "1w": 604_800_000, "1M": 2_592_000_000,
|
|
369
|
+
};
|
|
370
|
+
/** Docs "Candle snapshot": "Only the most recent 5000 candles are available". */
|
|
371
|
+
export const HL_CANDLE_CAP = 5000;
|
|
372
|
+
/** Earliest candleSnapshot start (unix ms) for `interval` at `now`: now − 5000 × interval. Unknown interval → NaN. */
|
|
373
|
+
export function hlEarliestCandleStart(interval, now = Date.now()) {
|
|
374
|
+
return now - HL_CANDLE_CAP * (HL_CANDLE_INTERVAL_MS[interval] ?? Number.NaN);
|
|
375
|
+
}
|
|
376
|
+
/** recentTrades returns the last 10 trades, with no count parameter (live 2026-10-19). */
|
|
377
|
+
export const HL_RECENT_TRADES_CAP = 10;
|
|
378
|
+
/** l2Book returns at most 20 levels per side (docs l2Book; live 2026-10-19). */
|
|
379
|
+
export const HL_BOOK_LEVELS_CAP = 20;
|
|
380
|
+
/** Time-range info responses carry at most 500 elements (docs "Info endpoint" pagination note). */
|
|
381
|
+
const HL_INFO_PAGE = 500;
|
|
382
|
+
/** Largest `market funding --limit` (hourly rows ≈ 208 days) and the page guard of one read. */
|
|
383
|
+
export const HL_FUNDING_HISTORY_MAX_LIMIT = 5000;
|
|
384
|
+
const HL_FUNDING_HISTORY_MAX_PAGES = 20;
|
|
346
385
|
/** A required field of an HL info row as a string; missing / empty → EXCHANGE_ERROR. */
|
|
347
386
|
function hlField(row, key, endpoint) {
|
|
348
387
|
const v = row[key];
|
|
@@ -362,6 +401,203 @@ function hlSide(v, endpoint) {
|
|
|
362
401
|
return "sell";
|
|
363
402
|
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid ${endpoint} row with unknown side ${JSON.stringify(v)}`, { exchange: "hyperliquid" });
|
|
364
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Open positions of one clearinghouseState (info `clearinghouseState` {user[, dex]}): rows with
|
|
406
|
+
* szi 0 dropped. The ONE mapper of the keyed `getPositions` and the public-address read
|
|
407
|
+
* (`account positions --address`); `symbolOf` names the row (keyed: the adapter's display name,
|
|
408
|
+
* public: the plain coin — "BTC", HIP-3 "xyz:GOLD").
|
|
409
|
+
*/
|
|
410
|
+
export function hlPositionRows(state, symbolOf) {
|
|
411
|
+
const positions = (state?.assetPositions ?? []);
|
|
412
|
+
return positions
|
|
413
|
+
.filter((p) => {
|
|
414
|
+
const pos = (p.position ?? p);
|
|
415
|
+
return parseFiniteVenueNumber(pos.szi, "position.szi", "hyperliquid") !== 0;
|
|
416
|
+
})
|
|
417
|
+
.map((p) => {
|
|
418
|
+
const pos = (p.position ?? p);
|
|
419
|
+
const szi = parseFiniteVenueNumber(pos.szi, "position.szi", "hyperliquid");
|
|
420
|
+
const positionValue = pos.positionValue !== undefined && pos.positionValue !== null
|
|
421
|
+
? parseFiniteVenueNumber(pos.positionValue, "position.positionValue", "hyperliquid")
|
|
422
|
+
: 0;
|
|
423
|
+
return {
|
|
424
|
+
symbol: symbolOf(String(pos.coin ?? "")),
|
|
425
|
+
side: szi > 0 ? "long" : "short",
|
|
426
|
+
size: String(Math.abs(szi)),
|
|
427
|
+
entryPrice: String(pos.entryPx ?? "0"),
|
|
428
|
+
// clearinghouseState has no per-position mark: positionValue / |szi| is computed, so it is
|
|
429
|
+
// formatted (no "2727.2000000000003").
|
|
430
|
+
markPrice: szi !== 0 && positionValue !== 0
|
|
431
|
+
? formatComputedPrice(positionValue / Math.abs(szi))
|
|
432
|
+
: String(pos.markPx ?? "0"),
|
|
433
|
+
liquidationPrice: pos.liquidationPx != null ? String(pos.liquidationPx) : null,
|
|
434
|
+
unrealizedPnl: String(pos.unrealizedPnl ?? "0"),
|
|
435
|
+
leverage: parseFiniteVenueNumber(pos.leverage?.value, "position.leverage.value", "hyperliquid", { defaultValue: 1 }),
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* info `userAbstraction` answer → mode. Per SSOT Rule #2: an unrecognized venue response throws —
|
|
441
|
+
* no silent default.
|
|
442
|
+
*/
|
|
443
|
+
export function hlAbstractionModeOf(raw) {
|
|
444
|
+
if (raw === "unifiedAccount")
|
|
445
|
+
return "unified";
|
|
446
|
+
if (raw === "portfolioMargin")
|
|
447
|
+
return "portfolio";
|
|
448
|
+
if (raw === "disabled")
|
|
449
|
+
return "standard";
|
|
450
|
+
if (raw === "default")
|
|
451
|
+
return "default";
|
|
452
|
+
// "dexAbstraction" is HL's discontinued fourth mode, still returned for some users. It is
|
|
453
|
+
// NOT unified: "USDC balances default to perps balance, all other collateral defaults to
|
|
454
|
+
// spot balance" (docs trading/account-abstraction-modes). Handled explicitly in hlBalanceOf().
|
|
455
|
+
if (raw === "dexAbstraction")
|
|
456
|
+
return "dexAbstraction";
|
|
457
|
+
throw new PerpError("INVALID_PARAMS", `UNKNOWN_ACCOUNT_MODE: HL returned unrecognized userAbstraction mode: ${JSON.stringify(raw)}`, { remediation: "Check app.hyperliquid.xyz Settings → Account Mode" });
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Portfolio-margin collateral rows: the venue returns an `ltv` on exactly the eligible collateral
|
|
461
|
+
* tokens (live 2026-10-26: USDC, HYPE, USDT0 carry it, memecoins do not; unified-mode rows never
|
|
462
|
+
* do), plus the dex's collateral token itself.
|
|
463
|
+
*/
|
|
464
|
+
export function hlIsPortfolioCollateral(row, collateralToken) {
|
|
465
|
+
return Number(row.token) === collateralToken || Object.prototype.hasOwnProperty.call(row, "ltv");
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* USDC price of each spot token (index → price) from info `spotMetaAndAssetCtxs`: the mark of its
|
|
469
|
+
* USDC-quoted pair (quote token 0); several pairs for one token → the one with the larger 24h
|
|
470
|
+
* notional. USDC itself is 1. Malformed payload → EXCHANGE_ERROR (never a guessed price).
|
|
471
|
+
*/
|
|
472
|
+
export function hlSpotUsdPricesOf(raw) {
|
|
473
|
+
const arr = raw;
|
|
474
|
+
const universe = Array.isArray(arr) ? arr[0]?.universe : undefined;
|
|
475
|
+
const ctxs = Array.isArray(arr) ? arr[1] : undefined;
|
|
476
|
+
if (!Array.isArray(universe) || !Array.isArray(ctxs)) {
|
|
477
|
+
throw new PerpError("EXCHANGE_ERROR", "Hyperliquid spotMetaAndAssetCtxs: expected [meta, ctxs]", { exchange: "hyperliquid" });
|
|
478
|
+
}
|
|
479
|
+
const best = new Map();
|
|
480
|
+
for (const pair of universe) {
|
|
481
|
+
const tokens = pair.tokens;
|
|
482
|
+
if (!Array.isArray(tokens) || tokens[1] !== 0 || typeof pair.index !== "number")
|
|
483
|
+
continue;
|
|
484
|
+
const ctx = ctxs[pair.index];
|
|
485
|
+
const price = Number(ctx?.markPx);
|
|
486
|
+
if (!Number.isFinite(price) || price <= 0)
|
|
487
|
+
continue;
|
|
488
|
+
const vol = Number(ctx?.dayNtlVlm);
|
|
489
|
+
const base = Number(tokens[0]);
|
|
490
|
+
const prev = best.get(base);
|
|
491
|
+
if (prev === undefined || (Number.isFinite(vol) ? vol : 0) > prev.vol)
|
|
492
|
+
best.set(base, { price, vol: Number.isFinite(vol) ? vol : 0 });
|
|
493
|
+
}
|
|
494
|
+
const out = new Map([[0, 1]]);
|
|
495
|
+
for (const [t, v] of best)
|
|
496
|
+
out.set(t, v.price);
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
export async function hlBalanceOf(state, mode, dex, reads) {
|
|
500
|
+
const s = state;
|
|
501
|
+
const notes = [];
|
|
502
|
+
const margin = (s?.marginSummary ?? {});
|
|
503
|
+
const cross = (s?.crossMarginSummary ?? {});
|
|
504
|
+
const marginUsed = parseFiniteVenueNumber(margin.totalMarginUsed ?? cross.totalMarginUsed, "marginSummary.totalMarginUsed", "hyperliquid");
|
|
505
|
+
// Sum unrealized PnL directly from positions (reliable for both main + dex accounts)
|
|
506
|
+
const positions = (s?.assetPositions ?? []);
|
|
507
|
+
let unrealizedPnl = 0;
|
|
508
|
+
for (const entry of positions) {
|
|
509
|
+
const pos = (entry.position ?? entry);
|
|
510
|
+
unrealizedPnl += parseFiniteVenueNumber(pos.unrealizedPnl, "position.unrealizedPnl", "hyperliquid");
|
|
511
|
+
}
|
|
512
|
+
let equity;
|
|
513
|
+
let available;
|
|
514
|
+
let portfolio = null;
|
|
515
|
+
// dexAbstraction (discontinued): USDC (token 0) lives in the NATIVE perp balance, any other
|
|
516
|
+
// collateral token in spot. Native perps → the perp state below. A HIP-3 dex with non-USDC
|
|
517
|
+
// collateral → its spot row. A USDC-collateral HIP-3 dex is refused: its dex state is not the
|
|
518
|
+
// balance ("Cross margin on HIP-3 DEXs does not behave intuitively for DEX abstraction users").
|
|
519
|
+
let spotIsTruth = mode === "unified" || mode === "portfolio";
|
|
520
|
+
if (mode === "dexAbstraction" && dex) {
|
|
521
|
+
if (await reads.collateralToken() === 0) {
|
|
522
|
+
throw new PerpError("NOT_IMPLEMENTED", `Hyperliquid account mode "dexAbstraction" (discontinued by the venue): the USDC balance of HIP-3 dex "${dex}" cannot be read reliably`, {
|
|
523
|
+
exchange: "hyperliquid",
|
|
524
|
+
remediation: "Switch the account mode first: perp -e hyperliquid wallet manage account-mode unified (or standard)",
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
spotIsTruth = true;
|
|
528
|
+
}
|
|
529
|
+
if (spotIsTruth) {
|
|
530
|
+
const { row, balances, token } = await reads.spotCollateralRow();
|
|
531
|
+
const coin = row === null ? `token#${token}` : String(row.coin);
|
|
532
|
+
const spotTotal = parseFiniteVenueNumber(row?.total, `spotBalance.${coin}.total`, "hyperliquid");
|
|
533
|
+
const spotHold = parseFiniteVenueNumber(row?.hold, `spotBalance.${coin}.hold`, "hyperliquid");
|
|
534
|
+
equity = spotTotal;
|
|
535
|
+
available = spotTotal - spotHold;
|
|
536
|
+
if (mode === "portfolio") {
|
|
537
|
+
const prices = await reads.spotUsdPrices();
|
|
538
|
+
const rows = [];
|
|
539
|
+
let value = 0;
|
|
540
|
+
for (const b of balances) {
|
|
541
|
+
if (!hlIsPortfolioCollateral(b, token))
|
|
542
|
+
continue;
|
|
543
|
+
const c = String(b.coin);
|
|
544
|
+
const total = parseFiniteVenueNumber(b.total, `spotBalance.${c}.total`, "hyperliquid");
|
|
545
|
+
if (total === 0)
|
|
546
|
+
continue; // an empty row adds nothing (USDT0 0 with a hold: the hold is in available)
|
|
547
|
+
const isCollateral = Number(b.token) === token;
|
|
548
|
+
let priceUsd;
|
|
549
|
+
if (isCollateral)
|
|
550
|
+
priceUsd = 1;
|
|
551
|
+
else {
|
|
552
|
+
const p = prices.get(Number(b.token));
|
|
553
|
+
if (p === undefined) {
|
|
554
|
+
throw new PerpError("PRICE_STALE", `Hyperliquid portfolio margin: no USDC spot price for collateral ${c} (token ${String(b.token)}) — equity unknown`, {
|
|
555
|
+
exchange: "hyperliquid", remediation: "Retry shortly; the spot market context read may have failed",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
priceUsd = p;
|
|
559
|
+
}
|
|
560
|
+
const valueUsd = total * priceUsd;
|
|
561
|
+
value += valueUsd;
|
|
562
|
+
rows.push({
|
|
563
|
+
token: c, total: String(b.total), priceUsd: String(priceUsd), valueUsd: String(valueUsd),
|
|
564
|
+
ltv: typeof b.ltv === "string" ? b.ltv : null,
|
|
565
|
+
borrowed: typeof b.borrowed === "string" ? b.borrowed : null,
|
|
566
|
+
supplied: typeof b.supplied === "string" ? b.supplied : null,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
equity = value;
|
|
570
|
+
const ratio = (await reads.spotCollateralRow()).portfolioMarginRatio;
|
|
571
|
+
portfolio = { collateral: rows, portfolioMarginRatio: typeof ratio === "string" ? ratio : null };
|
|
572
|
+
notes.push(`[hl] portfolio margin: equity = Σ collateral total × spot USDC price (${rows.map(r => `${r.token} ${r.total}${r.priceUsd !== "1" ? ` × ${r.priceUsd}` : ""}`).join(" + ")}); non-collateral spot tokens are listed in spot, vault equity is not included\n`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
equity = parseFiniteVenueNumber(margin.accountValue ?? cross.accountValue, "marginSummary.accountValue", "hyperliquid");
|
|
577
|
+
available = parseFiniteVenueNumber(s?.withdrawable, "withdrawable", "hyperliquid");
|
|
578
|
+
if (mode === "default" && equity === 0) {
|
|
579
|
+
// Unset mode: do not assume unified. If the perp side reads 0 while spot holds
|
|
580
|
+
// the collateral token, say so instead of reporting an empty account silently.
|
|
581
|
+
const { row, token } = await reads.spotCollateralRow();
|
|
582
|
+
const spotTotal = parseFiniteVenueNumber(row?.total, `spotBalance.token#${token}.total`, "hyperliquid");
|
|
583
|
+
if (spotTotal > 0) {
|
|
584
|
+
notes.push(`[hl] account mode is unset ("default"): perp equity${dex ? ` on dex ${dex}` : ""} is 0 while spot holds ${spotTotal} ${String(row?.coin)}. ` +
|
|
585
|
+
`That spot balance is not perp collateral here. Either switch the account to unified ` +
|
|
586
|
+
`(perp wallet manage account-mode unified) or move it: perp -e hyperliquid trade spot-transfer to-perp <amount>\n`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
balance: {
|
|
592
|
+
equity: String(equity),
|
|
593
|
+
available: String(available),
|
|
594
|
+
marginUsed: String(marginUsed),
|
|
595
|
+
unrealizedPnl: String(unrealizedPnl),
|
|
596
|
+
...(portfolio ?? {}),
|
|
597
|
+
},
|
|
598
|
+
notes,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
365
601
|
export class HyperliquidAdapter {
|
|
366
602
|
name = "hyperliquid";
|
|
367
603
|
chain = "evm";
|
|
@@ -375,6 +611,8 @@ export class HyperliquidAdapter {
|
|
|
375
611
|
* semantics for accounts that have never opted in. Codex v0.12.12 final QA #1.
|
|
376
612
|
*/
|
|
377
613
|
isUnifiedAccount = false;
|
|
614
|
+
/** The account abstraction mode resolved in init() (null until then / without an address). */
|
|
615
|
+
accountMode = null;
|
|
378
616
|
_address;
|
|
379
617
|
_privateKey;
|
|
380
618
|
_testnet;
|
|
@@ -400,6 +638,8 @@ export class HyperliquidAdapter {
|
|
|
400
638
|
* configured. Set by createHyperliquid — the adapter never reads settings.
|
|
401
639
|
*/
|
|
402
640
|
builderFee = null;
|
|
641
|
+
/** recentTrades' fixed tape size (no paging): `market trades` defaults to min(20, this). */
|
|
642
|
+
recentTradesCap = HL_RECENT_TRADES_CAP;
|
|
403
643
|
constructor(privateKey, testnet = false) {
|
|
404
644
|
// No SDK: /info reads go through _infoPost, /exchange actions are signed here (hl-signing.ts).
|
|
405
645
|
this._address = "";
|
|
@@ -409,9 +649,39 @@ export class HyperliquidAdapter {
|
|
|
409
649
|
/** Set the HIP-3 deployed perp dex to query/trade on. */
|
|
410
650
|
setDex(dex) {
|
|
411
651
|
this._dex = dex;
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
this.
|
|
652
|
+
// Fresh market state for the new dex — NEW objects, never .clear(): a map shared with another
|
|
653
|
+
// adapter (prototype copy) must not be emptied under it. Rebuilt lazily (_loadAssetMap).
|
|
654
|
+
this._assetMap = new Map();
|
|
655
|
+
this._assetMapReverse = new Map();
|
|
656
|
+
this._szDecimalsMap = new Map();
|
|
657
|
+
this._marginModeMap = new Map();
|
|
658
|
+
this._assetMapError = null;
|
|
659
|
+
this._marketsCache = null;
|
|
660
|
+
this._marketsCacheTime = 0;
|
|
661
|
+
this._delisted = new Set();
|
|
662
|
+
this._delistedAssets = new Set();
|
|
663
|
+
this._chsInFlight = null;
|
|
664
|
+
this._collateralTokenIdx = null;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* An independent adapter for HIP-3 dex `dex` ("" = native perps) with this adapter's account
|
|
668
|
+
* configuration (key / signers / address / network / builder gate) and its OWN market state
|
|
669
|
+
* (asset maps, markets cache, delisted sets, in-flight reads). Fan-outs over dexes use this:
|
|
670
|
+
* Object.create(adapter) + setDex() shared the prototype's maps, so setDex cleared the native
|
|
671
|
+
* adapter's asset map and the dex copies served the native markets cache.
|
|
672
|
+
*/
|
|
673
|
+
forDex(dex) {
|
|
674
|
+
const a = new HyperliquidAdapter(this._privateKey || undefined, this._testnet);
|
|
675
|
+
a._address = this._address;
|
|
676
|
+
a._evmSigner = this._evmSigner;
|
|
677
|
+
a._agentSigner = this._agentSigner;
|
|
678
|
+
a._agentMeta = this._agentMeta;
|
|
679
|
+
a._useNoAgent = this._useNoAgent;
|
|
680
|
+
a.isUnifiedAccount = this.isUnifiedAccount;
|
|
681
|
+
a.accountMode = this.accountMode;
|
|
682
|
+
a.builderFee = this.builderFee;
|
|
683
|
+
a.setDex(dex);
|
|
684
|
+
return a;
|
|
415
685
|
}
|
|
416
686
|
get dex() {
|
|
417
687
|
return this._dex;
|
|
@@ -544,6 +814,7 @@ export class HyperliquidAdapter {
|
|
|
544
814
|
// every caller that branches on the account mode.
|
|
545
815
|
if (this._address) {
|
|
546
816
|
const mode = await this._getAbstractionMode();
|
|
817
|
+
this.accountMode = mode;
|
|
547
818
|
// dexAbstraction is not unified (USDC stays in the perp balance).
|
|
548
819
|
this.isUnifiedAccount = mode === "unified" || mode === "portfolio";
|
|
549
820
|
}
|
|
@@ -562,6 +833,7 @@ export class HyperliquidAdapter {
|
|
|
562
833
|
/** Load asset index map — supports native and HIP-3 dex. */
|
|
563
834
|
async _loadAssetMap() {
|
|
564
835
|
this._assetMapError = null;
|
|
836
|
+
this._delistedAssets = new Set();
|
|
565
837
|
try {
|
|
566
838
|
if (this._dex) {
|
|
567
839
|
// HIP-3 deployed dex: asset indices use global offset scheme
|
|
@@ -598,6 +870,10 @@ export class HyperliquidAdapter {
|
|
|
598
870
|
this._assetMap.set(fullName, globalIdx);
|
|
599
871
|
this._assetMap.set(baseName, globalIdx);
|
|
600
872
|
this._assetMapReverse.set(globalIdx, fullName);
|
|
873
|
+
if (asset.isDelisted === true) {
|
|
874
|
+
this._delistedAssets.add(fullName);
|
|
875
|
+
this._delistedAssets.add(baseName);
|
|
876
|
+
}
|
|
601
877
|
// Cache szDecimals for dex assets (critical for order sizing)
|
|
602
878
|
if (asset.szDecimals !== undefined) {
|
|
603
879
|
this._szDecimalsMap.set(fullName, asset.szDecimals);
|
|
@@ -620,6 +896,8 @@ export class HyperliquidAdapter {
|
|
|
620
896
|
const sym = `${asset.name}-PERP`;
|
|
621
897
|
this._assetMap.set(sym, idx);
|
|
622
898
|
this._assetMapReverse.set(idx, sym);
|
|
899
|
+
if (asset.isDelisted === true)
|
|
900
|
+
this._delistedAssets.add(sym);
|
|
623
901
|
if (asset.szDecimals !== undefined) {
|
|
624
902
|
this._szDecimalsMap.set(sym, asset.szDecimals);
|
|
625
903
|
}
|
|
@@ -653,7 +931,7 @@ export class HyperliquidAdapter {
|
|
|
653
931
|
// A dex-prefixed symbol must name THIS adapter's dex — never rewritten onto another dex.
|
|
654
932
|
const [prefix, base] = sym.split(":");
|
|
655
933
|
if (prefix.toLowerCase() !== this._dex.toLowerCase()) {
|
|
656
|
-
throw new PerpError("SYMBOL_NOT_FOUND", `Hyperliquid symbol ${
|
|
934
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Hyperliquid symbol ${prefix.toLowerCase()}:${base} is on dex "${prefix.toLowerCase()}", not ${this._dex ? `this adapter's dex "${this._dex}"` : "the native perp dex"}`, {
|
|
657
935
|
exchange: "hyperliquid", remediation: `Select the dex: perp -e hyperliquid --dex ${prefix.toLowerCase()} ...`,
|
|
658
936
|
});
|
|
659
937
|
}
|
|
@@ -726,7 +1004,14 @@ export class HyperliquidAdapter {
|
|
|
726
1004
|
}
|
|
727
1005
|
return idx;
|
|
728
1006
|
}
|
|
729
|
-
throw
|
|
1007
|
+
throw this._unknownSymbol(symbol, `Unknown asset: ${symbol}`);
|
|
1008
|
+
}
|
|
1009
|
+
/** SYMBOL_NOT_FOUND for a coin the venue does not list (on this adapter's dex), with the listing command. */
|
|
1010
|
+
_unknownSymbol(symbol, message) {
|
|
1011
|
+
return new PerpError("SYMBOL_NOT_FOUND", message ?? `Hyperliquid has no market ${symbol.toUpperCase()}${this._dex ? ` on dex "${this._dex}"` : ""}`, {
|
|
1012
|
+
exchange: "hyperliquid",
|
|
1013
|
+
remediation: `List markets: perp -e hyperliquid${this._dex ? ` --dex ${this._dex}` : ""} market list`,
|
|
1014
|
+
});
|
|
730
1015
|
}
|
|
731
1016
|
async getMarkets() {
|
|
732
1017
|
if (this._marketsCache && Date.now() - this._marketsCacheTime < HyperliquidAdapter.CACHE_TTL)
|
|
@@ -785,32 +1070,109 @@ export class HyperliquidAdapter {
|
|
|
785
1070
|
this._assetMapReverse.set(i, sym);
|
|
786
1071
|
});
|
|
787
1072
|
}
|
|
788
|
-
|
|
1073
|
+
// Delisted assets (meta `isDelisted: true`; live 2026-10-14: 56 of 234 native perps, all with
|
|
1074
|
+
// funding / dayNtlVlm / openInterest "0.0") are not markets: excluded from the listing and
|
|
1075
|
+
// everything built on it. They stay in the asset map (_loadAssetMap), so an explicit symbol
|
|
1076
|
+
// still resolves for orders / position reads.
|
|
1077
|
+
const delisted = new Set();
|
|
1078
|
+
const result = universe.flatMap((asset, i) => {
|
|
1079
|
+
if (asset.isDelisted === true) {
|
|
1080
|
+
delisted.add(String(asset.name));
|
|
1081
|
+
return [];
|
|
1082
|
+
}
|
|
789
1083
|
const ctx = (ctxs[i] ?? {});
|
|
790
1084
|
const sym = String(asset.name);
|
|
791
1085
|
const szDec = this._szDecimalsMap.get(sym) ?? (asset.szDecimals !== undefined ? Number(asset.szDecimals) : null);
|
|
792
|
-
return {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1086
|
+
return [{
|
|
1087
|
+
// One symbol shape on every venue: the plain asset ("BTC"; HIP-3 "km:US500"), the
|
|
1088
|
+
// venue's wire coin in venueSymbol. The internal "<COIN>-PERP" key is not shown.
|
|
1089
|
+
symbol: sym.replace(/-PERP$/, ""),
|
|
1090
|
+
// The coin exactly as the venue names it on the wire (native "kPEPE", HIP-3 "xyz:TSLA" —
|
|
1091
|
+
// lower-case dex prefix); _venueCoin upper-cases its input, so it is not used here.
|
|
1092
|
+
venueSymbol: sym.replace(/-PERP$/, ""),
|
|
1093
|
+
markPrice: String(ctx.markPx ?? mids[sym] ?? "0"),
|
|
1094
|
+
indexPrice: ctx.oraclePx != null ? String(ctx.oraclePx) : null,
|
|
1095
|
+
fundingRate: ctx.funding != null ? String(ctx.funding) : null,
|
|
1096
|
+
// metaAndAssetCtxs `funding` = the running hour's rate (WS allDexsAssetCtxs: accruing).
|
|
1097
|
+
fundingRateKind: ctx.funding != null ? "accruing" : null,
|
|
1098
|
+
volume24h: ctx.dayNtlVlm != null ? String(ctx.dayNtlVlm) : null,
|
|
1099
|
+
openInterest: ctx.openInterest != null ? String(ctx.openInterest) : null,
|
|
1100
|
+
maxLeverage: Number(asset.maxLeverage ?? 50),
|
|
1101
|
+
sizeDecimals: szDec,
|
|
1102
|
+
// HL has no separate lot-size field; the step is implied by szDecimals (10^-szDecimals,
|
|
1103
|
+
// the same value the keyless MCP get_markets path states).
|
|
1104
|
+
stepSize: szDec !== null && Number.isInteger(szDec) && szDec >= 0 ? (szDec === 0 ? "1" : `0.${"0".repeat(szDec - 1)}1`) : null,
|
|
1105
|
+
// HL funding is uniform: a fraction per 1 h (venue fundingRateBasis), core and HIP-3.
|
|
1106
|
+
fundingHours: 1,
|
|
1107
|
+
marginTiers: hlMarginTiersOf(asset.marginTableId, tierMap),
|
|
1108
|
+
}];
|
|
807
1109
|
});
|
|
1110
|
+
this._delisted = delisted;
|
|
808
1111
|
this._marketsCache = result;
|
|
809
1112
|
this._marketsCacheTime = Date.now();
|
|
810
1113
|
return result;
|
|
811
1114
|
}
|
|
812
|
-
|
|
1115
|
+
/** Display names of the delisted assets seen by the last getMarkets() (meta isDelisted). */
|
|
1116
|
+
_delisted = new Set();
|
|
1117
|
+
/** Asset-map keys of the delisted assets in `meta` (_loadAssetMap): "MATIC-PERP"; HIP-3 "km:US500" and "US500". */
|
|
1118
|
+
_delistedAssets = new Set();
|
|
1119
|
+
/**
|
|
1120
|
+
* Why `symbol` is not in getMarkets(): it is delisted (message), or null when it is not a
|
|
1121
|
+
* delisted asset. Read after getMarkets() (or once the asset map is loaded). `market info` uses
|
|
1122
|
+
* it to say "delisted" rather than "not found" — the asset map still resolves it for orders /
|
|
1123
|
+
* positions / history. A HIP-3 bare name ("US500" on --dex km) is matched by its venue coin.
|
|
1124
|
+
*/
|
|
1125
|
+
delistedMessage(symbol) {
|
|
1126
|
+
const resolved = this.resolveSymbol(symbol.toUpperCase());
|
|
1127
|
+
const coin = this._venueCoin(symbol).replace(/^([^:]+):/, (_m, dex) => `${dex.toLowerCase()}:`); // "KM:US500" → "km:US500"
|
|
1128
|
+
const isDelisted = this._delisted.has(resolved) || this._delisted.has(coin) || this._delistedAssets.has(resolved);
|
|
1129
|
+
if (!isDelisted)
|
|
1130
|
+
return null;
|
|
1131
|
+
return `${this._dex ? coin : resolved.replace(/-PERP$/, "")} is delisted on Hyperliquid (positions / history still readable)`;
|
|
1132
|
+
}
|
|
1133
|
+
/** SYMBOL_NOT_FOUND (details.reason "delisted") for a delisted `symbol`, with a dex-aware remediation; null when listed. */
|
|
1134
|
+
delistedError(symbol) {
|
|
1135
|
+
const msg = this.delistedMessage(symbol);
|
|
1136
|
+
if (msg === null)
|
|
1137
|
+
return null;
|
|
1138
|
+
return new PerpError("SYMBOL_NOT_FOUND", msg, {
|
|
1139
|
+
exchange: "hyperliquid",
|
|
1140
|
+
reason: "delisted",
|
|
1141
|
+
remediation: `List live markets: perp -e hyperliquid${this._dex ? ` --dex ${this._dex}` : ""} market list`,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Per-symbol MARKET-DATA reads (trades, funding, book, klines): the symbol must resolve
|
|
1146
|
+
* (SYMBOL_NOT_FOUND) and must not be delisted — HL keeps serving a delisted coin's last tape
|
|
1147
|
+
* (MATIC: trades from 2024-09-09) and zero-rate funding rows stamped every hour (live
|
|
1148
|
+
* 2026-10-19), which read as a live market. Orders / positions / history do not call this.
|
|
1149
|
+
*/
|
|
1150
|
+
async _listedMarket(symbol) {
|
|
1151
|
+
await this.getAssetIndex(symbol);
|
|
1152
|
+
const err = this.delistedError(symbol);
|
|
1153
|
+
if (err !== null)
|
|
1154
|
+
throw err;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Public order-book read (`market book`, bots, MCP): l2Book serves at most HL_BOOK_LEVELS_CAP
|
|
1158
|
+
* (20) levels per side and cannot be paged, so a `depth` above it is INVALID_PARAMS naming the
|
|
1159
|
+
* cap. Refuses delisted markets (_listedMarket). Order paths use _l2Book (no delisted gate):
|
|
1160
|
+
* a reduce-only close of a delisted asset must still get its reference price.
|
|
1161
|
+
*/
|
|
1162
|
+
async getOrderbook(symbol, depth) {
|
|
1163
|
+
if (depth !== undefined && !(Number.isInteger(depth) && depth > 0 && depth <= HL_BOOK_LEVELS_CAP)) {
|
|
1164
|
+
throw new PerpError("INVALID_PARAMS", `Hyperliquid order book: depth must be an integer 1..${HL_BOOK_LEVELS_CAP} (got ${depth}) — l2Book serves at most ${HL_BOOK_LEVELS_CAP} levels per side`, {
|
|
1165
|
+
exchange: "hyperliquid", remediation: `Use -d ${HL_BOOK_LEVELS_CAP} or less`,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
await this._listedMarket(symbol);
|
|
1169
|
+
return this._l2Book(symbol);
|
|
1170
|
+
}
|
|
1171
|
+
/** l2Book of `symbol` (native or this adapter's dex), no listing gate. Unlisted coin → SYMBOL_NOT_FOUND. */
|
|
1172
|
+
async _l2Book(symbol) {
|
|
813
1173
|
let levels;
|
|
1174
|
+
// l2Book answers `null` for a coin the venue does not list (live 2026-10-14, coin "NOPE"):
|
|
1175
|
+
// SYMBOL_NOT_FOUND, never an empty book (which reads as "no liquidity").
|
|
814
1176
|
if (this._dex) {
|
|
815
1177
|
// HIP-3 dex: full dex-prefixed coin (e.g. "US500" → "km:US500") + dex param
|
|
816
1178
|
const book = await this._infoPost({
|
|
@@ -818,11 +1180,15 @@ export class HyperliquidAdapter {
|
|
|
818
1180
|
coin: this._venueCoin(symbol),
|
|
819
1181
|
dex: this._dex,
|
|
820
1182
|
});
|
|
821
|
-
|
|
1183
|
+
if (book === null)
|
|
1184
|
+
throw this._unknownSymbol(symbol);
|
|
1185
|
+
levels = book.levels ?? [[], []];
|
|
822
1186
|
}
|
|
823
1187
|
else {
|
|
824
1188
|
const book = await this._infoPost({ type: "l2Book", coin: this._venueCoin(symbol) });
|
|
825
|
-
|
|
1189
|
+
if (book === null)
|
|
1190
|
+
throw this._unknownSymbol(symbol);
|
|
1191
|
+
levels = book.levels ?? [[], []];
|
|
826
1192
|
}
|
|
827
1193
|
return {
|
|
828
1194
|
bids: (levels[0] ?? []).map((l) => [
|
|
@@ -851,7 +1217,7 @@ export class HyperliquidAdapter {
|
|
|
851
1217
|
}
|
|
852
1218
|
ensureAddress() {
|
|
853
1219
|
if (!this._address) {
|
|
854
|
-
throw
|
|
1220
|
+
throw noAccountKeyError(this.name);
|
|
855
1221
|
}
|
|
856
1222
|
}
|
|
857
1223
|
/** The in-flight clearinghouseState read: parallel callers (getBalance + getPositions) share one request. */
|
|
@@ -894,23 +1260,7 @@ export class HyperliquidAdapter {
|
|
|
894
1260
|
async _getAbstractionMode(opts = {}) {
|
|
895
1261
|
this.ensureAddress();
|
|
896
1262
|
const key = `${hlAbstractionCachePrefix(this._network)}${this._address.toLowerCase()}`;
|
|
897
|
-
return (opts.fresh === true ? fetchAndCache : withCache)(key, HL_ABSTRACTION_TTL_MS, async () => {
|
|
898
|
-
const raw = (await this._infoPost({ type: "userAbstraction", user: this._address }));
|
|
899
|
-
if (raw === "unifiedAccount")
|
|
900
|
-
return "unified";
|
|
901
|
-
if (raw === "portfolioMargin")
|
|
902
|
-
return "portfolio";
|
|
903
|
-
if (raw === "disabled")
|
|
904
|
-
return "standard";
|
|
905
|
-
if (raw === "default")
|
|
906
|
-
return "default";
|
|
907
|
-
// "dexAbstraction" is HL's discontinued fourth mode, still returned for some users. It is
|
|
908
|
-
// NOT unified: "USDC balances default to perps balance, all other collateral defaults to
|
|
909
|
-
// spot balance" (docs trading/account-abstraction-modes). Handled explicitly in getBalance().
|
|
910
|
-
if (raw === "dexAbstraction")
|
|
911
|
-
return "dexAbstraction";
|
|
912
|
-
throw new PerpError("INVALID_PARAMS", `UNKNOWN_ACCOUNT_MODE: HL returned unrecognized userAbstraction mode: ${JSON.stringify(raw)}`, { remediation: "Check app.hyperliquid.xyz Settings → Account Mode" });
|
|
913
|
-
});
|
|
1263
|
+
return (opts.fresh === true ? fetchAndCache : withCache)(key, HL_ABSTRACTION_TTL_MS, async () => hlAbstractionModeOf(await this._infoPost({ type: "userAbstraction", user: this._address })));
|
|
914
1264
|
}
|
|
915
1265
|
/** Collateral token index of this adapter's perp dex (info `meta{dex}.collateralToken`; 0 = USDC on native perps). */
|
|
916
1266
|
_collateralTokenIdx = null;
|
|
@@ -941,127 +1291,22 @@ export class HyperliquidAdapter {
|
|
|
941
1291
|
const [spotState, token] = await Promise.all([this._getSpotClearinghouseState(), this._getCollateralToken()]);
|
|
942
1292
|
const balances = (spotState?.balances ?? []);
|
|
943
1293
|
const row = balances.find((b) => Number(b.token) === token) ?? null;
|
|
944
|
-
return { row, balances, token };
|
|
1294
|
+
return { row, balances, token, portfolioMarginRatio: spotState?.portfolioMarginRatio };
|
|
945
1295
|
}
|
|
946
1296
|
async getBalance() {
|
|
947
1297
|
const state = await this._getClearinghouseState();
|
|
948
|
-
const s = state;
|
|
949
|
-
const margin = (s?.marginSummary ?? {});
|
|
950
|
-
const cross = (s?.crossMarginSummary ?? {});
|
|
951
|
-
const marginUsed = parseFiniteVenueNumber(margin.totalMarginUsed ?? cross.totalMarginUsed, "marginSummary.totalMarginUsed", "hyperliquid");
|
|
952
|
-
// Sum unrealized PnL directly from positions (reliable for both main + dex accounts)
|
|
953
|
-
const positions = (s?.assetPositions ?? []);
|
|
954
|
-
let unrealizedPnl = 0;
|
|
955
|
-
for (const entry of positions) {
|
|
956
|
-
const pos = (entry.position ?? entry);
|
|
957
|
-
unrealizedPnl += parseFiniteVenueNumber(pos.unrealizedPnl, "position.unrealizedPnl", "hyperliquid");
|
|
958
|
-
}
|
|
959
|
-
let equity;
|
|
960
|
-
let available;
|
|
961
|
-
// Branch by the user's account abstraction mode (Rule #2: no silent fallback).
|
|
962
|
-
// - "unified" / "portfolio": per HL docs, API users see all balances and holds
|
|
963
|
-
// in the spot clearinghouse state; the dex's collateral token row (by token
|
|
964
|
-
// index from meta{dex}.collateralToken) is the equity. Adding perp margin
|
|
965
|
-
// would double-count.
|
|
966
|
-
// - "standard" / "default": perp clearinghouse (per dex) is the collateral source.
|
|
967
|
-
//
|
|
968
|
-
// Portfolio-margin mode: eligible collateral includes HYPE, BTC, USDT besides
|
|
969
|
-
// USDC. Only the collateral token is counted; a stderr note lists the rest.
|
|
970
1298
|
const mode = await this._getAbstractionMode();
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
exchange: "hyperliquid",
|
|
980
|
-
remediation: "Switch the account mode first: perp -e hyperliquid wallet manage account-mode unified (or standard)",
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
spotIsTruth = true;
|
|
984
|
-
}
|
|
985
|
-
if (spotIsTruth) {
|
|
986
|
-
const { row, balances, token } = await this._spotCollateralRow();
|
|
987
|
-
const coin = row === null ? `token#${token}` : String(row.coin);
|
|
988
|
-
const spotTotal = parseFiniteVenueNumber(row?.total, `spotBalance.${coin}.total`, "hyperliquid");
|
|
989
|
-
const spotHold = parseFiniteVenueNumber(row?.hold, `spotBalance.${coin}.hold`, "hyperliquid");
|
|
990
|
-
equity = spotTotal;
|
|
991
|
-
available = spotTotal - spotHold;
|
|
992
|
-
if (mode === "portfolio") {
|
|
993
|
-
// Eligible collateral per trading/account-abstraction-modes.md ("eligible
|
|
994
|
-
// assets, which are currently HYPE, BTC, USDC, USDT").
|
|
995
|
-
const PORTFOLIO_COLLATERAL = ["HYPE", "BTC", "USDT"];
|
|
996
|
-
const others = balances.filter((b) => {
|
|
997
|
-
if (Number(b.token) === token)
|
|
998
|
-
return false;
|
|
999
|
-
const c = String(b.coin);
|
|
1000
|
-
// Reject NaN/empty here too — venue payload corruption shouldn't
|
|
1001
|
-
// silently filter a token in or out.
|
|
1002
|
-
const total = parseFiniteVenueNumber(b.total, `spotBalance.${c}.total`, "hyperliquid");
|
|
1003
|
-
if (total <= 0)
|
|
1004
|
-
return false;
|
|
1005
|
-
return PORTFOLIO_COLLATERAL.some((p) => c === p || c.startsWith(`${p}-`));
|
|
1006
|
-
});
|
|
1007
|
-
if (others.length > 0) {
|
|
1008
|
-
const summary = others.map((b) => `${b.coin}=${b.total}`).join(", ");
|
|
1009
|
-
process.stderr.write(`[hl] portfolio mode: non-${coin} collateral present but not counted in equity (${summary}). ` +
|
|
1010
|
-
`Equity reflects ${coin} only; full multi-asset accounting pending.\n`);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
equity = parseFiniteVenueNumber(margin.accountValue ?? cross.accountValue, "marginSummary.accountValue", "hyperliquid");
|
|
1016
|
-
available = parseFiniteVenueNumber(s?.withdrawable, "withdrawable", "hyperliquid");
|
|
1017
|
-
if (mode === "default" && equity === 0) {
|
|
1018
|
-
// Unset mode: do not assume unified. If the perp side reads 0 while spot holds
|
|
1019
|
-
// the collateral token, say so instead of reporting an empty account silently.
|
|
1020
|
-
const { row, token } = await this._spotCollateralRow();
|
|
1021
|
-
const spotTotal = parseFiniteVenueNumber(row?.total, `spotBalance.token#${token}.total`, "hyperliquid");
|
|
1022
|
-
if (spotTotal > 0) {
|
|
1023
|
-
process.stderr.write(`[hl] account mode is unset ("default"): perp equity${this._dex ? ` on dex ${this._dex}` : ""} is 0 while spot holds ${spotTotal} ${String(row?.coin)}. ` +
|
|
1024
|
-
`That spot balance is not perp collateral here. Either switch the account to unified ` +
|
|
1025
|
-
`(perp wallet manage account-mode unified) or move it: perp -e hyperliquid trade spot-transfer to-perp <amount>\n`);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
return {
|
|
1030
|
-
equity: String(equity),
|
|
1031
|
-
available: String(available),
|
|
1032
|
-
marginUsed: String(marginUsed),
|
|
1033
|
-
unrealizedPnl: String(unrealizedPnl),
|
|
1034
|
-
};
|
|
1299
|
+
const { balance, notes } = await hlBalanceOf(state, mode, this._dex, {
|
|
1300
|
+
spotCollateralRow: () => this._spotCollateralRow(),
|
|
1301
|
+
collateralToken: () => this._getCollateralToken(),
|
|
1302
|
+
spotUsdPrices: async () => hlSpotUsdPricesOf(await this._infoPost({ type: "spotMetaAndAssetCtxs" })),
|
|
1303
|
+
});
|
|
1304
|
+
for (const n of notes)
|
|
1305
|
+
process.stderr.write(n);
|
|
1306
|
+
return balance;
|
|
1035
1307
|
}
|
|
1036
1308
|
async getPositions() {
|
|
1037
|
-
|
|
1038
|
-
const positions = (state?.assetPositions ?? []);
|
|
1039
|
-
return positions
|
|
1040
|
-
.filter((p) => {
|
|
1041
|
-
const pos = (p.position ?? p);
|
|
1042
|
-
return parseFiniteVenueNumber(pos.szi, "position.szi", "hyperliquid") !== 0;
|
|
1043
|
-
})
|
|
1044
|
-
.map((p) => {
|
|
1045
|
-
const pos = (p.position ?? p);
|
|
1046
|
-
const szi = parseFiniteVenueNumber(pos.szi, "position.szi", "hyperliquid");
|
|
1047
|
-
const positionValue = pos.positionValue !== undefined && pos.positionValue !== null
|
|
1048
|
-
? parseFiniteVenueNumber(pos.positionValue, "position.positionValue", "hyperliquid")
|
|
1049
|
-
: 0;
|
|
1050
|
-
return {
|
|
1051
|
-
symbol: this._displaySymbol(String(pos.coin ?? "")),
|
|
1052
|
-
side: szi > 0 ? "long" : "short",
|
|
1053
|
-
size: String(Math.abs(szi)),
|
|
1054
|
-
entryPrice: String(pos.entryPx ?? "0"),
|
|
1055
|
-
// clearinghouseState has no per-position mark: positionValue / |szi| is computed, so it is
|
|
1056
|
-
// formatted (no "2727.2000000000003").
|
|
1057
|
-
markPrice: szi !== 0 && positionValue !== 0
|
|
1058
|
-
? formatComputedPrice(positionValue / Math.abs(szi))
|
|
1059
|
-
: String(pos.markPx ?? "0"),
|
|
1060
|
-
liquidationPrice: pos.liquidationPx != null ? String(pos.liquidationPx) : null,
|
|
1061
|
-
unrealizedPnl: String(pos.unrealizedPnl ?? "0"),
|
|
1062
|
-
leverage: parseFiniteVenueNumber(pos.leverage?.value, "position.leverage.value", "hyperliquid", { defaultValue: 1 }),
|
|
1063
|
-
};
|
|
1064
|
-
});
|
|
1309
|
+
return hlPositionRows(await this._getClearinghouseState(), (coin) => this._displaySymbol(coin));
|
|
1065
1310
|
}
|
|
1066
1311
|
/**
|
|
1067
1312
|
* The leverage this account applies to `symbol`, position or not — info `activeAssetData`
|
|
@@ -1234,11 +1479,11 @@ export class HyperliquidAdapter {
|
|
|
1234
1479
|
}
|
|
1235
1480
|
}
|
|
1236
1481
|
/**
|
|
1237
|
-
* Market order = slippage-bounded IOC limit
|
|
1482
|
+
* Market order = slippage-bounded IOC limit for
|
|
1238
1483
|
* native perps and HIP-3 dexes alike. Replaces the SDK's marketOpen/marketClose
|
|
1239
1484
|
* (hidden built-in slippage) and the former HIP-3 5% constant.
|
|
1240
1485
|
*
|
|
1241
|
-
* Reference price (the single declared source
|
|
1486
|
+
* Reference price (the single declared source): the l2Book mid
|
|
1242
1487
|
* (best bid + best ask) / 2 of this coin — see referencePrice(). No runtime
|
|
1243
1488
|
* substitute (mark, allMids, last trade): a missing side → PRICE_STALE before
|
|
1244
1489
|
* anything is sent. Protection = reference ± opts.slippage.bps, aligned toward
|
|
@@ -1287,10 +1532,11 @@ export class HyperliquidAdapter {
|
|
|
1287
1532
|
/**
|
|
1288
1533
|
* The single declared reference price for market-type protection prices:
|
|
1289
1534
|
* l2Book mid = (best bid + best ask) / 2. Works for native perps and HIP-3
|
|
1290
|
-
* (
|
|
1535
|
+
* (_l2Book passes the dex). Either side missing / non-positive → PRICE_STALE. Not gated on
|
|
1536
|
+
* delisting: a reduce-only market close of a delisted asset is sent (the venue decides).
|
|
1291
1537
|
*/
|
|
1292
1538
|
async referencePrice(symbol) {
|
|
1293
|
-
const book = await this.
|
|
1539
|
+
const book = await this._l2Book(symbol);
|
|
1294
1540
|
const bid = Number(book.bids[0]?.[0]);
|
|
1295
1541
|
const ask = Number(book.asks[0]?.[0]);
|
|
1296
1542
|
if (!(bid > 0) || !(ask > 0) || !Number.isFinite(bid) || !Number.isFinite(ask)) {
|
|
@@ -1531,7 +1777,7 @@ export class HyperliquidAdapter {
|
|
|
1531
1777
|
return hlAlignPriceToGrid(price, this.getSzDecimals(symbol), 6, side === "buy" ? "down" : "up");
|
|
1532
1778
|
}
|
|
1533
1779
|
async limitOrder(symbol, side, price, size, opts) {
|
|
1534
|
-
// TIF aliases
|
|
1780
|
+
// TIF aliases: GTC/IOC/ALO or native spellings; FOK → CAPABILITY_UNSUPPORTED.
|
|
1535
1781
|
let action;
|
|
1536
1782
|
const cloid = opts?.clientOrderId === undefined ? null : HyperliquidAdapter.requireCloid(opts.clientOrderId);
|
|
1537
1783
|
try { // pre-send (see marketOrder)
|
|
@@ -1680,7 +1926,7 @@ export class HyperliquidAdapter {
|
|
|
1680
1926
|
}
|
|
1681
1927
|
// ── Interface methods ──
|
|
1682
1928
|
/**
|
|
1683
|
-
* Amend
|
|
1929
|
+
* Amend via HL native batchModify. `size` = the NEW
|
|
1684
1930
|
* open (unfilled) size — native-modify semantics, not the original total. The
|
|
1685
1931
|
* order's own tif (Gtc/Alo) and reduceOnly are read from frontendOpenOrders
|
|
1686
1932
|
* and preserved; nothing is hardcoded.
|
|
@@ -1769,33 +2015,84 @@ export class HyperliquidAdapter {
|
|
|
1769
2015
|
reduceOnly: opts?.reduceOnly ?? true,
|
|
1770
2016
|
});
|
|
1771
2017
|
}
|
|
1772
|
-
|
|
2018
|
+
/**
|
|
2019
|
+
* recentTrades: the venue's last HL_RECENT_TRADES_CAP (10) trades — no count parameter, no
|
|
2020
|
+
* paging. `limit` undefined = the venue max (10); an explicit `limit` above 10 is INVALID_PARAMS
|
|
2021
|
+
* naming the cap (never fewer rows than asked, silently). Row `symbol` is the raw venue coin
|
|
2022
|
+
* ("BTC", "km:US500"), as in the WS codec — not the "BTC-PERP" display name of `market list`.
|
|
2023
|
+
*/
|
|
2024
|
+
async getRecentTrades(symbol, limit) {
|
|
2025
|
+
const n = limit ?? HL_RECENT_TRADES_CAP;
|
|
2026
|
+
if (!(Number.isInteger(n) && n > 0)) {
|
|
2027
|
+
throw new PerpError("INVALID_PARAMS", `Hyperliquid trades: limit must be a positive integer (got ${limit})`, { exchange: "hyperliquid" });
|
|
2028
|
+
}
|
|
2029
|
+
if (n > HL_RECENT_TRADES_CAP) {
|
|
2030
|
+
throw new PerpError("INVALID_PARAMS", `Hyperliquid trades: limit ${n} exceeds the venue cap of ${HL_RECENT_TRADES_CAP} (recentTrades serves only the last ${HL_RECENT_TRADES_CAP} trades, no paging)`, {
|
|
2031
|
+
exchange: "hyperliquid", remediation: `Use --limit ${HL_RECENT_TRADES_CAP} or less`,
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
// recentTrades answers HTTP 500 "null" for an unlisted coin (live 2026-10-14), which would read
|
|
2035
|
+
// as a retryable venue error: resolve the coin against the asset map first (SYMBOL_NOT_FOUND).
|
|
2036
|
+
await this._listedMarket(symbol);
|
|
1773
2037
|
const trades = await this._infoPost({ type: "recentTrades", coin: this._venueCoin(symbol) });
|
|
1774
|
-
|
|
1775
|
-
.
|
|
2038
|
+
if (trades === null)
|
|
2039
|
+
throw this._unknownSymbol(symbol);
|
|
2040
|
+
return trades
|
|
2041
|
+
.slice(0, n)
|
|
1776
2042
|
.map((t) => ({
|
|
1777
2043
|
time: Number(t.time ?? 0),
|
|
1778
2044
|
symbol: String(t.coin ?? symbol.toUpperCase()),
|
|
1779
2045
|
side: String(t.side) === "B" ? "buy" : "sell",
|
|
1780
2046
|
price: String(t.px ?? "0"),
|
|
1781
2047
|
size: String(t.sz ?? ""),
|
|
1782
|
-
fee:
|
|
2048
|
+
fee: null, // the public tape carries no fee
|
|
1783
2049
|
}));
|
|
1784
2050
|
}
|
|
2051
|
+
/**
|
|
2052
|
+
* candleSnapshot. Only the most recent HL_CANDLE_CAP (5000) candles of an interval exist (docs)
|
|
2053
|
+
* and older ones cannot be paged to: a `startTime` before hlEarliestCandleStart() is refused
|
|
2054
|
+
* up front (INVALID_PARAMS naming the earliest allowed start) — never a silently shortened
|
|
2055
|
+
* range. `closed` (additive) is false on the in-progress candle (close time `T`, or open +
|
|
2056
|
+
* interval when `T` is absent, still in the future). "1M" = 30-day buckets; "1w" opens
|
|
2057
|
+
* Thursday 00:00 UTC.
|
|
2058
|
+
*/
|
|
1785
2059
|
async getKlines(symbol, interval, startTime, endTime) {
|
|
2060
|
+
const ivMs = HL_CANDLE_INTERVAL_MS[interval];
|
|
2061
|
+
if (ivMs === undefined) {
|
|
2062
|
+
throw new PerpError("INVALID_PARAMS", `Hyperliquid has no ${JSON.stringify(interval)} candles`, {
|
|
2063
|
+
exchange: "hyperliquid", remediation: `Use one of: ${Object.keys(HL_CANDLE_INTERVAL_MS).join(", ")}`,
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
const now = Date.now();
|
|
2067
|
+
const earliest = hlEarliestCandleStart(interval, now);
|
|
2068
|
+
if (startTime < earliest) {
|
|
2069
|
+
throw new PerpError("INVALID_PARAMS", `Hyperliquid serves only the most recent ${HL_CANDLE_CAP} ${interval} candles: the earliest allowed start is ${new Date(earliest).toISOString()} (${earliest}); requested ${new Date(startTime).toISOString()}`, {
|
|
2070
|
+
exchange: "hyperliquid", remediation: `Use --start ${earliest} or later, or a longer interval`,
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
await this._listedMarket(symbol); // unlisted coin: SYMBOL_NOT_FOUND (candleSnapshot answers HTTP 500 "null")
|
|
1786
2074
|
const candles = await this._infoPost({
|
|
1787
2075
|
type: "candleSnapshot",
|
|
1788
2076
|
req: { coin: this._venueCoin(symbol), interval, startTime, endTime },
|
|
1789
2077
|
});
|
|
1790
|
-
return (candles ?? []).map((c) =>
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2078
|
+
return (candles ?? []).map((c) => {
|
|
2079
|
+
assertCandleInterval(this.name, interval, c.i);
|
|
2080
|
+
const time = Number(c.t ?? 0);
|
|
2081
|
+
const closeAt = Number.isFinite(Number(c.T)) && c.T !== undefined && c.T !== null ? Number(c.T) : time + ivMs;
|
|
2082
|
+
return {
|
|
2083
|
+
time,
|
|
2084
|
+
open: String(c.o ?? "0"),
|
|
2085
|
+
high: String(c.h ?? "0"),
|
|
2086
|
+
low: String(c.l ?? "0"),
|
|
2087
|
+
close: String(c.c ?? "0"),
|
|
2088
|
+
// absent / non-numeric → null (never "")
|
|
2089
|
+
volume: typeof c.v === "string" && c.v.trim() !== "" && Number.isFinite(Number(c.v)) ? c.v
|
|
2090
|
+
: typeof c.v === "number" && Number.isFinite(c.v) ? String(c.v) : null,
|
|
2091
|
+
quoteVolume: null, // candleSnapshot carries base volume only
|
|
2092
|
+
trades: Number(c.n ?? 0),
|
|
2093
|
+
closed: closeAt < now,
|
|
2094
|
+
};
|
|
2095
|
+
});
|
|
1799
2096
|
}
|
|
1800
2097
|
/**
|
|
1801
2098
|
* Order history: POST /info {type:"historicalOrders"} ("Retrieve a user's historical orders",
|
|
@@ -1942,15 +2239,59 @@ export class HyperliquidAdapter {
|
|
|
1942
2239
|
return coin.startsWith(`${this._dex}:`);
|
|
1943
2240
|
return !/[:@#/]/.test(coin);
|
|
1944
2241
|
}
|
|
2242
|
+
/**
|
|
2243
|
+
* The last `limit` hourly funding settlements. HL funding is hourly, so the window starts
|
|
2244
|
+
* `limit + 1` hours back (one spare hour for a settlement not yet published), and fundingHistory
|
|
2245
|
+
* pages are read forward from the last row's time (docs: at most 500 elements per response;
|
|
2246
|
+
* "use the last returned timestamp as the next startTime") — deduped by time, oldest first.
|
|
2247
|
+
*/
|
|
1945
2248
|
async getFundingHistory(symbol, limit = 10) {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2249
|
+
if (!(Number.isInteger(limit) && limit > 0)) {
|
|
2250
|
+
throw new PerpError("INVALID_PARAMS", `Hyperliquid funding history: limit must be a positive integer (got ${limit})`, { exchange: "hyperliquid" });
|
|
2251
|
+
}
|
|
2252
|
+
if (limit > HL_FUNDING_HISTORY_MAX_LIMIT) {
|
|
2253
|
+
throw new PerpError("INVALID_PARAMS", `Hyperliquid funding history: limit ${limit} exceeds ${HL_FUNDING_HISTORY_MAX_LIMIT} hourly rows`, { exchange: "hyperliquid", remediation: `Use --limit ${HL_FUNDING_HISTORY_MAX_LIMIT} or less` });
|
|
2254
|
+
}
|
|
2255
|
+
await this._listedMarket(symbol); // unlisted coin: SYMBOL_NOT_FOUND (fundingHistory answers HTTP 500)
|
|
2256
|
+
const history = await this._fundingHistorySince(this._venueCoin(symbol), Date.now() - (limit + 1) * 60 * 60 * 1000);
|
|
2257
|
+
return history.slice(-limit).map((h) => ({
|
|
1949
2258
|
time: Number(h.time ?? 0),
|
|
1950
2259
|
rate: venueRateField(h.fundingRate, "fundingHistory fundingRate", "hyperliquid"),
|
|
1951
2260
|
price: null,
|
|
2261
|
+
intervalHours: 1,
|
|
2262
|
+
rateKind: "lastSettled",
|
|
1952
2263
|
}));
|
|
1953
2264
|
}
|
|
2265
|
+
/** Every fundingHistory row of `coin` since `startTime`, paged forward (500 per response), deduped by time, oldest first. */
|
|
2266
|
+
async _fundingHistorySince(coin, startTime) {
|
|
2267
|
+
const byTime = new Map();
|
|
2268
|
+
let from = startTime;
|
|
2269
|
+
for (let page = 0;; page++) {
|
|
2270
|
+
if (page >= HL_FUNDING_HISTORY_MAX_PAGES) {
|
|
2271
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid fundingHistory needs more than ${HL_FUNDING_HISTORY_MAX_PAGES} pages — the result would be incomplete`, { exchange: "hyperliquid" });
|
|
2272
|
+
}
|
|
2273
|
+
const rows = await this._infoPost({ type: "fundingHistory", coin, startTime: from });
|
|
2274
|
+
if (!Array.isArray(rows)) {
|
|
2275
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid fundingHistory returned no list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "hyperliquid" });
|
|
2276
|
+
}
|
|
2277
|
+
let last = -Infinity;
|
|
2278
|
+
let fresh = 0;
|
|
2279
|
+
for (const h of rows) {
|
|
2280
|
+
const t = Number(h.time);
|
|
2281
|
+
if (!Number.isFinite(t))
|
|
2282
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid fundingHistory row without a numeric time: ${JSON.stringify(h).slice(0, 160)}`, { exchange: "hyperliquid" });
|
|
2283
|
+
last = Math.max(last, t);
|
|
2284
|
+
if (!byTime.has(t)) {
|
|
2285
|
+
byTime.set(t, h);
|
|
2286
|
+
fresh++;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
if (rows.length < HL_INFO_PAGE || fresh === 0)
|
|
2290
|
+
break;
|
|
2291
|
+
from = last;
|
|
2292
|
+
}
|
|
2293
|
+
return [...byTime.entries()].sort((a, b) => a[0] - b[0]).map(([, h]) => h);
|
|
2294
|
+
}
|
|
1954
2295
|
// ────────────────────────────────────────────────────────────
|
|
1955
2296
|
// Extended methods (from Python SDK / nktkas TS SDK analysis)
|
|
1956
2297
|
// ────────────────────────────────────────────────────────────
|
|
@@ -1958,7 +2299,7 @@ export class HyperliquidAdapter {
|
|
|
1958
2299
|
* Place a trigger order (stop loss / take profit).
|
|
1959
2300
|
* Python SDK: order() with trigger type {"trigger": {triggerPx, isMarket, tpsl: "tp"|"sl"}}
|
|
1960
2301
|
*
|
|
1961
|
-
* Pricing
|
|
2302
|
+
* Pricing:
|
|
1962
2303
|
* - isMarket (default): HL native market trigger; `p` = trigger ± 10 % (the venue's documented
|
|
1963
2304
|
* market TP/SL slippage tolerance, HL_MARKET_TPSL_SLIPPAGE_BPS), aligned toward crossing.
|
|
1964
2305
|
* - limit trigger with an explicit `limitPrice`: that price, rounded to HL's grid.
|
|
@@ -2479,31 +2820,33 @@ export class HyperliquidAdapter {
|
|
|
2479
2820
|
return this._sendExchangeAction({ type: "claimRewards" });
|
|
2480
2821
|
}
|
|
2481
2822
|
/**
|
|
2482
|
-
* List
|
|
2823
|
+
* List the HIP-3 deployed perp dexes: allPerpMetas (universe per dex; index 0 = native perps)
|
|
2824
|
+
* joined by index with perpDexs (name, deployer — live 2026-10-19 both list the dexes in the
|
|
2825
|
+
* same order). `assets` holds the LIVE (non-delisted) assets only; `delistedAssets` the rest and
|
|
2826
|
+
* `delisted` is true when every asset of the dex is delisted (km, flx, hyna, cash, vntl, abcd on
|
|
2827
|
+
* 2026-10-19). Delisted dexes stay listed: accounts may still hold positions there.
|
|
2483
2828
|
*/
|
|
2484
2829
|
async listDeployedDexes() {
|
|
2485
|
-
const allMetas = await
|
|
2830
|
+
const [allMetas, dexList] = await Promise.all([
|
|
2831
|
+
this._infoPost({ type: "allPerpMetas" }),
|
|
2832
|
+
withCache(`mkt:hl:${this._network}:perpDexs`, HL_MARKET_META_TTL_MS, () => this._infoPost({ type: "perpDexs" })),
|
|
2833
|
+
]);
|
|
2486
2834
|
if (!Array.isArray(allMetas))
|
|
2487
2835
|
return [];
|
|
2488
2836
|
const dexes = [];
|
|
2489
|
-
// allPerpMetas returns an array: [nativePerps, dex1, dex2, ...]
|
|
2490
|
-
// Each entry has { universe, marginTables, collateralToken }
|
|
2491
|
-
// Dex name is derived from the asset prefix (e.g., "xyz:TSLA" → "xyz")
|
|
2492
|
-
// Skip index 0 (native/validator perps — no prefix)
|
|
2493
2837
|
for (let i = 1; i < allMetas.length; i++) {
|
|
2494
|
-
const
|
|
2495
|
-
const universe = (meta.universe ?? []);
|
|
2838
|
+
const universe = (allMetas[i]?.universe ?? []);
|
|
2496
2839
|
if (universe.length === 0)
|
|
2497
2840
|
continue;
|
|
2498
|
-
// Extract dex name from the first asset's prefix
|
|
2499
2841
|
const firstAsset = universe[0].name;
|
|
2500
2842
|
const colonIdx = firstAsset.indexOf(":");
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2843
|
+
const prefix = colonIdx > 0 ? firstAsset.slice(0, colonIdx) : `dex-${i}`;
|
|
2844
|
+
const entry = Array.isArray(dexList) ? dexList[i] : null;
|
|
2845
|
+
// The deployer is read from the perpDexs entry of the same dex (by index, checked by name).
|
|
2846
|
+
const deployer = entry?.name === prefix && typeof entry.deployer === "string" ? entry.deployer : "";
|
|
2847
|
+
const live = universe.filter(a => a.isDelisted !== true).map(a => a.name);
|
|
2848
|
+
const gone = universe.filter(a => a.isDelisted === true).map(a => a.name);
|
|
2849
|
+
dexes.push({ name: prefix, deployer, assets: live, delistedAssets: gone, delisted: live.length === 0 });
|
|
2507
2850
|
}
|
|
2508
2851
|
return dexes;
|
|
2509
2852
|
}
|
|
@@ -2574,6 +2917,11 @@ export class HyperliquidAdapter {
|
|
|
2574
2917
|
// share an aggregated weight limit of 1200 per minute") → typed RATE_LIMITED.
|
|
2575
2918
|
if (res.status === 429)
|
|
2576
2919
|
throw rateLimitedError("hyperliquid", res.headers.get("retry-after"), `info ${String(body.type)}: ${text}`);
|
|
2920
|
+
// HTTP 422 "Failed to deserialize the JSON body into the target type" = the request is
|
|
2921
|
+
// malformed (bad interval / field type, live 2026-10-19): a caller error, not retryable.
|
|
2922
|
+
if (res.status === 422) {
|
|
2923
|
+
throw new PerpError("INVALID_PARAMS", `Hyperliquid info ${String(body.type)} rejected the request (422): ${text.slice(0, 200)}`, { exchange: "hyperliquid" });
|
|
2924
|
+
}
|
|
2577
2925
|
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid info ${String(body.type)} failed (${res.status}): ${text.slice(0, 200)}`, { exchange: "hyperliquid" });
|
|
2578
2926
|
}
|
|
2579
2927
|
return res.json();
|