perp-cli 0.15.0 → 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.
Files changed (154) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +129 -535
  4. package/README.zh.md +243 -0
  5. package/dist/api/public/lighter.d.ts +5 -0
  6. package/dist/api/public/lighter.js +5 -0
  7. package/dist/api/public/pacifica.js +2 -2
  8. package/dist/arb/sizing.js +4 -2
  9. package/dist/arb/state.d.ts +6 -0
  10. package/dist/balance-by-address.d.ts +39 -0
  11. package/dist/balance-by-address.js +44 -0
  12. package/dist/bot/quote-rules.js +1 -1
  13. package/dist/bot/strategies/funding-arb-strategy.js +10 -3
  14. package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
  15. package/dist/bot/strategies/funding-arb-utils.js +48 -15
  16. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  17. package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
  18. package/dist/bot/strategies/spot-perp-executor.js +2 -2
  19. package/dist/cli-program.js +1 -1
  20. package/dist/commands/account.js +168 -24
  21. package/dist/commands/arb/index.js +19 -77
  22. package/dist/commands/arb-auto.d.ts +24 -0
  23. package/dist/commands/arb-auto.js +155 -81
  24. package/dist/commands/arb.d.ts +8 -0
  25. package/dist/commands/arb.js +49 -23
  26. package/dist/commands/backtest.js +23 -21
  27. package/dist/commands/market.d.ts +48 -0
  28. package/dist/commands/market.js +241 -78
  29. package/dist/commands/options.d.ts +5 -0
  30. package/dist/commands/options.js +20 -3
  31. package/dist/commands/pacifica-earn.js +8 -21
  32. package/dist/commands/portfolio.d.ts +16 -0
  33. package/dist/commands/portfolio.js +51 -8
  34. package/dist/commands/trade.js +4 -4
  35. package/dist/cross-chain-margin.d.ts +3 -1
  36. package/dist/cross-chain-margin.js +4 -2
  37. package/dist/dashboard/server.js +2 -2
  38. package/dist/dashboard/ui.js +9 -6
  39. package/dist/errors.d.ts +5 -0
  40. package/dist/errors.js +10 -0
  41. package/dist/exchanges/address.d.ts +8 -0
  42. package/dist/exchanges/address.js +47 -0
  43. package/dist/exchanges/arcus/adapter.d.ts +10 -6
  44. package/dist/exchanges/arcus/adapter.js +33 -6
  45. package/dist/exchanges/arcus/public.js +3 -2
  46. package/dist/exchanges/aster.d.ts +27 -7
  47. package/dist/exchanges/aster.js +124 -42
  48. package/dist/exchanges/canonical-symbols.d.ts +7 -0
  49. package/dist/exchanges/canonical-symbols.js +20 -0
  50. package/dist/exchanges/capabilities.d.ts +2 -0
  51. package/dist/exchanges/capabilities.js +2 -1
  52. package/dist/exchanges/derive/options.d.ts +5 -0
  53. package/dist/exchanges/derive/options.js +20 -1
  54. package/dist/exchanges/extended/adapter.d.ts +16 -2
  55. package/dist/exchanges/extended/adapter.js +96 -34
  56. package/dist/exchanges/extended/public.d.ts +3 -0
  57. package/dist/exchanges/extended/public.js +9 -2
  58. package/dist/exchanges/grvt/adapter.d.ts +16 -2
  59. package/dist/exchanges/grvt/adapter.js +98 -21
  60. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  61. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  62. package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
  63. package/dist/exchanges/hyperliquid-spot.js +20 -12
  64. package/dist/exchanges/hyperliquid.d.ts +180 -13
  65. package/dist/exchanges/hyperliquid.js +487 -185
  66. package/dist/exchanges/interface.d.ts +64 -8
  67. package/dist/exchanges/interface.js +31 -0
  68. package/dist/exchanges/lighter-public.d.ts +66 -11
  69. package/dist/exchanges/lighter-public.js +249 -81
  70. package/dist/exchanges/lighter-spot.d.ts +7 -1
  71. package/dist/exchanges/lighter-spot.js +99 -90
  72. package/dist/exchanges/lighter.d.ts +70 -11
  73. package/dist/exchanges/lighter.js +285 -92
  74. package/dist/exchanges/ondo/adapter.d.ts +34 -8
  75. package/dist/exchanges/ondo/adapter.js +73 -16
  76. package/dist/exchanges/ondo/public.d.ts +27 -2
  77. package/dist/exchanges/ondo/public.js +63 -31
  78. package/dist/exchanges/outcome-interface.d.ts +4 -0
  79. package/dist/exchanges/pacifica-spot.d.ts +6 -0
  80. package/dist/exchanges/pacifica-spot.js +23 -15
  81. package/dist/exchanges/pacifica-vaults.d.ts +9 -0
  82. package/dist/exchanges/pacifica-vaults.js +14 -1
  83. package/dist/exchanges/pacifica.d.ts +77 -6
  84. package/dist/exchanges/pacifica.js +321 -130
  85. package/dist/exchanges/public-balances.d.ts +25 -0
  86. package/dist/exchanges/public-balances.js +158 -0
  87. package/dist/exchanges/public-positions.d.ts +43 -0
  88. package/dist/exchanges/public-positions.js +154 -0
  89. package/dist/exchanges/qfex/adapter.d.ts +52 -9
  90. package/dist/exchanges/qfex/adapter.js +74 -9
  91. package/dist/exchanges/qfex/public.d.ts +62 -9
  92. package/dist/exchanges/qfex/public.js +128 -22
  93. package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
  94. package/dist/exchanges/risex/adapter.d.ts +19 -7
  95. package/dist/exchanges/risex/adapter.js +58 -12
  96. package/dist/exchanges/spot-factory.js +1 -1
  97. package/dist/exchanges/txflow/adapter.d.ts +14 -7
  98. package/dist/exchanges/txflow/adapter.js +51 -19
  99. package/dist/exchanges/txflow/client.js +3 -1
  100. package/dist/exchanges/txflow/public.d.ts +12 -1
  101. package/dist/exchanges/txflow/public.js +82 -11
  102. package/dist/exchanges/unknown-symbol.d.ts +6 -1
  103. package/dist/exchanges/unknown-symbol.js +7 -2
  104. package/dist/exchanges/variational/adapter.d.ts +2 -6
  105. package/dist/exchanges/venue-types.d.ts +46 -3
  106. package/dist/exchanges/venue-types.js +80 -9
  107. package/dist/exchanges/venue.d.ts +99 -3
  108. package/dist/exchanges/venue.js +72 -0
  109. package/dist/exchanges/venues/hyperliquid.js +9 -0
  110. package/dist/exchanges/venues/lighter-rh.js +13 -3
  111. package/dist/exchanges/venues/lighter.js +13 -3
  112. package/dist/exchanges/venues/pacifica.js +9 -0
  113. package/dist/exchanges/venues/risex.js +15 -0
  114. package/dist/funding/legacy-public.d.ts +5 -3
  115. package/dist/funding/legacy-public.js +33 -37
  116. package/dist/funding/rates.d.ts +54 -5
  117. package/dist/funding/rates.js +49 -13
  118. package/dist/liquidity.d.ts +3 -1
  119. package/dist/liquidity.js +8 -4
  120. package/dist/mcp-pacifica-earn.js +11 -5
  121. package/dist/mcp-server.js +104 -15
  122. package/dist/pacifica/client.d.ts +2 -1
  123. package/dist/pacifica/client.js +11 -0
  124. package/dist/positions-by-address.d.ts +34 -0
  125. package/dist/positions-by-address.js +46 -0
  126. package/dist/strategies/funding-arb.js +35 -62
  127. package/dist/utils.d.ts +11 -1
  128. package/dist/venue-health.js +1 -1
  129. package/dist/ws/arcus-market.d.ts +9 -0
  130. package/dist/ws/arcus-market.js +9 -0
  131. package/dist/ws/aster-market.d.ts +33 -0
  132. package/dist/ws/aster-market.js +30 -0
  133. package/dist/ws/index.d.ts +30 -9
  134. package/dist/ws/index.js +180 -15
  135. package/dist/ws/lighter-market.d.ts +14 -0
  136. package/dist/ws/lighter-market.js +13 -0
  137. package/dist/ws/node.d.ts +30 -9
  138. package/dist/ws/node.js +204 -16
  139. package/dist/ws/unclassified.d.ts +16 -0
  140. package/dist/ws/unclassified.js +43 -0
  141. package/package.json +15 -2
  142. package/skills/perp-cli/SKILL.md +97 -33
  143. package/skills/perp-cli/package.json +15 -2
  144. package/skills/perp-cli/references/agent-operations.md +15 -12
  145. package/skills/perp-cli/references/commands.md +67 -12
  146. package/skills/perp-cli/references/strategies.md +38 -3
  147. package/skills/perp-cli/references/ws-library.md +55 -24
  148. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  149. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  150. package/skills/perp-cli/scripts/preflight.sh +27 -19
  151. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  152. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  153. package/dist/cli-spec.d.ts +0 -46
  154. package/dist/cli-spec.js +0 -81
@@ -1,15 +1,139 @@
1
- import { tapeSkippedOf } from "../exchanges/interface.js";
1
+ import { sortByTime, tapeSkippedOf } from "../exchanges/interface.js";
2
2
  import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors, normalizeSymbolArg } from "../utils.js";
3
3
  import chalk from "chalk";
4
4
  import { PerpError } from "../errors.js";
5
5
  import { midDecimal } from "../exchanges/precision.js";
6
- import { hasPacificaSdk, isDexCapable, klinesNeedAuth } from "../exchanges/capabilities.js";
7
- import { getVenue, listVenuesWithRole, tryResolveVenueId, unofficialApiNote } from "../exchanges/venue.js";
6
+ import { isDexCapable, klinesNeedAuth } from "../exchanges/capabilities.js";
7
+ import { getVenue, listVenuesWithRole, tryResolveVenueId, unofficialApiNote, withFundingVerified } from "../exchanges/venue.js";
8
8
  import "../exchanges/venues/index.js";
9
- /** Venues with an adapter (market-data-only venues have none — see `perp arb scan`). */
10
- const EXCHANGES = listVenuesWithRole("account").map(v => v.id);
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
+ }
11
120
  /** null = venue does not publish the value → "-" (never rendered as $0). */
12
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
+ };
13
137
  /**
14
138
  * Funding cell. On a venue whose funding interval is per market (VenueDefinition.fundingRateBasis
15
139
  * "perMarket": Variational, Aster, GRVT) the raw rate is labelled with its interval ("+0.0079%/8h"), since
@@ -41,7 +165,7 @@ function formatOpenInterest(m, withUsd) {
41
165
  const mark = Number(m.markPrice);
42
166
  if (!withUsd || !Number.isFinite(oi) || !(mark > 0))
43
167
  return base;
44
- return `${base} ${chalk.gray(`(≈ $${formatUsd(oi * mark)} at mark)`)}`;
168
+ return `${base} ${chalk.gray(`(≈ ${priceOrDash(String(oi * mark), m)} at mark)`)}`;
45
169
  }
46
170
  function printRfqQuotes(q, json) {
47
171
  if (json)
@@ -110,11 +234,11 @@ getAccountAdapter) {
110
234
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
111
235
  const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI (base)", "Max Lev"];
112
236
  const marketRow = (m, perMarket = false) => [
113
- chalk.white.bold(m.symbol),
114
- `$${formatUsd(m.markPrice)}`,
115
- usdOrDash(m.indexPrice),
237
+ symbolCell(m),
238
+ priceOrDash(m.markPrice, m),
239
+ priceOrDash(m.indexPrice, m),
116
240
  fundingCell(m, perMarket),
117
- usdOrDash(m.volume24h),
241
+ volumeCell(m),
118
242
  formatOpenInterest(m, false),
119
243
  m.maxLeverage === null ? chalk.gray("-") : String(m.maxLeverage) + "x",
120
244
  ];
@@ -122,17 +246,17 @@ getAccountAdapter) {
122
246
  if (!explicitExchange && getAdapterForExchange) {
123
247
  const grouped = {};
124
248
  const errors = {};
125
- await Promise.all(EXCHANGES.map(async (ex) => {
249
+ const listed = [];
250
+ await Promise.all(MARKET_DATA_VENUES.map(async (ex) => {
126
251
  try {
127
252
  const adapter = await getAdapterForExchange(ex);
128
253
  grouped[ex] = await adapter.getMarkets();
254
+ listed.push(adapter);
129
255
  // --hip3: fetch HIP-3 dex markets
130
256
  if (opts.hip3 && ex === "hyperliquid" && isDexCapable(adapter)) {
131
257
  const dexes = await adapter.listDeployedDexes();
132
258
  await Promise.allSettled(dexes.map(async (d) => {
133
- const dexAdapter = Object.create(adapter);
134
- dexAdapter.setDex(d.name);
135
- const markets = await dexAdapter.getMarkets();
259
+ const markets = await adapter.forDex(d.name).getMarkets();
136
260
  if (markets.length > 0)
137
261
  grouped[`hip3:${d.name}`] = markets;
138
262
  }));
@@ -143,7 +267,7 @@ getAccountAdapter) {
143
267
  }
144
268
  }));
145
269
  if (isJson()) {
146
- 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())));
147
271
  }
148
272
  // Per-exchange tables
149
273
  for (const [ex, markets] of Object.entries(grouped)) {
@@ -157,6 +281,7 @@ getAccountAdapter) {
157
281
  for (const [ex, msg] of Object.entries(errors)) {
158
282
  console.log(chalk.gray(` ${ex}: ${msg}`));
159
283
  }
284
+ printUnofficialNotes(Object.keys(grouped));
160
285
  return;
161
286
  }
162
287
  // Single exchange mode
@@ -167,18 +292,16 @@ getAccountAdapter) {
167
292
  if (opts.hip3 && isDexCapable(adapter)) {
168
293
  const dexes = await adapter.listDeployedDexes();
169
294
  await Promise.allSettled(dexes.map(async (d) => {
170
- const dexAdapter = Object.create(adapter);
171
- dexAdapter.setDex(d.name);
172
- const m = await dexAdapter.getMarkets();
295
+ const m = await adapter.forDex(d.name).getMarkets();
173
296
  if (m.length > 0)
174
297
  hip3[d.name] = m;
175
298
  }));
176
299
  }
177
300
  if (isJson()) {
178
301
  if (Object.keys(hip3).length > 0) {
179
- 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)])) }));
180
303
  }
181
- return printJson(jsonOk(markets));
304
+ return printJson(jsonOk(withFundingVerified(adapter.name, markets), fundingWarnings([adapter], markets)));
182
305
  }
183
306
  const perMarket = perMarketFunding(adapter.name);
184
307
  // Rows that carry an asset class (Extended: crypto / equity / etf / index / commodity / fx / preIpo) show it.
@@ -204,25 +327,27 @@ getAccountAdapter) {
204
327
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
205
328
  const PRICE_HEADERS = ["Symbol", "Mark", "Index", "Funding"];
206
329
  const priceRow = (m, perMarket = false) => [
207
- chalk.white.bold(m.symbol),
208
- `$${formatUsd(m.markPrice)}`,
209
- usdOrDash(m.indexPrice),
330
+ symbolCell(m),
331
+ priceOrDash(m.markPrice, m),
332
+ priceOrDash(m.indexPrice, m),
210
333
  fundingCell(m, perMarket),
211
334
  ];
212
335
  if (!explicitExchange && getAdapterForExchange) {
213
336
  const grouped = {};
214
337
  const errors = {};
215
- await Promise.all(EXCHANGES.map(async (ex) => {
338
+ const listed = [];
339
+ await Promise.all(MARKET_DATA_VENUES.map(async (ex) => {
216
340
  try {
217
341
  const adapter = await getAdapterForExchange(ex);
218
342
  grouped[ex] = await adapter.getMarkets();
343
+ listed.push(adapter);
219
344
  }
220
345
  catch (err) {
221
346
  errors[ex] = err instanceof Error ? err.message : String(err);
222
347
  }
223
348
  }));
224
349
  if (isJson()) {
225
- 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())));
226
351
  }
227
352
  for (const [ex, markets] of Object.entries(grouped)) {
228
353
  if (markets.length === 0)
@@ -232,29 +357,15 @@ getAccountAdapter) {
232
357
  }
233
358
  for (const [ex, msg] of Object.entries(errors))
234
359
  console.log(chalk.gray(` ${ex}: ${msg}`));
360
+ printUnofficialNotes(Object.keys(grouped));
235
361
  return;
236
362
  }
237
363
  const adapter = await getAdapter();
238
- if (hasPacificaSdk(adapter)) {
239
- const sdk = adapter.sdk;
240
- const prices = await sdk.getPrices();
241
- if (isJson())
242
- return printJson(jsonOk(prices));
243
- const rows = prices.map((p) => [
244
- chalk.white.bold(p.symbol),
245
- `$${formatUsd(p.mark)}`,
246
- `$${formatUsd(p.mid)}`,
247
- `$${formatUsd(p.oracle)}`,
248
- formatPercent(p.funding),
249
- ]);
250
- console.log(makeTable(["Symbol", "Mark", "Mid", "Oracle", "Funding"], rows));
251
- }
252
- else {
253
- const markets = await adapter.getMarkets();
254
- if (isJson())
255
- return printJson(jsonOk(markets));
256
- console.log(makeTable(PRICE_HEADERS, markets.map(m => priceRow(m, perMarketFunding(adapter.name)))));
257
- }
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)))));
258
369
  });
259
370
  market
260
371
  .command("info <symbol>")
@@ -268,20 +379,25 @@ getAccountAdapter) {
268
379
  return void await withJsonErrors(isJson(), async () => {
269
380
  const one = await single.call(adapter, symbol);
270
381
  if (isJson())
271
- return printJson(jsonOk(one));
382
+ return printJson(jsonOk(withFundingVerified(adapter.name, [one])[0]));
272
383
  printMarketInfo(adapter.name, one);
273
384
  });
274
385
  }
275
386
  const markets = await adapter.getMarkets();
276
387
  const sym = symbol.toUpperCase();
388
+ const bare = bareSymbolOf(symbol).toUpperCase();
277
389
  const m = markets.find((mk) => {
278
390
  const s = mk.symbol.toUpperCase();
279
- return s === sym || s === `${sym}-PERP` || s.replace(/-PERP$/, "") === sym
280
- || s.split(":").pop() === sym; // HIP-3: "km:US500" matches "US500"
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"
281
394
  });
282
395
  if (!m) {
283
396
  // Printed (envelope / Error + Remediation) and exit 1 via withJsonErrors.
284
397
  return void await withJsonErrors(isJson(), async () => {
398
+ const delistedErr = adapter.delistedError?.(symbol) ?? null;
399
+ if (delistedErr !== null)
400
+ throw delistedErr;
285
401
  const delisted = typeof adapter.delistedMessage === "function"
286
402
  ? adapter.delistedMessage(symbol)
287
403
  : null;
@@ -292,7 +408,7 @@ getAccountAdapter) {
292
408
  });
293
409
  }
294
410
  if (isJson())
295
- return printJson(jsonOk(m));
411
+ return printJson(jsonOk(withFundingVerified(adapter.name, [m])[0], fundingWarnings([adapter], [m])));
296
412
  printMarketInfo(adapter.name, m);
297
413
  });
298
414
  market
@@ -300,15 +416,19 @@ getAccountAdapter) {
300
416
  .description("Show orderbook for a symbol")
301
417
  .option("-d, --depth <n>", "Number of levels", "10")
302
418
  .action(async (symbol, opts) => {
419
+ const depth = parsePositiveIntArg(opts.depth, "--depth");
303
420
  const adapter = await getAdapter();
304
421
  const sym = normalizeSymbolArg(symbol);
305
422
  // RFQ venue (Variational): no order book — the maker's indicative quotes, labelled as such.
306
423
  if (adapter.getRfqQuotes !== undefined)
307
424
  return printRfqQuotes(await adapter.getRfqQuotes(sym), isJson());
308
- const book = await adapter.getOrderbook(sym);
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) };
309
430
  if (isJson())
310
431
  return printJson(jsonOk(book));
311
- const depth = parseInt(opts.depth);
312
432
  const asks = book.asks.slice(0, depth).reverse();
313
433
  const bids = book.bids.slice(0, depth);
314
434
  // Compute spread
@@ -329,21 +449,24 @@ getAccountAdapter) {
329
449
  return "█".repeat(len);
330
450
  };
331
451
  console.log(chalk.cyan.bold(`\n Orderbook: ${sym} (${adapter.name})\n`));
332
- console.log(chalk.gray(" Price Size Cum$ Depth"));
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`));
333
456
  console.log(chalk.gray(" " + "─".repeat(60)));
334
457
  asks.forEach(([p, a], i) => {
335
458
  const cumUsd = askCums[i] * (Number(p));
336
- console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
459
+ console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} ${money(cumUsd).padEnd(quote === null ? 15 : 18)}`) +
337
460
  chalk.red.dim(bar(askCums[i])));
338
461
  });
339
462
  // Spread line
340
463
  const spreadStr = spreadAbs > 0
341
- ? `$${formatUsd(spreadAbs)} (${spreadPct.toFixed(3)}%)`
464
+ ? `${money(spreadAbs)} (${spreadPct.toFixed(3)}%)`
342
465
  : "─";
343
466
  console.log(chalk.gray(` ${"─".repeat(16)} spread: ${spreadStr} ${"─".repeat(10)}`));
344
467
  bids.forEach(([p, a], i) => {
345
468
  const cumUsd = bidCums[i] * (Number(p));
346
- console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
469
+ console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} ${money(cumUsd).padEnd(quote === null ? 15 : 18)}`) +
347
470
  chalk.green.dim(bar(bidCums[i])));
348
471
  });
349
472
  console.log();
@@ -351,11 +474,15 @@ getAccountAdapter) {
351
474
  market
352
475
  .command("trades <symbol>")
353
476
  .description("Recent trades for a symbol")
354
- .option("--limit <n>", "Number of recent trades to show", "20")
477
+ .option("--limit <n>", "Number of recent trades to show (default: the venue's own default, within its cap)")
355
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");
356
482
  const adapter = await getAdapter();
357
- const trades = await adapter.getRecentTrades(normalizeSymbolArg(symbol), parseInt(opts.limit));
358
- const skipped = tapeSkippedOf(trades);
483
+ const raw = await adapter.getRecentTrades(normalizeSymbolArg(symbol), limit);
484
+ const trades = newestFirst(raw);
485
+ const skipped = tapeSkippedOf(trades); // metadata survives the sort (sortByTime)
359
486
  if (isJson())
360
487
  return printJson(jsonOk(trades, skipped !== null ? { skippedRows: skipped } : undefined));
361
488
  if (skipped !== null)
@@ -377,31 +504,35 @@ getAccountAdapter) {
377
504
  .description("Funding rate history")
378
505
  .option("-l, --limit <n>", "Number of records", "10")
379
506
  .action(async (symbol, opts) => {
507
+ const limit = parsePositiveIntArg(opts.limit, "--limit");
380
508
  const adapter = await getAdapter();
381
509
  // A venue without a funding-history API (Variational): its current funding terms, labelled as such.
382
510
  if (adapter.getFundingTerms !== undefined)
383
511
  return printFundingTerms(await adapter.getFundingTerms(normalizeSymbolArg(symbol)), isJson());
384
- const history = await adapter.getFundingHistory(normalizeSymbolArg(symbol), parseInt(opts.limit));
512
+ const history = newestFirst(await adapter.getFundingHistory(normalizeSymbolArg(symbol), limit));
385
513
  if (isJson())
386
514
  return printJson(jsonOk(history));
387
515
  if (history.length === 0) {
388
516
  console.log(chalk.gray("\n No funding history.\n"));
389
517
  return;
390
518
  }
391
- const hasOracle = history.some((h) => h.price != null && h.price !== "0" && h.price !== "");
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 !== "");
392
522
  const rows = history.map((h) => {
393
523
  const base = [
394
524
  new Date(h.time).toLocaleString(),
395
- 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`,
396
527
  ];
397
- if (hasOracle)
528
+ if (hasPrice)
398
529
  base.push(h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"));
399
530
  return base;
400
531
  });
401
- const headers = hasOracle ? ["Time", "Funding Rate", "Oracle"] : ["Time", "Funding Rate"];
532
+ const headers = hasPrice ? ["Time", "Funding Rate", "Interval", "Price"] : ["Time", "Funding Rate", "Interval"];
402
533
  console.log(makeTable(headers, rows));
403
- if (!hasOracle)
404
- console.log(chalk.gray(" Note: Oracle price not available for this exchange.\n"));
534
+ if (!hasPrice)
535
+ console.log(chalk.gray(" Note: no reference price published with this venue's funding prints.\n"));
405
536
  });
406
537
  market
407
538
  .command("mid <symbol>")
@@ -448,16 +579,22 @@ getAccountAdapter) {
448
579
  market
449
580
  .command("kline <symbol> <interval>")
450
581
  .description("Kline/candlestick data (intervals: 1m,5m,15m,1h,4h,1d,...)")
451
- .option("--start <ts>", "Start time (unix ms)")
452
- .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)")
453
584
  .action(async (symbol, interval, opts) => {
454
585
  const publicAdapter = await getAdapter();
455
586
  // A venue whose candles endpoint is authenticated (Ondo) is read with the registered key.
456
587
  const adapter = getAccountAdapter !== undefined && klinesNeedAuth(publicAdapter) ? await getAccountAdapter() : publicAdapter;
457
588
  const now = Date.now();
458
- const startTime = opts.start ? parseInt(opts.start) : now - 24 * 60 * 60 * 1000;
459
- const endTime = opts.end ? parseInt(opts.end) : now;
460
- const klines = await adapter.getKlines(normalizeSymbolArg(symbol), interval, startTime, endTime);
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");
461
598
  if (isJson())
462
599
  return printJson(jsonOk(klines));
463
600
  if (klines.length === 0) {
@@ -467,10 +604,11 @@ getAccountAdapter) {
467
604
  // ISO (UTC) candle open times — one format for every venue, independent of the host locale.
468
605
  const rows = klines.slice(-20).map((k) => [
469
606
  new Date(k.time).toISOString(),
470
- `$${formatUsd(k.open)}`,
471
- `$${formatUsd(k.high)}`,
472
- `$${formatUsd(k.low)}`,
473
- `$${formatUsd(k.close)}`,
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),
474
612
  k.volume === null ? chalk.gray("-") : k.volume,
475
613
  k.trades === null ? chalk.gray("-") : String(k.trades),
476
614
  ]);
@@ -501,7 +639,8 @@ getAccountAdapter) {
501
639
  const rows = dexes.map(d => [
502
640
  chalk.white.bold(d.name),
503
641
  chalk.gray(d.deployer ? d.deployer.slice(0, 6) + "..." + d.deployer.slice(-4) : "-"),
504
- String(d.assets.length),
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),
505
644
  d.assets.slice(0, 5).join(", ") + (d.assets.length > 5 ? ` +${d.assets.length - 5}` : ""),
506
645
  ]);
507
646
  console.log(makeTable(["DEX", "Deployer", "Assets", "Markets"], rows));
@@ -510,13 +649,37 @@ getAccountAdapter) {
510
649
  });
511
650
  });
512
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
+ }
513
669
  function printMarketInfo(venue, m) {
514
670
  console.log(chalk.cyan.bold(`\n ${m.symbol} on ${venue}\n`));
515
- console.log(` Mark Price: $${formatUsd(m.markPrice)}`);
516
- console.log(` Index Price: ${usdOrDash(m.indexPrice)}`);
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)}`);
517
676
  // Per-market funding interval (GRVT 4h / 8h, Variational, Aster): the rate carries its interval.
518
677
  console.log(` Funding Rate: ${fundingCell(m, perMarketFunding(venue))}`);
519
- console.log(` 24h Volume: ${usdOrDash(m.volume24h)}`);
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)}`);
520
683
  console.log(` Open Interest: ${formatOpenInterest(m, true)}`);
521
684
  console.log(` Max Leverage: ${m.maxLeverage === null ? chalk.gray("-") : `${m.maxLeverage}x`}`);
522
685
  console.log();
@@ -7,3 +7,8 @@
7
7
  */
8
8
  import { Command } from "commander";
9
9
  export declare function registerOptionsCommands(program: Command, isJson: () => boolean): void;
10
+ /**
11
+ * Greeks by significant figures (3), not fixed decimals: toFixed(3) printed theta / vega of cheap
12
+ * options as -0.000. Derive publishes greeks at a 1e-5 resolution, so "0" can mean |x| < 0.00001.
13
+ */
14
+ export declare function fmtGreek(v: string | null | undefined): string;
@@ -90,8 +90,21 @@ function fmtPx(v) {
90
90
  function fmtPct(v) {
91
91
  return v === null ? dash : `${(Number(v) * 100).toFixed(1)}%`;
92
92
  }
93
- function fmtGreek(v) {
94
- return v === null || v === undefined ? dash : Number(v).toFixed(3);
93
+ /**
94
+ * Greeks by significant figures (3), not fixed decimals: toFixed(3) printed theta / vega of cheap
95
+ * options as -0.000. Derive publishes greeks at a 1e-5 resolution, so "0" can mean |x| < 0.00001.
96
+ */
97
+ export function fmtGreek(v) {
98
+ if (v === null || v === undefined)
99
+ return dash;
100
+ const n = Number(v);
101
+ if (!Number.isFinite(n))
102
+ return dash;
103
+ if (n === 0)
104
+ return "0";
105
+ // Fixed notation, 3 significant figures (String() switches to 1.23e-7 below 1e-6).
106
+ const decimals = Math.max(0, 2 - Math.floor(Math.log10(Math.abs(n))));
107
+ return n.toFixed(Math.min(decimals, 20)).replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
95
108
  }
96
109
  function sideCells(q) {
97
110
  if (q === null)
@@ -130,5 +143,9 @@ function printQuote(q) {
130
143
  ["rho", fmtGreek(q.greeks?.rho), "OI", q.openInterest],
131
144
  ["24h contracts", q.stats24h.contracts, "24h trades", String(q.stats24h.trades)],
132
145
  ];
133
- console.log(makeTable(["field", "value", "field", "value"], rows) + "\n");
146
+ console.log(makeTable(["field", "value", "field", "value"], rows));
147
+ if (q.venue === "derive")
148
+ console.log(chalk.gray(" Greeks: 3 significant figures; Derive publishes them at a 1e-5 resolution (0 = |value| < 0.00001).\n"));
149
+ else
150
+ console.log();
134
151
  }