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
package/dist/commands/market.js
CHANGED
|
@@ -1,49 +1,262 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sortByTime, tapeSkippedOf } from "../exchanges/interface.js";
|
|
2
|
+
import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors, normalizeSymbolArg } from "../utils.js";
|
|
2
3
|
import chalk from "chalk";
|
|
3
4
|
import { PerpError } from "../errors.js";
|
|
4
5
|
import { midDecimal } from "../exchanges/precision.js";
|
|
5
|
-
import {
|
|
6
|
-
import { listVenuesWithRole } from "../exchanges/venue.js";
|
|
6
|
+
import { isDexCapable, klinesNeedAuth } from "../exchanges/capabilities.js";
|
|
7
|
+
import { getVenue, listVenuesWithRole, tryResolveVenueId, unofficialApiNote, withFundingVerified } from "../exchanges/venue.js";
|
|
7
8
|
import "../exchanges/venues/index.js";
|
|
8
|
-
/**
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Venues of the multi-venue `market list|prices` (no -e): every venue with the marketData role
|
|
11
|
+
* and a (public) adapter — market-data-only venues (TxFlow, Variational, GRVT, Extended) included.
|
|
12
|
+
* They were left out in 0.15.0 because the list was the account-role venues.
|
|
13
|
+
*/
|
|
14
|
+
export const MARKET_DATA_VENUES = listVenuesWithRole("marketData").filter(v => v.create !== null).map(v => v.id);
|
|
15
|
+
/**
|
|
16
|
+
* `--limit` / `-l` / `--depth`: a positive integer, the same rule on every venue (parseInt took
|
|
17
|
+
* "abc" as NaN and "-5" as a negative slice). Anything else → INVALID_PARAMS before any request.
|
|
18
|
+
*/
|
|
19
|
+
export function parsePositiveIntArg(raw, flag) {
|
|
20
|
+
const t = raw.trim();
|
|
21
|
+
if (!/^\d+$/.test(t) || Number(t) < 1 || !Number.isSafeInteger(Number(t))) {
|
|
22
|
+
throw new PerpError("INVALID_PARAMS", `${flag} must be a positive integer (got "${raw}")`, { remediation: `Pass e.g. ${flag} 10` });
|
|
23
|
+
}
|
|
24
|
+
return Number(t);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Most order-book levels per side each venue serves (measured live 2026-10-19; the request size
|
|
28
|
+
* where the venue takes one): one rule for every venue — `--depth` above it is INVALID_PARAMS
|
|
29
|
+
* naming the max, never clamped or silently short. Absent = the full book (Extended).
|
|
30
|
+
*/
|
|
31
|
+
export const BOOK_DEPTH_MAX = {
|
|
32
|
+
hyperliquid: 20, pacifica: 10, lighter: 50, "lighter-rh": 50, aster: 50, arcus: 50, ondo: 50,
|
|
33
|
+
qfex: 20, txflow: 20, risex: 250, grvt: 500,
|
|
34
|
+
};
|
|
35
|
+
export function assertBookDepth(venue, depth) {
|
|
36
|
+
const id = tryResolveVenueId(venue);
|
|
37
|
+
const max = id === null ? undefined : BOOK_DEPTH_MAX[id];
|
|
38
|
+
if (max !== undefined && depth > max) {
|
|
39
|
+
throw new PerpError("INVALID_PARAMS", `--depth ${depth} exceeds ${venue}'s order book (max ${max} levels per side)`, {
|
|
40
|
+
exchange: venue, remediation: `Pass --depth 1..${max}`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `--start` / `--end`: unix ms (digits) or an ISO-8601 date / time (`Date.parse`; "2026-10-18" is
|
|
46
|
+
* UTC midnight). parseInt("2026-10-18") read 2026 ms. Non-finite → INVALID_PARAMS.
|
|
47
|
+
*/
|
|
48
|
+
export function parseTimeArg(raw, flag) {
|
|
49
|
+
const t = raw.trim();
|
|
50
|
+
const ms = /^\d+$/.test(t) ? Number(t) : /^\d{4}-\d{2}-\d{2}/.test(t) ? Date.parse(t) : NaN;
|
|
51
|
+
// A unix-seconds value (10 digits today) read as ms lands in 1970: refused with a hint.
|
|
52
|
+
if (/^\d+$/.test(t) && ms < 1e11) {
|
|
53
|
+
throw new PerpError("INVALID_PARAMS", `${flag} ${raw} looks like unix seconds; pass milliseconds or an ISO date`, {
|
|
54
|
+
remediation: `Pass e.g. ${flag} ${t}000 or ${flag} 2026-10-18T00:00:00Z`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isFinite(ms)) {
|
|
58
|
+
throw new PerpError("INVALID_PARAMS", `${flag} must be unix milliseconds or an ISO date (got "${raw}")`, {
|
|
59
|
+
remediation: `Pass e.g. ${flag} 1792368000000 or ${flag} 2026-10-18T00:00:00Z`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return ms;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bare market symbol of a CLI argument as `market info` matches it against getMarkets() rows:
|
|
66
|
+
* "btc-perp" → "BTC", Extended "BTC-USD" / "TSLA_24_5-USD" → "BTC" / "TSLA", TxFlow "BTC-USDC" → "BTC"
|
|
67
|
+
* (book / funding resolve these through the venue; info read the raw argument and missed them).
|
|
68
|
+
* HIP-3 "xyz:XYZ100" keeps its prefix.
|
|
69
|
+
*/
|
|
70
|
+
export function bareSymbolOf(arg) {
|
|
71
|
+
return normalizeSymbolArg(arg.trim()).replace(/-PERP$/, "").replace(/-(USD|USDC|USDT)$/, "").replace(/_24_5$/, "");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* History order, one rule for every venue: trades and funding history newest first, candles
|
|
75
|
+
* oldest first (Aster / QFEX answered funding oldest first, GRVT trades oldest first). A stable
|
|
76
|
+
* copy — the adapter's array (and its non-enumerable tape metadata) is left as is.
|
|
77
|
+
*/
|
|
78
|
+
export function newestFirst(rows) {
|
|
79
|
+
return sortByTime(rows, "desc");
|
|
80
|
+
}
|
|
81
|
+
/** Human reason of a listed-but-untradable market, keyed on `status` (null when none is known). */
|
|
82
|
+
const NOT_TRADABLE_REASON = {
|
|
83
|
+
nonUsdQuote: "non-USD quote — readable, not tradable through perp-cli",
|
|
84
|
+
offHours: "outside the underlying's trading hours",
|
|
85
|
+
reduceOnly: "reduce-only",
|
|
86
|
+
halted: "trading halted",
|
|
87
|
+
};
|
|
88
|
+
export function notTradableReason(m) {
|
|
89
|
+
if (m.tradable !== false)
|
|
90
|
+
return null;
|
|
91
|
+
if (typeof m.status === "string" && m.status !== "")
|
|
92
|
+
return NOT_TRADABLE_REASON[m.status] ?? m.status;
|
|
93
|
+
return "not tradable right now";
|
|
94
|
+
}
|
|
95
|
+
/** Symbol cell; a listed market that is not tradable is marked with its reason. */
|
|
96
|
+
function symbolCell(m) {
|
|
97
|
+
const why = notTradableReason(m);
|
|
98
|
+
return why === null ? chalk.white.bold(m.symbol) : `${chalk.white.bold(m.symbol)} ${chalk.yellow(`(${why})`)}`;
|
|
99
|
+
}
|
|
100
|
+
/** Multi-venue human output: the footnote of every listed venue read through an unofficial API. */
|
|
101
|
+
function printUnofficialNotes(venues) {
|
|
102
|
+
for (const ex of venues) {
|
|
103
|
+
const id = tryResolveVenueId(ex);
|
|
104
|
+
const note = id === null ? null : unofficialApiNote(getVenue(id));
|
|
105
|
+
if (note !== null)
|
|
106
|
+
console.log(chalk.yellow(` * ${note}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* meta.warnings for market rows whose fundingRate is null for a known reason (Lighter: the
|
|
111
|
+
* settled-rate snapshot could not be read) — the reason reaches --json, not only stderr.
|
|
112
|
+
*/
|
|
113
|
+
function fundingWarnings(adapters, rows = null) {
|
|
114
|
+
// Tied to the rows served: no warning next to rows that all carry a rate (a cached listing).
|
|
115
|
+
if (rows !== null && !rows.some(r => r.fundingRate === null))
|
|
116
|
+
return undefined;
|
|
117
|
+
const w = adapters.map(a => a.fundingWarning?.() ?? null).filter((x) => x !== null);
|
|
118
|
+
return w.length > 0 ? { warnings: w } : undefined;
|
|
119
|
+
}
|
|
10
120
|
/** null = venue does not publish the value → "-" (never rendered as $0). */
|
|
11
121
|
const usdOrDash = (v) => (v === null ? chalk.gray("-") : `$${formatUsd(v)}`);
|
|
122
|
+
/** Non-USD quote currency of a row (QFEX "SAMSUNG-KRW": quoteAsset "KRW"); null = USD or not stated. */
|
|
123
|
+
const nonUsdQuoteOf = (m) => {
|
|
124
|
+
const q = m.quoteAsset;
|
|
125
|
+
return typeof q === "string" && q !== "USD" ? q : null;
|
|
126
|
+
};
|
|
127
|
+
/** A price of row `m` in its quote currency: "$1.23" for USD, "213,500 KRW" otherwise; null → "-". */
|
|
128
|
+
const priceOrDash = (v, m) => {
|
|
129
|
+
const q = nonUsdQuoteOf(m);
|
|
130
|
+
return v === null ? chalk.gray("-") : q === null ? `$${formatUsd(v)}` : `${formatUsd(v)} ${q}`;
|
|
131
|
+
};
|
|
132
|
+
/** 24h volume: USD, or (no USD figure) the venue's quote-currency notional labelled with its currency. */
|
|
133
|
+
const volumeCell = (m) => {
|
|
134
|
+
const local = m.volume24hQuote;
|
|
135
|
+
return m.volume24h === null && typeof local === "string" ? priceOrDash(local, m) : usdOrDash(m.volume24h);
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Funding cell. On a venue whose funding interval is per market (VenueDefinition.fundingRateBasis
|
|
139
|
+
* "perMarket": Variational, Aster, GRVT) the raw rate is labelled with its interval ("+0.0079%/8h"), since
|
|
140
|
+
* the rows of one table cover different periods.
|
|
141
|
+
*/
|
|
142
|
+
function fundingCell(m, perMarket) {
|
|
143
|
+
if (m.fundingRate == null)
|
|
144
|
+
return chalk.gray("N/A");
|
|
145
|
+
const hours = perMarket ? m.fundingHours ?? null : null;
|
|
146
|
+
return hours === null ? formatPercent(m.fundingRate) : `${formatPercent(m.fundingRate)}${chalk.gray(`/${hours}h`)}`;
|
|
147
|
+
}
|
|
148
|
+
/** True when `venueName` is a registered venue whose funding interval is per market. */
|
|
149
|
+
function perMarketFunding(venueName) {
|
|
150
|
+
const id = tryResolveVenueId(venueName);
|
|
151
|
+
return id !== null && getVenue(id).fundingRateBasis === "perMarket";
|
|
152
|
+
}
|
|
153
|
+
/** Base asset of a market symbol: "BTC-PERP" → "BTC", "xyz:XYZ100" → "XYZ100". */
|
|
154
|
+
const baseAssetOf = (symbol) => symbol.split(":").pop().replace(/-PERP$/i, "");
|
|
155
|
+
/**
|
|
156
|
+
* Open interest is published in BASE-asset units by every venue (ExchangeMarketInfo.openInterest),
|
|
157
|
+
* so it is shown as "<n> <BASE>" — never with a "$". `withUsd` adds the notional at the mark
|
|
158
|
+
* price, labelled as such; null (not published) → "-".
|
|
159
|
+
*/
|
|
160
|
+
function formatOpenInterest(m, withUsd) {
|
|
161
|
+
if (m.openInterest === null)
|
|
162
|
+
return chalk.gray("-");
|
|
163
|
+
const oi = Number(m.openInterest);
|
|
164
|
+
const base = `${Number.isFinite(oi) ? oi.toLocaleString("en-US", { maximumFractionDigits: 4 }) : m.openInterest} ${baseAssetOf(m.symbol)}`;
|
|
165
|
+
const mark = Number(m.markPrice);
|
|
166
|
+
if (!withUsd || !Number.isFinite(oi) || !(mark > 0))
|
|
167
|
+
return base;
|
|
168
|
+
return `${base} ${chalk.gray(`(≈ ${priceOrDash(String(oi * mark), m)} at mark)`)}`;
|
|
169
|
+
}
|
|
170
|
+
function printRfqQuotes(q, json) {
|
|
171
|
+
if (json)
|
|
172
|
+
return printJson(jsonOk(q));
|
|
173
|
+
console.log(chalk.cyan.bold(`\n RFQ quotes: ${q.ticker} (${q.venue}) — not an order book\n`));
|
|
174
|
+
console.log(` Mark: $${formatUsd(q.markPrice)} Base spread: ${q.baseSpreadBps === null ? chalk.gray("-") : `${Number(q.baseSpreadBps).toFixed(2)} bps`} Quoted at: ${q.updatedAt ?? chalk.gray("-")}`);
|
|
175
|
+
if (q.quotes.length === 0) {
|
|
176
|
+
console.log(chalk.gray("\n No quotes published right now (the market may be closed).\n"));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log(makeTable(["Size", "Bid", "Ask", "Spread"], q.quotes.map(t => {
|
|
180
|
+
const bid = Number(t.bid);
|
|
181
|
+
const ask = Number(t.ask);
|
|
182
|
+
const mid = (bid + ask) / 2;
|
|
183
|
+
return [
|
|
184
|
+
t.notionalUsd === null ? "min order" : `$${formatUsd(t.notionalUsd)}`,
|
|
185
|
+
chalk.green(`$${formatUsd(t.bid)}`),
|
|
186
|
+
chalk.red(`$${formatUsd(t.ask)}`),
|
|
187
|
+
mid > 0 ? `${((ask - bid) / mid * 10_000).toFixed(2)} bps` : chalk.gray("-"),
|
|
188
|
+
];
|
|
189
|
+
})));
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.gray(` ${q.note}\n`));
|
|
192
|
+
}
|
|
193
|
+
function printFundingTerms(t, json) {
|
|
194
|
+
if (json)
|
|
195
|
+
return printJson(jsonOk(t));
|
|
196
|
+
console.log(chalk.cyan.bold(`\n Funding: ${t.ticker} (${t.venue}) — current terms, no history published\n`));
|
|
197
|
+
if (t.swap !== null) {
|
|
198
|
+
const row = (label, r) => r === null
|
|
199
|
+
? [label, chalk.gray("-"), chalk.gray("-"), chalk.gray("-"), chalk.gray("-")]
|
|
200
|
+
: [label, formatPercent(r.intervalLong), formatPercent(r.intervalShort), formatPercent(r.annualizedLong), formatPercent(r.annualizedShort)];
|
|
201
|
+
console.log(makeTable(["Payment", "Long / day", "Short / day", "Long APR", "Short APR"], [row("last applied", t.swap.last), row("next", t.swap.next)]));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(` Rate kind: ${t.rateKind ?? chalk.gray("-")}`);
|
|
205
|
+
console.log(` Interval: ${t.intervalHours === null ? chalk.gray("-") : `${t.intervalHours}h`}`);
|
|
206
|
+
console.log(` Per interval: ${t.intervalRate === null ? chalk.gray("N/A") : formatPercent(t.intervalRate)}`);
|
|
207
|
+
console.log(` Hourly: ${t.hourlyRate === null ? chalk.gray("N/A") : formatPercent(t.hourlyRate)}`);
|
|
208
|
+
console.log(` APR: ${t.annualizedPct === null ? chalk.gray("N/A") : `${t.annualizedPct.toFixed(2)}%`}`);
|
|
209
|
+
}
|
|
210
|
+
console.log(` Next funding: ${t.nextFundingTime === null ? chalk.gray("-") : new Date(t.nextFundingTime).toLocaleString()}`);
|
|
211
|
+
console.log(chalk.gray(` ${t.note}\n`));
|
|
212
|
+
}
|
|
12
213
|
export function registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange,
|
|
13
214
|
/** Credentialed adapter of the selected venue — for market data the venue serves only to an authenticated account. */
|
|
14
215
|
getAccountAdapter) {
|
|
15
216
|
const market = program.command("market").description("Market data commands");
|
|
217
|
+
// Human output for a venue read through an UNOFFICIAL API carries a footnote (JSON rows carry venueApi).
|
|
218
|
+
market.hook("postAction", () => {
|
|
219
|
+
if (isJson())
|
|
220
|
+
return;
|
|
221
|
+
const raw = program.opts().exchange;
|
|
222
|
+
const id = raw === undefined ? null : tryResolveVenueId(raw);
|
|
223
|
+
if (id === null)
|
|
224
|
+
return;
|
|
225
|
+
const note = unofficialApiNote(getVenue(id));
|
|
226
|
+
if (note !== null)
|
|
227
|
+
console.log(chalk.yellow(` * ${note}\n`));
|
|
228
|
+
});
|
|
16
229
|
market
|
|
17
230
|
.command("list")
|
|
18
231
|
.description("List all available markets")
|
|
19
232
|
.option("--hip3", "Include HIP-3 dex markets (Hyperliquid)")
|
|
20
233
|
.action(async (opts) => {
|
|
21
234
|
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
22
|
-
const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"];
|
|
23
|
-
const marketRow = (m) => [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
235
|
+
const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI (base)", "Max Lev"];
|
|
236
|
+
const marketRow = (m, perMarket = false) => [
|
|
237
|
+
symbolCell(m),
|
|
238
|
+
priceOrDash(m.markPrice, m),
|
|
239
|
+
priceOrDash(m.indexPrice, m),
|
|
240
|
+
fundingCell(m, perMarket),
|
|
241
|
+
volumeCell(m),
|
|
242
|
+
formatOpenInterest(m, false),
|
|
30
243
|
m.maxLeverage === null ? chalk.gray("-") : String(m.maxLeverage) + "x",
|
|
31
244
|
];
|
|
32
245
|
// Multi-exchange mode
|
|
33
246
|
if (!explicitExchange && getAdapterForExchange) {
|
|
34
247
|
const grouped = {};
|
|
35
248
|
const errors = {};
|
|
36
|
-
|
|
249
|
+
const listed = [];
|
|
250
|
+
await Promise.all(MARKET_DATA_VENUES.map(async (ex) => {
|
|
37
251
|
try {
|
|
38
252
|
const adapter = await getAdapterForExchange(ex);
|
|
39
253
|
grouped[ex] = await adapter.getMarkets();
|
|
254
|
+
listed.push(adapter);
|
|
40
255
|
// --hip3: fetch HIP-3 dex markets
|
|
41
256
|
if (opts.hip3 && ex === "hyperliquid" && isDexCapable(adapter)) {
|
|
42
257
|
const dexes = await adapter.listDeployedDexes();
|
|
43
258
|
await Promise.allSettled(dexes.map(async (d) => {
|
|
44
|
-
const
|
|
45
|
-
dexAdapter.setDex(d.name);
|
|
46
|
-
const markets = await dexAdapter.getMarkets();
|
|
259
|
+
const markets = await adapter.forDex(d.name).getMarkets();
|
|
47
260
|
if (markets.length > 0)
|
|
48
261
|
grouped[`hip3:${d.name}`] = markets;
|
|
49
262
|
}));
|
|
@@ -54,7 +267,7 @@ getAccountAdapter) {
|
|
|
54
267
|
}
|
|
55
268
|
}));
|
|
56
269
|
if (isJson()) {
|
|
57
|
-
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
270
|
+
return printJson(jsonOk({ exchanges: Object.fromEntries(Object.entries(grouped).map(([ex, rows]) => [ex, withFundingVerified(ex, rows)])), errors: Object.keys(errors).length > 0 ? errors : undefined }, fundingWarnings(listed, Object.values(grouped).flat())));
|
|
58
271
|
}
|
|
59
272
|
// Per-exchange tables
|
|
60
273
|
for (const [ex, markets] of Object.entries(grouped)) {
|
|
@@ -63,11 +276,12 @@ getAccountAdapter) {
|
|
|
63
276
|
const isHip3 = ex.startsWith("hip3:");
|
|
64
277
|
const label = isHip3 ? chalk.magenta.bold(` HIP-3: ${ex.slice(5)}`) : chalk.cyan.bold(` ${ex.toUpperCase()}`);
|
|
65
278
|
console.log(`\n${label} ${chalk.gray(`(${markets.length} markets)`)}`);
|
|
66
|
-
console.log(makeTable(HEADERS, markets.map(marketRow)));
|
|
279
|
+
console.log(makeTable(HEADERS, markets.map(m => marketRow(m))));
|
|
67
280
|
}
|
|
68
281
|
for (const [ex, msg] of Object.entries(errors)) {
|
|
69
282
|
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
70
283
|
}
|
|
284
|
+
printUnofficialNotes(Object.keys(grouped));
|
|
71
285
|
return;
|
|
72
286
|
}
|
|
73
287
|
// Single exchange mode
|
|
@@ -78,23 +292,32 @@ getAccountAdapter) {
|
|
|
78
292
|
if (opts.hip3 && isDexCapable(adapter)) {
|
|
79
293
|
const dexes = await adapter.listDeployedDexes();
|
|
80
294
|
await Promise.allSettled(dexes.map(async (d) => {
|
|
81
|
-
const
|
|
82
|
-
dexAdapter.setDex(d.name);
|
|
83
|
-
const m = await dexAdapter.getMarkets();
|
|
295
|
+
const m = await adapter.forDex(d.name).getMarkets();
|
|
84
296
|
if (m.length > 0)
|
|
85
297
|
hip3[d.name] = m;
|
|
86
298
|
}));
|
|
87
299
|
}
|
|
88
300
|
if (isJson()) {
|
|
89
301
|
if (Object.keys(hip3).length > 0) {
|
|
90
|
-
return printJson(jsonOk({ main: markets, hip3 }));
|
|
302
|
+
return printJson(jsonOk({ main: withFundingVerified(adapter.name, markets), hip3: Object.fromEntries(Object.entries(hip3).map(([d, rows]) => [d, withFundingVerified(adapter.name, rows)])) }));
|
|
91
303
|
}
|
|
92
|
-
return printJson(jsonOk(markets));
|
|
304
|
+
return printJson(jsonOk(withFundingVerified(adapter.name, markets), fundingWarnings([adapter], markets)));
|
|
305
|
+
}
|
|
306
|
+
const perMarket = perMarketFunding(adapter.name);
|
|
307
|
+
// Rows that carry an asset class (Extended: crypto / equity / etf / index / commodity / fx / preIpo) show it.
|
|
308
|
+
const classOf = (m) => {
|
|
309
|
+
const c = m.assetClass;
|
|
310
|
+
return typeof c === "string" ? c : null;
|
|
311
|
+
};
|
|
312
|
+
if (markets.length > 0 && markets.every(m => classOf(m) !== null)) {
|
|
313
|
+
console.log(makeTable([...HEADERS, "Class"], markets.map(m => [...marketRow(m, perMarket), classOf(m)])));
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
console.log(makeTable(HEADERS, markets.map(m => marketRow(m, perMarket))));
|
|
93
317
|
}
|
|
94
|
-
console.log(makeTable(HEADERS, markets.map(marketRow)));
|
|
95
318
|
for (const [dex, m] of Object.entries(hip3)) {
|
|
96
319
|
console.log(chalk.magenta.bold(`\n HIP-3: ${dex}`) + chalk.gray(` (${m.length} markets)`));
|
|
97
|
-
console.log(makeTable(HEADERS, m.map(marketRow)));
|
|
320
|
+
console.log(makeTable(HEADERS, m.map(x => marketRow(x))));
|
|
98
321
|
}
|
|
99
322
|
});
|
|
100
323
|
market
|
|
@@ -103,98 +326,109 @@ getAccountAdapter) {
|
|
|
103
326
|
.action(async () => {
|
|
104
327
|
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
105
328
|
const PRICE_HEADERS = ["Symbol", "Mark", "Index", "Funding"];
|
|
106
|
-
const priceRow = (m) => [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
329
|
+
const priceRow = (m, perMarket = false) => [
|
|
330
|
+
symbolCell(m),
|
|
331
|
+
priceOrDash(m.markPrice, m),
|
|
332
|
+
priceOrDash(m.indexPrice, m),
|
|
333
|
+
fundingCell(m, perMarket),
|
|
111
334
|
];
|
|
112
335
|
if (!explicitExchange && getAdapterForExchange) {
|
|
113
336
|
const grouped = {};
|
|
114
337
|
const errors = {};
|
|
115
|
-
|
|
338
|
+
const listed = [];
|
|
339
|
+
await Promise.all(MARKET_DATA_VENUES.map(async (ex) => {
|
|
116
340
|
try {
|
|
117
341
|
const adapter = await getAdapterForExchange(ex);
|
|
118
342
|
grouped[ex] = await adapter.getMarkets();
|
|
343
|
+
listed.push(adapter);
|
|
119
344
|
}
|
|
120
345
|
catch (err) {
|
|
121
346
|
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
122
347
|
}
|
|
123
348
|
}));
|
|
124
349
|
if (isJson()) {
|
|
125
|
-
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
350
|
+
return printJson(jsonOk({ exchanges: Object.fromEntries(Object.entries(grouped).map(([ex, rows]) => [ex, withFundingVerified(ex, rows)])), errors: Object.keys(errors).length > 0 ? errors : undefined }, fundingWarnings(listed, Object.values(grouped).flat())));
|
|
126
351
|
}
|
|
127
352
|
for (const [ex, markets] of Object.entries(grouped)) {
|
|
128
353
|
if (markets.length === 0)
|
|
129
354
|
continue;
|
|
130
355
|
console.log(chalk.cyan.bold(`\n ${ex.toUpperCase()}`) + chalk.gray(` (${markets.length} markets)`));
|
|
131
|
-
console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
|
|
356
|
+
console.log(makeTable(PRICE_HEADERS, markets.map(m => priceRow(m))));
|
|
132
357
|
}
|
|
133
358
|
for (const [ex, msg] of Object.entries(errors))
|
|
134
359
|
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
360
|
+
printUnofficialNotes(Object.keys(grouped));
|
|
135
361
|
return;
|
|
136
362
|
}
|
|
137
363
|
const adapter = await getAdapter();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const rows = prices.map((p) => [
|
|
144
|
-
chalk.white.bold(p.symbol),
|
|
145
|
-
`$${formatUsd(p.mark)}`,
|
|
146
|
-
`$${formatUsd(p.mid)}`,
|
|
147
|
-
`$${formatUsd(p.oracle)}`,
|
|
148
|
-
formatPercent(p.funding),
|
|
149
|
-
]);
|
|
150
|
-
console.log(makeTable(["Symbol", "Mark", "Mid", "Oracle", "Funding"], rows));
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
const markets = await adapter.getMarkets();
|
|
154
|
-
if (isJson())
|
|
155
|
-
return printJson(jsonOk(markets));
|
|
156
|
-
console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
|
|
157
|
-
}
|
|
364
|
+
// One normalized shape on every venue (Pacifica included — it printed the raw /info/prices rows).
|
|
365
|
+
const markets = await adapter.getMarkets();
|
|
366
|
+
if (isJson())
|
|
367
|
+
return printJson(jsonOk(withFundingVerified(adapter.name, markets), fundingWarnings([adapter], markets)));
|
|
368
|
+
console.log(makeTable(PRICE_HEADERS, markets.map(m => priceRow(m, perMarketFunding(adapter.name)))));
|
|
158
369
|
});
|
|
159
370
|
market
|
|
160
371
|
.command("info <symbol>")
|
|
161
372
|
.description("Detailed info for a single market")
|
|
162
373
|
.action(async (symbol) => {
|
|
163
374
|
const adapter = await getAdapter();
|
|
375
|
+
// A venue that serves one market directly (GRVT: one ticker + one margin-rules read instead
|
|
376
|
+
// of the whole fan-out) answers here; SYMBOL_NOT_FOUND comes from its own directory.
|
|
377
|
+
const single = adapter.getMarketInfo;
|
|
378
|
+
if (typeof single === "function") {
|
|
379
|
+
return void await withJsonErrors(isJson(), async () => {
|
|
380
|
+
const one = await single.call(adapter, symbol);
|
|
381
|
+
if (isJson())
|
|
382
|
+
return printJson(jsonOk(withFundingVerified(adapter.name, [one])[0]));
|
|
383
|
+
printMarketInfo(adapter.name, one);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
164
386
|
const markets = await adapter.getMarkets();
|
|
165
387
|
const sym = symbol.toUpperCase();
|
|
388
|
+
const bare = bareSymbolOf(symbol).toUpperCase();
|
|
166
389
|
const m = markets.find((mk) => {
|
|
167
390
|
const s = mk.symbol.toUpperCase();
|
|
168
|
-
|
|
169
|
-
|
|
391
|
+
const sBare = s.replace(/-PERP$/, "");
|
|
392
|
+
return s === sym || sBare === sym || sBare === bare
|
|
393
|
+
|| s.split(":").pop() === sym || s.split(":").pop() === bare; // HIP-3: "km:US500" matches "US500"
|
|
170
394
|
});
|
|
171
395
|
if (!m) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
396
|
+
// Printed (envelope / Error + Remediation) and exit 1 via withJsonErrors.
|
|
397
|
+
return void await withJsonErrors(isJson(), async () => {
|
|
398
|
+
const delistedErr = adapter.delistedError?.(symbol) ?? null;
|
|
399
|
+
if (delistedErr !== null)
|
|
400
|
+
throw delistedErr;
|
|
401
|
+
const delisted = typeof adapter.delistedMessage === "function"
|
|
402
|
+
? adapter.delistedMessage(symbol)
|
|
403
|
+
: null;
|
|
404
|
+
throw new PerpError("SYMBOL_NOT_FOUND", delisted ?? `Market ${normalizeSymbolArg(symbol)} not found on ${adapter.name}`, {
|
|
405
|
+
exchange: adapter.name,
|
|
406
|
+
remediation: `List markets: perp -e ${adapter.name} market list`,
|
|
407
|
+
});
|
|
408
|
+
});
|
|
176
409
|
}
|
|
177
410
|
if (isJson())
|
|
178
|
-
return printJson(jsonOk(m));
|
|
179
|
-
|
|
180
|
-
console.log(` Mark Price: $${formatUsd(m.markPrice)}`);
|
|
181
|
-
console.log(` Index Price: ${usdOrDash(m.indexPrice)}`);
|
|
182
|
-
console.log(` Funding Rate: ${m.fundingRate != null ? formatPercent(m.fundingRate) : chalk.gray("N/A")}`);
|
|
183
|
-
console.log(` 24h Volume: ${usdOrDash(m.volume24h)}`);
|
|
184
|
-
console.log(` Open Interest: ${usdOrDash(m.openInterest)}`);
|
|
185
|
-
console.log(` Max Leverage: ${m.maxLeverage === null ? chalk.gray("-") : `${m.maxLeverage}x`}`);
|
|
186
|
-
console.log();
|
|
411
|
+
return printJson(jsonOk(withFundingVerified(adapter.name, [m])[0], fundingWarnings([adapter], [m])));
|
|
412
|
+
printMarketInfo(adapter.name, m);
|
|
187
413
|
});
|
|
188
414
|
market
|
|
189
415
|
.command("book <symbol>")
|
|
190
416
|
.description("Show orderbook for a symbol")
|
|
191
417
|
.option("-d, --depth <n>", "Number of levels", "10")
|
|
192
418
|
.action(async (symbol, opts) => {
|
|
419
|
+
const depth = parsePositiveIntArg(opts.depth, "--depth");
|
|
193
420
|
const adapter = await getAdapter();
|
|
194
|
-
const
|
|
421
|
+
const sym = normalizeSymbolArg(symbol);
|
|
422
|
+
// RFQ venue (Variational): no order book — the maker's indicative quotes, labelled as such.
|
|
423
|
+
if (adapter.getRfqQuotes !== undefined)
|
|
424
|
+
return printRfqQuotes(await adapter.getRfqQuotes(sym), isJson());
|
|
425
|
+
// `depth` reaches venues that size the request by it (GRVT, RISEx); every answer is cut to
|
|
426
|
+
// `depth` levels per side — JSON included (it printed the venue's full book).
|
|
427
|
+
assertBookDepth(adapter.name, depth);
|
|
428
|
+
const full = await adapter.getOrderbook(sym, depth);
|
|
429
|
+
const book = { ...full, bids: full.bids.slice(0, depth), asks: full.asks.slice(0, depth) };
|
|
195
430
|
if (isJson())
|
|
196
431
|
return printJson(jsonOk(book));
|
|
197
|
-
const depth = parseInt(opts.depth);
|
|
198
432
|
const asks = book.asks.slice(0, depth).reverse();
|
|
199
433
|
const bids = book.bids.slice(0, depth);
|
|
200
434
|
// Compute spread
|
|
@@ -214,22 +448,25 @@ getAccountAdapter) {
|
|
|
214
448
|
const len = maxCum > 0 ? Math.round((cum / maxCum) * BAR_WIDTH) : 0;
|
|
215
449
|
return "█".repeat(len);
|
|
216
450
|
};
|
|
217
|
-
console.log(chalk.cyan.bold(`\n Orderbook: ${
|
|
218
|
-
|
|
451
|
+
console.log(chalk.cyan.bold(`\n Orderbook: ${sym} (${adapter.name})\n`));
|
|
452
|
+
// A book with a non-USD quoteAsset (QFEX SAMSUNG-KRW) shows its currency, never a $.
|
|
453
|
+
const quote = nonUsdQuoteOf(book);
|
|
454
|
+
const money = (v) => priceOrDash(String(v), book);
|
|
455
|
+
console.log(chalk.gray(` Price Size ${quote === null ? "Cum$ " : `Cum (${quote})`.padEnd(14)} Depth`));
|
|
219
456
|
console.log(chalk.gray(" " + "─".repeat(60)));
|
|
220
457
|
asks.forEach(([p, a], i) => {
|
|
221
458
|
const cumUsd = askCums[i] * (Number(p));
|
|
222
|
-
console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)}
|
|
459
|
+
console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} ${money(cumUsd).padEnd(quote === null ? 15 : 18)}`) +
|
|
223
460
|
chalk.red.dim(bar(askCums[i])));
|
|
224
461
|
});
|
|
225
462
|
// Spread line
|
|
226
463
|
const spreadStr = spreadAbs > 0
|
|
227
|
-
?
|
|
464
|
+
? `${money(spreadAbs)} (${spreadPct.toFixed(3)}%)`
|
|
228
465
|
: "─";
|
|
229
466
|
console.log(chalk.gray(` ${"─".repeat(16)} spread: ${spreadStr} ${"─".repeat(10)}`));
|
|
230
467
|
bids.forEach(([p, a], i) => {
|
|
231
468
|
const cumUsd = bidCums[i] * (Number(p));
|
|
232
|
-
console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)}
|
|
469
|
+
console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} ${money(cumUsd).padEnd(quote === null ? 15 : 18)}`) +
|
|
233
470
|
chalk.green.dim(bar(bidCums[i])));
|
|
234
471
|
});
|
|
235
472
|
console.log();
|
|
@@ -237,12 +474,19 @@ getAccountAdapter) {
|
|
|
237
474
|
market
|
|
238
475
|
.command("trades <symbol>")
|
|
239
476
|
.description("Recent trades for a symbol")
|
|
240
|
-
.option("--limit <n>", "Number of recent trades to show
|
|
477
|
+
.option("--limit <n>", "Number of recent trades to show (default: the venue's own default, within its cap)")
|
|
241
478
|
.action(async (symbol, opts) => {
|
|
479
|
+
// No --limit → undefined: each adapter applies its own default within its venue cap, so a
|
|
480
|
+
// plain `market trades` never trips a cap (HL serves 10).
|
|
481
|
+
const limit = opts.limit === undefined ? undefined : parsePositiveIntArg(opts.limit, "--limit");
|
|
242
482
|
const adapter = await getAdapter();
|
|
243
|
-
const
|
|
483
|
+
const raw = await adapter.getRecentTrades(normalizeSymbolArg(symbol), limit);
|
|
484
|
+
const trades = newestFirst(raw);
|
|
485
|
+
const skipped = tapeSkippedOf(trades); // metadata survives the sort (sortByTime)
|
|
244
486
|
if (isJson())
|
|
245
|
-
return printJson(jsonOk(trades));
|
|
487
|
+
return printJson(jsonOk(trades, skipped !== null ? { skippedRows: skipped } : undefined));
|
|
488
|
+
if (skipped !== null)
|
|
489
|
+
console.log(chalk.yellow(` ${skipped.count} tape row(s) skipped: ${skipped.reason}`));
|
|
246
490
|
if (trades.length === 0) {
|
|
247
491
|
console.log(chalk.gray("\n No recent trades.\n"));
|
|
248
492
|
return;
|
|
@@ -260,34 +504,41 @@ getAccountAdapter) {
|
|
|
260
504
|
.description("Funding rate history")
|
|
261
505
|
.option("-l, --limit <n>", "Number of records", "10")
|
|
262
506
|
.action(async (symbol, opts) => {
|
|
507
|
+
const limit = parsePositiveIntArg(opts.limit, "--limit");
|
|
263
508
|
const adapter = await getAdapter();
|
|
264
|
-
|
|
509
|
+
// A venue without a funding-history API (Variational): its current funding terms, labelled as such.
|
|
510
|
+
if (adapter.getFundingTerms !== undefined)
|
|
511
|
+
return printFundingTerms(await adapter.getFundingTerms(normalizeSymbolArg(symbol)), isJson());
|
|
512
|
+
const history = newestFirst(await adapter.getFundingHistory(normalizeSymbolArg(symbol), limit));
|
|
265
513
|
if (isJson())
|
|
266
514
|
return printJson(jsonOk(history));
|
|
267
515
|
if (history.length === 0) {
|
|
268
516
|
console.log(chalk.gray("\n No funding history.\n"));
|
|
269
517
|
return;
|
|
270
518
|
}
|
|
271
|
-
|
|
519
|
+
// `price` is the venue's reference price of the print — oracle (Pacifica), mark (GRVT, Aster)
|
|
520
|
+
// or index (RISEx) — so the column is "Price", not "Oracle"; each rate shows its interval.
|
|
521
|
+
const hasPrice = history.some((h) => h.price != null && h.price !== "0" && h.price !== "");
|
|
272
522
|
const rows = history.map((h) => {
|
|
273
523
|
const base = [
|
|
274
524
|
new Date(h.time).toLocaleString(),
|
|
275
|
-
formatPercent(h.rate),
|
|
525
|
+
h.rate === null ? chalk.gray(h.marketClosed === true ? "market closed" : "N/A") : formatPercent(h.rate),
|
|
526
|
+
h.intervalHours === null ? chalk.gray("-") : `${h.intervalHours}h`,
|
|
276
527
|
];
|
|
277
|
-
if (
|
|
528
|
+
if (hasPrice)
|
|
278
529
|
base.push(h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"));
|
|
279
530
|
return base;
|
|
280
531
|
});
|
|
281
|
-
const headers =
|
|
532
|
+
const headers = hasPrice ? ["Time", "Funding Rate", "Interval", "Price"] : ["Time", "Funding Rate", "Interval"];
|
|
282
533
|
console.log(makeTable(headers, rows));
|
|
283
|
-
if (!
|
|
284
|
-
console.log(chalk.gray(" Note:
|
|
534
|
+
if (!hasPrice)
|
|
535
|
+
console.log(chalk.gray(" Note: no reference price published with this venue's funding prints.\n"));
|
|
285
536
|
});
|
|
286
537
|
market
|
|
287
538
|
.command("mid <symbol>")
|
|
288
539
|
.description("Get mid price for a single symbol (fast)")
|
|
289
540
|
.action(async (symbol) => {
|
|
290
|
-
const sym = symbol
|
|
541
|
+
const sym = normalizeSymbolArg(symbol);
|
|
291
542
|
// withJsonErrors: every failure (incl. an empty book) is an error envelope AND exit code 1.
|
|
292
543
|
await withJsonErrors(isJson(), async () => {
|
|
293
544
|
const adapter = await getAdapter();
|
|
@@ -328,30 +579,38 @@ getAccountAdapter) {
|
|
|
328
579
|
market
|
|
329
580
|
.command("kline <symbol> <interval>")
|
|
330
581
|
.description("Kline/candlestick data (intervals: 1m,5m,15m,1h,4h,1d,...)")
|
|
331
|
-
.option("--start <ts>", "Start time (unix ms)")
|
|
332
|
-
.option("--end <ts>", "End time (unix ms)")
|
|
582
|
+
.option("--start <ts>", "Start time (unix ms or ISO date, e.g. 2026-10-18T00:00:00Z)")
|
|
583
|
+
.option("--end <ts>", "End time (unix ms or ISO date)")
|
|
333
584
|
.action(async (symbol, interval, opts) => {
|
|
334
585
|
const publicAdapter = await getAdapter();
|
|
335
586
|
// A venue whose candles endpoint is authenticated (Ondo) is read with the registered key.
|
|
336
587
|
const adapter = getAccountAdapter !== undefined && klinesNeedAuth(publicAdapter) ? await getAccountAdapter() : publicAdapter;
|
|
337
588
|
const now = Date.now();
|
|
338
|
-
const startTime = opts.start ?
|
|
339
|
-
const endTime = opts.end ?
|
|
340
|
-
|
|
589
|
+
const startTime = opts.start !== undefined ? parseTimeArg(opts.start, "--start") : now - 24 * 60 * 60 * 1000;
|
|
590
|
+
const endTime = opts.end !== undefined ? parseTimeArg(opts.end, "--end") : now;
|
|
591
|
+
if (startTime > endTime) {
|
|
592
|
+
throw new PerpError("INVALID_PARAMS", `--start (${new Date(startTime).toISOString()}) is after --end (${new Date(endTime).toISOString()})`, {
|
|
593
|
+
remediation: "Pass --start before --end",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// Candles oldest first (ascending open time) on every venue.
|
|
597
|
+
const klines = sortByTime(await adapter.getKlines(normalizeSymbolArg(symbol), interval, startTime, endTime), "asc");
|
|
341
598
|
if (isJson())
|
|
342
599
|
return printJson(jsonOk(klines));
|
|
343
600
|
if (klines.length === 0) {
|
|
344
601
|
console.log(chalk.gray("\n No kline data.\n"));
|
|
345
602
|
return;
|
|
346
603
|
}
|
|
604
|
+
// ISO (UTC) candle open times — one format for every venue, independent of the host locale.
|
|
347
605
|
const rows = klines.slice(-20).map((k) => [
|
|
348
|
-
new Date(k.time).
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
k.
|
|
354
|
-
|
|
606
|
+
new Date(k.time).toISOString(),
|
|
607
|
+
// A row with a non-USD quoteAsset (QFEX SAMSUNG-KRW) prints its currency, never a $.
|
|
608
|
+
priceOrDash(k.open, k),
|
|
609
|
+
priceOrDash(k.high, k),
|
|
610
|
+
priceOrDash(k.low, k),
|
|
611
|
+
priceOrDash(k.close, k),
|
|
612
|
+
k.volume === null ? chalk.gray("-") : k.volume,
|
|
613
|
+
k.trades === null ? chalk.gray("-") : String(k.trades),
|
|
355
614
|
]);
|
|
356
615
|
console.log(makeTable(["Time", "Open", "High", "Low", "Close", "Volume", "Trades"], rows));
|
|
357
616
|
});
|
|
@@ -380,7 +639,8 @@ getAccountAdapter) {
|
|
|
380
639
|
const rows = dexes.map(d => [
|
|
381
640
|
chalk.white.bold(d.name),
|
|
382
641
|
chalk.gray(d.deployer ? d.deployer.slice(0, 6) + "..." + d.deployer.slice(-4) : "-"),
|
|
383
|
-
|
|
642
|
+
// Live (non-delisted) assets; a dex whose every asset is delisted is marked as such.
|
|
643
|
+
d.delisted === true ? chalk.gray("delisted") : String(d.assets.length),
|
|
384
644
|
d.assets.slice(0, 5).join(", ") + (d.assets.length > 5 ? ` +${d.assets.length - 5}` : ""),
|
|
385
645
|
]);
|
|
386
646
|
console.log(makeTable(["DEX", "Deployer", "Assets", "Markets"], rows));
|
|
@@ -389,3 +649,38 @@ getAccountAdapter) {
|
|
|
389
649
|
});
|
|
390
650
|
});
|
|
391
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Why a listed market is not tradable, from the row's `status` when the venue states it (QFEX
|
|
654
|
+
* "nonUsdQuote", Ondo "closed" / "reduceOnly"); the generic off-hours text only when it does not.
|
|
655
|
+
* null = tradable or no tradability flag.
|
|
656
|
+
*/
|
|
657
|
+
export function notTradableNote(m) {
|
|
658
|
+
const r = m;
|
|
659
|
+
if (r.tradable !== false)
|
|
660
|
+
return null;
|
|
661
|
+
if (r.status === "nonUsdQuote")
|
|
662
|
+
return `Read-only in perp-cli: quoted in ${typeof r.quoteAsset === "string" ? r.quoteAsset : "a non-USD currency"} (orders, sizing and risk limits are USD-only).`;
|
|
663
|
+
if (r.status === "closed")
|
|
664
|
+
return "Not tradable: the venue has closed this market to new trading.";
|
|
665
|
+
if (r.status === "reduceOnly")
|
|
666
|
+
return "Reduce-only on the venue: positions can be closed, not opened.";
|
|
667
|
+
return "Not tradable right now (off-hours, reduce-only or halted on the venue).";
|
|
668
|
+
}
|
|
669
|
+
function printMarketInfo(venue, m) {
|
|
670
|
+
console.log(chalk.cyan.bold(`\n ${m.symbol} on ${venue}\n`));
|
|
671
|
+
const note = notTradableNote(m);
|
|
672
|
+
if (note !== null)
|
|
673
|
+
console.log(chalk.yellow(` ${note}`));
|
|
674
|
+
console.log(` Mark Price: ${priceOrDash(m.markPrice, m)}`);
|
|
675
|
+
console.log(` Index Price: ${priceOrDash(m.indexPrice, m)}`);
|
|
676
|
+
// Per-market funding interval (GRVT 4h / 8h, Variational, Aster): the rate carries its interval.
|
|
677
|
+
console.log(` Funding Rate: ${fundingCell(m, perMarketFunding(venue))}`);
|
|
678
|
+
// A venue whose rate is unverified (QFEX) labels it on the row.
|
|
679
|
+
const fundingNote = m.fundingNote;
|
|
680
|
+
if (typeof fundingNote === "string")
|
|
681
|
+
console.log(chalk.yellow(` ${fundingNote}`));
|
|
682
|
+
console.log(` 24h Volume: ${volumeCell(m)}`);
|
|
683
|
+
console.log(` Open Interest: ${formatOpenInterest(m, true)}`);
|
|
684
|
+
console.log(` Max Leverage: ${m.maxLeverage === null ? chalk.gray("-") : `${m.maxLeverage}x`}`);
|
|
685
|
+
console.log();
|
|
686
|
+
}
|