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
@@ -19,7 +19,7 @@ import { isDryRun, networkOf, supportsDryRun } from "../cli-flags.js";
19
19
  import { PerpError } from "../errors.js";
20
20
  import { PacificaAdapter } from "../exchanges/pacifica.js";
21
21
  import { resolveVenueId } from "../exchanges/venue.js";
22
- import { PacificaClient } from "../pacifica/client.js";
22
+ import { PacificaClient, requireSolanaAddress } from "../pacifica/client.js";
23
23
  import { logExecution } from "../execution-log.js";
24
24
  import { jsonOk, makeTable, printJson } from "../utils.js";
25
25
  import { mulDecimal } from "../exchanges/precision.js";
@@ -27,7 +27,7 @@ import { cmpDecimal, isPlainDecimal, trimDecimal } from "../exchanges/pacifica-d
27
27
  import { fundsFailStatus } from "./funds.js";
28
28
  import { requireIdempotencyKey, restampPresigned, sendPresigned, signPresigned } from "../exchanges/pacifica-presigned.js";
29
29
  import { buildPrintDepositRequest, buildPrintEndRequest, daysUntil, fetchPrintEvents, fetchPrintGames, fetchPrintHistory, fetchPrintMarket, fetchPrintPositions, findPrintGame, quotePrintOrder, requireEndable, } from "../exchanges/pacifica-print.js";
30
- import { buildVaultDepositRequest, buildVaultWithdrawRequest, DEPOSIT_WHITELIST_API_STATUS, fetchDepositWhitelist, fetchVaults, findVault, heldShares, planVaultDeposit, planVaultWithdraw, withdrawWindowAt, } from "../exchanges/pacifica-vaults.js";
30
+ import { buildVaultDepositRequest, buildVaultWithdrawRequest, DEPOSIT_WHITELIST_API_STATUS, readDepositWhitelist, fetchVaults, findVault, heldShares, planVaultDeposit, planVaultWithdraw, withdrawWindowAt, } from "../exchanges/pacifica-vaults.js";
31
31
  const ts = (ms) => (ms === null ? "—" : new Date(ms).toISOString().replace("T", " ").slice(0, 16) + " UTC");
32
32
  export function registerPacificaEarnCommands(program, getAdapterForExchange, isJson) {
33
33
  /** Pacifica-only: an explicit -e naming another venue is refused (never silently ignored). */
@@ -288,11 +288,11 @@ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJ
288
288
  assertPacifica();
289
289
  const c = publicClient();
290
290
  const v = findVault(await fetchVaults(c, null), ref);
291
- const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address), whitelistOrNull(c, v.address)]);
291
+ const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address), readDepositWhitelist(c, v.address)]);
292
292
  const out = {
293
293
  ...v,
294
294
  live: { accountEquity: acct.account_equity ?? null, availableToWithdraw: acct.available_to_withdraw ?? null, positions: positions.map((p) => ({ symbol: p.symbol, side: p.side, amount: p.amount, entryPrice: p.entry_price })) },
295
- depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist },
295
+ depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist.addresses, error: whitelist.error },
296
296
  withdrawWindow: withdrawWindowAt(v.config, Date.now()),
297
297
  };
298
298
  if (isJson())
@@ -304,7 +304,7 @@ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJ
304
304
  "vault free balance": out.live.availableToWithdraw ?? "—", "performance fee": v.config.managerProfitShare ?? "none", "deposit cap": v.config.depositCap ?? "none",
305
305
  "lock-up after deposit": v.config.depositMinDurationMs === null ? "none" : `${(v.config.depositMinDurationMs / 3_600_000).toFixed(2)} h (reset by every deposit)`,
306
306
  "withdrawal window": `${windowLabel(v)}${out.withdrawWindow.restricted ? ` — now ${out.withdrawWindow.open ? "OPEN until" : "closed, opens"} ${ts(out.withdrawWindow.nextChangeAt)}` : ""}`,
307
- "deposit whitelist": `${whitelist === null ? "unknown" : whitelist.length === 0 ? "none (anyone)" : `${whitelist.length} address(es)`} (unofficial endpoint; the venue decides)`, APY: "not published",
307
+ "deposit whitelist": `${whitelist.addresses === null ? `unknown (${whitelist.error})` : whitelist.addresses.length === 0 ? "none (anyone)" : `${whitelist.addresses.length} address(es)`} (unofficial endpoint; the venue decides)`, APY: "not published",
308
308
  }))
309
309
  console.log(` ${k.padEnd(22)} ${val}`);
310
310
  if (out.live.positions.length > 0)
@@ -339,8 +339,8 @@ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJ
339
339
  const c = publicClient();
340
340
  const s = await masterSigning(o.account, dryRun);
341
341
  const v = findVault(await fetchVaults(c, s.identity.account), ref);
342
- const [info, whitelist] = await Promise.all([c.getAccount(s.identity.account), whitelistOrNull(c, v.address)]);
343
- const plan = planVaultDeposit(v, s.identity.account, amount, { availableToWithdraw: availableOf(info, s.identity.account), whitelist });
342
+ const [info, whitelist] = await Promise.all([c.getAccount(s.identity.account), readDepositWhitelist(c, v.address)]);
343
+ const plan = planVaultDeposit(v, s.identity.account, amount, { availableToWithdraw: availableOf(info, s.identity.account), whitelist: whitelist.addresses });
344
344
  const sharesBefore = heldShares(v, s.identity.account).shares ?? "0";
345
345
  const req = buildVaultDepositRequest(v.address, plan.amount, s.identity, { timestamp: Date.now(), idempotencyKey });
346
346
  if (!isJson())
@@ -393,21 +393,8 @@ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJ
393
393
  });
394
394
  }
395
395
  // ── helpers ──────────────────────────────────────────────────────────────
396
- /** Unofficial whitelist read: null when unreadable (advisory only — the venue's refusal is final). */
397
- async function whitelistOrNull(c, vault) {
398
- try {
399
- return await fetchDepositWhitelist(c, vault);
400
- }
401
- catch {
402
- return null;
403
- }
404
- }
405
396
  function requireAddress(s) {
406
- const a = s.trim();
407
- if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a)) {
408
- throw new PerpError("INVALID_PARAMS", `Not a Solana (base58) address: "${s}"`, { remediation: "Pass a base58 Solana address" });
409
- }
410
- return a;
397
+ return requireSolanaAddress(s);
411
398
  }
412
399
  function parseDirection(s) {
413
400
  const d = s.trim().toLowerCase();
@@ -16,10 +16,26 @@
16
16
  */
17
17
  import type { Command } from "commander";
18
18
  import type { ExchangeAdapter } from "../exchanges/index.js";
19
+ import type { DashboardExchange } from "../dashboard/index.js";
19
20
  import "../exchanges/venues/index.js";
21
+ /**
22
+ * HIP-3 dex names, read live from info `perpDexs` on the selected network (index 0 = the native
23
+ * dex, null). Throws on a failed or malformed read — never substitutes a fixed list (Rule #2):
24
+ * a stale list hides positions on any dex Hyperliquid adds later.
25
+ */
26
+ export declare function fetchHip3DexNames(): Promise<string[]>;
20
27
  /**
21
28
  * Open-position count on a HIP-3 dex; null = the read failed (unknown — NOT "no positions").
22
29
  * The failure is warned on stderr; callers must treat null as "may hold positions".
23
30
  */
24
31
  export declare function checkDexPositions(address: string, dex: string): Promise<number | null>;
32
+ /**
33
+ * HIP-3 dexes holding positions (or whose read failed) for `address`, scanning every dex of the
34
+ * live `perpDexs` list. `error` is set when the dex list itself could not be read: no dex is
35
+ * scanned and the caller must surface it (positions on HIP-3 dexes are unknown, not absent).
36
+ */
37
+ export declare function autoDetectHip3Dexes(address: string, getHLAdapterForDex: (dex: string) => Promise<ExchangeAdapter>, verbose: boolean): Promise<{
38
+ exchanges: DashboardExchange[];
39
+ error: string | null;
40
+ }>;
25
41
  export declare function registerPortfolioCommand(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean, version: string, getHLAdapterForDex?: (dex: string) => Promise<ExchangeAdapter>): void;
@@ -304,7 +304,7 @@ async function runHealth(exchangeList, network) {
304
304
  }
305
305
  // ── Arb top-5 ──────────────────────────────────────────────────────────────
306
306
  async function fetchArbTop() {
307
- const { fetchAllFundingRates, TOP_SYMBOLS } = await import("../funding-rates.js");
307
+ const { fetchAllFundingRates, isRankedComparison, TOP_SYMBOLS } = await import("../funding-rates.js");
308
308
  const { saveFundingSnapshot, getHistoricalAverages } = await import("../funding-history.js");
309
309
  let snapshot;
310
310
  try {
@@ -323,6 +323,7 @@ async function fetchArbTop() {
323
323
  }
324
324
  catch { /* ignore */ }
325
325
  const top = snapshot.symbols
326
+ .filter(isRankedComparison)
326
327
  .filter(s => s.maxSpreadAnnual >= 5)
327
328
  .sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual)
328
329
  .slice(0, 5);
@@ -527,7 +528,31 @@ async function runWatch(resolved, version, getAdapterForExchange, isJsonMode) {
527
528
  }
528
529
  }
529
530
  // ── Serve mode (delegates to dashboard infra) ──────────────────────────────
530
- const KNOWN_HIP3_DEXES = ["xyz", "flx", "hyna", "km", "cash", "vntl"];
531
+ /**
532
+ * HIP-3 dex names, read live from info `perpDexs` on the selected network (index 0 = the native
533
+ * dex, null). Throws on a failed or malformed read — never substitutes a fixed list (Rule #2):
534
+ * a stale list hides positions on any dex Hyperliquid adds later.
535
+ */
536
+ export async function fetchHip3DexNames() {
537
+ const res = await fetch(HYPERLIQUID_API_URL, {
538
+ method: "POST",
539
+ headers: { "Content-Type": "application/json" },
540
+ body: JSON.stringify({ type: "perpDexs" }),
541
+ });
542
+ if (!res.ok)
543
+ throw new Error(`info perpDexs HTTP ${res.status}`);
544
+ const list = await res.json();
545
+ if (!Array.isArray(list))
546
+ throw new Error("info perpDexs: expected an array");
547
+ return list.flatMap((d, i) => {
548
+ if (d === null)
549
+ return [];
550
+ const name = d?.name;
551
+ if (typeof name !== "string" || name === "")
552
+ throw new Error(`info perpDexs: entry ${i} has no dex name`);
553
+ return [name];
554
+ });
555
+ }
531
556
  /**
532
557
  * Open-position count on a HIP-3 dex; null = the read failed (unknown — NOT "no positions").
533
558
  * The failure is warned on stderr; callers must treat null as "may hold positions".
@@ -558,8 +583,22 @@ export async function checkDexPositions(address, dex) {
558
583
  return null;
559
584
  }
560
585
  }
561
- async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
562
- const checks = await Promise.all(KNOWN_HIP3_DEXES.map(async (dex) => ({
586
+ /**
587
+ * HIP-3 dexes holding positions (or whose read failed) for `address`, scanning every dex of the
588
+ * live `perpDexs` list. `error` is set when the dex list itself could not be read: no dex is
589
+ * scanned and the caller must surface it (positions on HIP-3 dexes are unknown, not absent).
590
+ */
591
+ export async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
592
+ let dexes;
593
+ try {
594
+ dexes = await fetchHip3DexNames();
595
+ }
596
+ catch (e) {
597
+ const error = `HIP-3 dex list unavailable — HIP-3 positions unknown, pass --dexes to monitor specific dexes: ${e instanceof Error ? e.message : String(e)}`;
598
+ process.stderr.write(`[warn] hl:hip3 ${error}\n`);
599
+ return { exchanges: [], error };
600
+ }
601
+ const checks = await Promise.all(dexes.map(async (dex) => ({
563
602
  dex,
564
603
  count: await checkDexPositions(address, dex),
565
604
  })));
@@ -580,7 +619,7 @@ async function autoDetectHip3Dexes(address, getHLAdapterForDex, verbose) {
580
619
  process.stderr.write(`[warn] hl:${dex} left out of the dashboard (adapter unavailable${count === null ? ", positions unknown" : `, ${count} position(s)`}): ${e instanceof Error ? e.message : String(e)}\n`);
581
620
  }
582
621
  }
583
- return results;
622
+ return { exchanges: results, error: null };
584
623
  }
585
624
  async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapterForDex) {
586
625
  const { startDashboard } = await import("../dashboard/index.js");
@@ -616,7 +655,9 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
616
655
  const hlAddr = hlEx ? hlEx.adapter.address ?? "" : "";
617
656
  if (hlAddr) {
618
657
  const detected = await autoDetectHip3Dexes(hlAddr, getHLAdapterForDex, false);
619
- exchanges.push(...detected);
658
+ exchanges.push(...detected.exchanges);
659
+ if (detected.error !== null)
660
+ errors["hl:hip3"] = detected.error;
620
661
  }
621
662
  }
622
663
  }
@@ -675,8 +716,10 @@ async function runServe(resolved, getAdapterForExchange, isJsonMode, getHLAdapte
675
716
  if (hlAddr) {
676
717
  console.log(chalk.gray(" Scanning HIP-3 dexes for active positions..."));
677
718
  const detected = await autoDetectHip3Dexes(hlAddr, getHLAdapterForDex, true);
678
- exchanges.push(...detected);
679
- if (!detected.length)
719
+ exchanges.push(...detected.exchanges);
720
+ if (detected.error !== null)
721
+ console.log(chalk.yellow(` ✗ hl:hip3: ${detected.error.slice(0, 120)}`));
722
+ else if (!detected.exchanges.length)
680
723
  console.log(chalk.gray(" No active HIP-3 dex positions found"));
681
724
  }
682
725
  }
@@ -303,7 +303,7 @@ function checkTpSlDirection(positionSide, ref, refLabel, p) {
303
303
  }
304
304
  /** The venue's current mark price for `symbol` (PRICE_STALE when it is not published / not positive). */
305
305
  async function markPriceOf(adapter, symbol) {
306
- const m = (await adapter.getMarkets()).find(x => symbolMatch(x.symbol, symbol));
306
+ const m = (await adapter.getMarkets({ funding: false })).find(x => symbolMatch(x.symbol, symbol));
307
307
  const px = m === undefined ? NaN : Number(m.markPrice);
308
308
  if (!(px > 0) || !Number.isFinite(px)) {
309
309
  throw new PerpError("PRICE_STALE", `${adapter.name} ${symbol}: no mark price to check the TP/SL direction against`, { exchange: adapter.name });
@@ -489,7 +489,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
489
489
  }
490
490
  /** Mark price of `sym` (split sizing / risk); no usable mark → PRICE_STALE, nothing sent. */
491
491
  async function splitMarkPrice(adapter, sym) {
492
- const market = (await adapter.getMarkets()).find(m => symbolMatch(m.symbol, sym));
492
+ const market = (await adapter.getMarkets({ funding: false })).find(m => symbolMatch(m.symbol, sym));
493
493
  const markPrice = market !== undefined ? Number(market.markPrice) : NaN;
494
494
  if (!(markPrice > 0)) {
495
495
  throw new PerpError("PRICE_STALE", `${adapter.name}: no usable mark price for ${sym} — cannot size the split; nothing was sent`, { exchange: adapter.name });
@@ -1489,8 +1489,8 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
1489
1489
  });
1490
1490
  // === Pacifica-only commands ===
1491
1491
  supportsDryRun(trade.command("edit <symbol> <orderId> <price> <size>"))
1492
- .description("Edit an existing order: new price and new open (unfilled) size; the order keeps its TIF / post-only (pacifica: re-created as ALO) — orderId, or c:<clientOrderId> on ondo/arcus/qfex (qfex: plain LIMIT / ALO only; the venue assigns a new order id, the client id is kept)")
1493
- .option("--tif <tif>", "Time-in-force of the replacement (cancel+replace venues only, e.g. ondo; ALO keeps post-only)")
1492
+ .description("Edit an existing order: new price and new open (unfilled) size; the order keeps its TIF / post-only (pacifica: TIF read from the order's client id — ALO via native edit, GTC / TOB via cancel+replace; a new order id either way) — use the orderId the edit returns; orderId, or c:<clientOrderId> on ondo/arcus/qfex (qfex: plain LIMIT / ALO only; the venue assigns a new order id, the client id is kept)")
1493
+ .option("--tif <tif>", "Time-in-force of the replacement (ondo / pacifica only; ALO keeps post-only)")
1494
1494
  .option("--force-tif", "Allow --tif to flip the order's post-only flag (ondo / pacifica); otherwise such a conflict is refused")
1495
1495
  .action(async (symbol, orderId, price, size, opts) => {
1496
1496
  const adapter = await getAdapter();
@@ -70,4 +70,6 @@ export declare function marginEntryBlocks(check: ChainMarginCheck, minMarginPct?
70
70
  * @param maxSlippagePct - Max acceptable slippage (default: 0.3%)
71
71
  * @returns USD size per leg, or 0 if not viable
72
72
  */
73
- export declare function computeAutoSize(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, maxSlippagePct?: number): Promise<number>;
73
+ export declare function computeAutoSize(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, maxSlippagePct?: number,
74
+ /** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
75
+ shortSymbol?: string): Promise<number>;
@@ -94,11 +94,13 @@ export function marginEntryBlocks(check, minMarginPct = 30) {
94
94
  * @param maxSlippagePct - Max acceptable slippage (default: 0.3%)
95
95
  * @returns USD size per leg, or 0 if not viable
96
96
  */
97
- export async function computeAutoSize(longAdapter, shortAdapter, symbol, maxSlippagePct = 0.3) {
97
+ export async function computeAutoSize(longAdapter, shortAdapter, symbol, maxSlippagePct = 0.3,
98
+ /** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
99
+ shortSymbol = symbol) {
98
100
  // Step 1: Get orderbooks from both sides
99
101
  const [longOB, shortOB, longBal, shortBal] = await Promise.all([
100
102
  longAdapter.getOrderbook(symbol),
101
- shortAdapter.getOrderbook(symbol),
103
+ shortAdapter.getOrderbook(shortSymbol),
102
104
  longAdapter.getBalance(),
103
105
  shortAdapter.getBalance(),
104
106
  ]);
@@ -44,11 +44,11 @@ async function pollArbData() {
44
44
  let exchangeStatus = {};
45
45
  let exchangeErrors = {};
46
46
  try {
47
- const { fetchAllFundingRates } = await import("../funding/rates.js");
47
+ const { fetchAllFundingRates, isRankedComparison } = await import("../funding/rates.js");
48
48
  const snapshot = await fetchAllFundingRates({ minSpread: 5 });
49
49
  exchangeStatus = snapshot.exchangeStatus;
50
50
  exchangeErrors = snapshot.exchangeErrors;
51
- for (const sym of snapshot.symbols.slice(0, 20)) {
51
+ for (const sym of snapshot.symbols.filter(isRankedComparison).slice(0, 20)) {
52
52
  opportunities.push({
53
53
  symbol: sym.symbol,
54
54
  longExchange: sym.longExchange,
@@ -235,6 +235,8 @@ function connect() {
235
235
  // ── Helpers ──
236
236
  function fmt(v, d=2) { const n=Number(v)||0; return n.toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}); }
237
237
  function usdOrDash(v, d=2) { return v==null ? '-' : '$'+fmt(v,d); }
238
+ // Market row cells: prices in the row's quote currency (QFEX SAMSUNG-KRW: "220,150.00 KRW", never $); OI is BASE units (never $); funding null = "-" (not 0%).
239
+ function marketCells(m) { const q=(typeof m.quoteAsset==='string'&&m.quoteAsset!=='USD')?escHtml(m.quoteAsset):null; const px=(v,d=2)=>v==null?'-':(q===null?'$'+fmt(v,d):fmt(v,d)+' '+q); const vol=m.volume24h==null&&typeof m.volume24hQuote==='string'?px(m.volume24hQuote,0):usdOrDash(m.volume24h,0); const base=escHtml(String(m.symbol).split(':').pop().replace(/-PERP$/i,'')); return { mark: px(m.markPrice), index: px(m.indexPrice), funding: m.fundingRate==null?'-':(Number(m.fundingRate)*100).toFixed(4)+'%', volume: vol, oi: m.openInterest==null?'-':Number(m.openInterest).toLocaleString('en-US',{maximumFractionDigits:4})+' '+base }; }
238
240
  function pnlClass(v) { return Number(v)>=0?'pnl-pos':'pnl-neg'; }
239
241
  function pnlSign(v) { const n=Number(v)||0; return n>=0?'+$'+fmt(Math.abs(n)):'-$'+fmt(Math.abs(n)); }
240
242
  function rateClass(v) { const n=Number(v)||0; return n>0?'rate-positive':n<0?'rate-negative':'rate-neutral'; }
@@ -243,7 +245,8 @@ function spreadClass(v) { return v>=50?'spread-high':v>=20?'spread-mid':'spread-
243
245
  const NA = '<span style="color:var(--yellow,#e5c07b)">unavailable</span>';
244
246
  function usdOrNA(v, d=2) { return v==null ? NA : '$'+fmt(v,d); }
245
247
  function pnlOrNA(v) { return v==null ? NA : pnlSign(v); }
246
- function escHtml(v) { return String(v).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[c]); }
248
+ // Every venue-supplied string (symbol, quoteAsset, dex, exchange, order fields) goes through this before innerHTML.
249
+ function escHtml(v) { return String(v).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]); }
247
250
 
248
251
  // ── Portfolio Rendering ──
249
252
  function renderTotals(t) {
@@ -325,16 +328,16 @@ function renderPanels(exchanges) {
325
328
  const notional = p.markPrice == null ? null : Math.abs(Number(p.size)) * Number(p.markPrice);
326
329
  const margin = notional == null ? null : p.leverage > 0 ? notional / p.leverage : notional;
327
330
  const roe = margin != null && margin > 0 && p.unrealizedPnl != null ? (Number(p.unrealizedPnl) / margin * 100) : null;
328
- return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${p.side}">\${p.side.toUpperCase()}</td><td>\${p.size}</td><td>\${usdOrNA(p.entryPrice)}</td><td>\${usdOrNA(p.markPrice)}</td><td>\${(p.liquidationPrice==null||p.liquidationPrice==='N/A')?'-':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">\${usdOrNA(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlOrNA(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe==null?NA:(roe>=0?'+':'')+fmt(roe,1)+'%'}</td><td>\${p.leverage}x</td></tr>\`;
331
+ return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${escHtml(dex)}</td>\`:''}<td>\${escHtml(sym)}</td><td class="side-\${escHtml(p.side)}">\${escHtml(String(p.side).toUpperCase())}</td><td>\${escHtml(p.size)}</td><td>\${usdOrNA(p.entryPrice)}</td><td>\${usdOrNA(p.markPrice)}</td><td>\${(p.liquidationPrice==null||p.liquidationPrice==='N/A')?'-':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">\${usdOrNA(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlOrNA(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe==null?NA:(roe>=0?'+':'')+fmt(roe,1)+'%'}</td><td>\${p.leverage}x</td></tr>\`;
329
332
  }).join('')}</tbody></table>\`:(posKnown?'<div class="empty-msg">No open positions</div>':'<div class="empty-msg">Positions '+NA+'</div>')}
330
333
  <div class="section-title">Open Orders (\${ordKnown ? filteredOrders.length : '?'})</div>
331
334
  \${filteredOrders.length?\`<table><thead><tr>\${isHL?'<th>DEX</th>':''}<th>Symbol</th><th>Side</th><th>Type</th><th>Price</th><th>Size</th><th>Filled</th><th>Status</th></tr></thead><tbody>\${filteredOrders.map(o=>{
332
335
  const dex = getDex(o.symbol);
333
336
  const sym = o.symbol.includes(':') ? o.symbol.split(':').slice(1).join(':') : o.symbol;
334
- return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${o.side}">\${o.side.toUpperCase()}</td><td>\${o.type}</td><td>$\${fmt(o.price)}</td><td>\${o.size}</td><td>\${o.filled}</td><td>\${o.status}</td></tr>\`;
337
+ return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${escHtml(dex)}</td>\`:''}<td>\${escHtml(sym)}</td><td class="side-\${escHtml(o.side)}">\${escHtml(String(o.side).toUpperCase())}</td><td>\${escHtml(o.type)}</td><td>$\${fmt(o.price)}</td><td>\${escHtml(o.size)}</td><td>\${escHtml(o.filled)}</td><td>\${escHtml(o.status)}</td></tr>\`;
335
338
  }).join('')}</tbody></table>\`:(ordKnown?'<div class="empty-msg">No open orders</div>':'<div class="empty-msg">Open orders '+NA+'</div>')}
336
339
  <div class="section-title">Markets (Top 10)</div>
337
- \${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);return\`<tr><td>\${m.symbol}</td><td>$\${fmt(m.markPrice)}</td><td>\${usdOrDash(m.indexPrice)}</td><td class="\${rateClass(fr)}">\${(fr*100).toFixed(4)}%</td><td>\${usdOrDash(m.volume24h,0)}</td><td>\${usdOrDash(m.openInterest,0)}</td><td>\${m.maxLeverage==null?'-':m.maxLeverage+'x'}</td></tr>\`;}).join('')}</tbody></table>\`:'<div class="empty-msg">No market data</div>'}
340
+ \${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);const c=marketCells(m);return\`<tr><td>\${escHtml(m.symbol)}</td><td>\${c.mark}</td><td>\${c.index}</td><td class="\${rateClass(fr)}">\${c.funding}</td><td>\${c.volume}</td><td>\${c.oi}</td><td>\${m.maxLeverage==null?'-':m.maxLeverage+'x'}</td></tr>\`;}).join('')}</tbody></table>\`:'<div class="empty-msg">No market data</div>'}
338
341
  </div>\`;
339
342
  }).join('');
340
343
 
@@ -419,7 +422,7 @@ function renderArb() {
419
422
  const barColor = o.spreadAnnual>=50?'var(--green)':o.spreadAnnual>=20?'var(--yellow)':'var(--muted)';
420
423
  const bestMark = o.rates.reduce((best,r) => r.markPrice>0?r.markPrice:best, 0);
421
424
  return \`<tr>
422
- <td style="font-weight:600">\${o.symbol}</td>
425
+ <td style="font-weight:600">\${escHtml(o.symbol)}</td>
423
426
  <td>
424
427
  <div class="\${spreadClass(o.spreadAnnual)}">\${fmt(o.spreadAnnual,1)}%</div>
425
428
  <div class="spread-bar"><div class="spread-bar-fill" style="width:\${barW}%;background:\${barColor}"></div></div>
@@ -514,7 +517,7 @@ function renderEvents() {
514
517
  if (!events.length) { el.innerHTML='<div class="empty-msg">Waiting for events...</div>'; return; }
515
518
  el.innerHTML = events.map(e => {
516
519
  const cls = e.isCrit?'crit':e.isWarn?'warn':'';
517
- return \`<div class="event"><span class="event-time">\${e.time}</span> <span class="event-type \${cls}">[\${e.type}]</span> <span>\${e.exchange}</span> <span style="color:var(--muted)">\${JSON.stringify(e.data).slice(0,120)}</span></div>\`;
520
+ return \`<div class="event"><span class="event-time">\${e.time}</span> <span class="event-type \${cls}">[\${e.type}]</span> <span>\${escHtml(e.exchange)}</span> <span style="color:var(--muted)">\${escHtml(JSON.stringify(e.data).slice(0,120))}</span></div>\`;
518
521
  }).join('');
519
522
  }
520
523
 
package/dist/errors.d.ts CHANGED
@@ -320,3 +320,8 @@ export declare function unknownActionRemediation(venue: string, action: string):
320
320
  * and "1e3" are refused). Returns the trimmed string.
321
321
  */
322
322
  export declare function parsePositiveDecimal(raw: string, what: string): string;
323
+ /**
324
+ * A keyless account read (positions, balance, orders, history): the one typed refusal every venue
325
+ * returns — NO_SIGNER_AVAILABLE with the setup remediation (never an UNKNOWN 500).
326
+ */
327
+ export declare function noAccountKeyError(venue: string): PerpError;
package/dist/errors.js CHANGED
@@ -388,3 +388,13 @@ export function parsePositiveDecimal(raw, what) {
388
388
  }
389
389
  return s;
390
390
  }
391
+ /**
392
+ * A keyless account read (positions, balance, orders, history): the one typed refusal every venue
393
+ * returns — NO_SIGNER_AVAILABLE with the setup remediation (never an UNKNOWN 500).
394
+ */
395
+ export function noAccountKeyError(venue) {
396
+ return new PerpError("NO_SIGNER_AVAILABLE", `${venue}: no account key configured — account data unavailable`, {
397
+ exchange: venue,
398
+ remediation: `Set up an account for ${venue}: perp setup (or perp -e ${venue} wallet show to check)`,
399
+ });
400
+ }
@@ -0,0 +1,8 @@
1
+ export type AddressKind = "evm" | "solana";
2
+ export declare function isEvmAddress(value: string): boolean;
3
+ /** A base58 string that decodes to a 32-byte public key. */
4
+ export declare function isSolanaAddress(value: string): boolean;
5
+ /** Pacifica account / Print order / vault address: base58 32 bytes, else INVALID_PARAMS. */
6
+ export declare function requireSolanaAddress(value: string, field?: string): string;
7
+ /** `address` in the form `kind` expects (trimmed), else INVALID_PARAMS naming the expected form. */
8
+ export declare function requireAddressOfKind(venue: string, kind: AddressKind, address: string): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Public account address formats (keyless reads by address): EVM `0x` + 40 hex (Hyperliquid,
3
+ * Lighter) and base58 Solana public keys (Pacifica). A wrong format is refused with
4
+ * INVALID_PARAMS before any request — the venues answer a foreign address with an empty success
5
+ * (Pacifica `success: true, []`), which would read as "no positions".
6
+ */
7
+ import bs58 from "bs58";
8
+ import { PerpError } from "../errors.js";
9
+ const EVM_RE = /^0x[0-9a-fA-F]{40}$/;
10
+ export function isEvmAddress(value) {
11
+ return EVM_RE.test(value.trim());
12
+ }
13
+ /** A base58 string that decodes to a 32-byte public key. */
14
+ export function isSolanaAddress(value) {
15
+ try {
16
+ return bs58.decode(value.trim()).length === 32;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /** Pacifica account / Print order / vault address: base58 32 bytes, else INVALID_PARAMS. */
23
+ export function requireSolanaAddress(value, field = "address") {
24
+ const a = value.trim();
25
+ if (!isSolanaAddress(a)) {
26
+ throw new PerpError("INVALID_PARAMS", `Not a Solana (base58) ${field}: "${value}"${isEvmAddress(a) ? " — that is an EVM address; Pacifica accounts are Solana addresses" : ""}`, {
27
+ exchange: "pacifica", remediation: "Pass the base58 Solana address you use on Pacifica",
28
+ });
29
+ }
30
+ return a;
31
+ }
32
+ /** `address` in the form `kind` expects (trimmed), else INVALID_PARAMS naming the expected form. */
33
+ export function requireAddressOfKind(venue, kind, address) {
34
+ const a = address.trim();
35
+ if (kind === "evm") {
36
+ if (isEvmAddress(a))
37
+ return a;
38
+ throw new PerpError("INVALID_PARAMS", `${venue} expects an EVM 0x address (0x + 40 hex)${isSolanaAddress(a) ? " — that is a Solana address" : ""}: "${address}"`, {
39
+ exchange: venue, remediation: `Pass the 0x address of the ${venue} account`,
40
+ });
41
+ }
42
+ if (isSolanaAddress(a))
43
+ return a;
44
+ throw new PerpError("INVALID_PARAMS", `${venue} expects a base58 Solana address${isEvmAddress(a) ? " — that is an EVM address" : ""}: "${address}"`, {
45
+ exchange: venue, remediation: `Pass the base58 Solana address of the ${venue} account`,
46
+ });
47
+ }
@@ -22,7 +22,7 @@
22
22
  * an `entryTpsl` bundle.
23
23
  */
24
24
  import type { EvmSigner } from "../../signer/interface.js";
25
- import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade } from "../interface.js";
25
+ import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade, FundingHistoryRow } from "../interface.js";
26
26
  import type { ResolvedSlippage } from "../slippage.js";
27
27
  import { type TradingV2 } from "../trading-v2.js";
28
28
  import type { VenueCreateContext, VenueDefinition } from "../venue.js";
@@ -37,6 +37,8 @@ export declare const ARCUS_GOOD_TIL_HORIZON_MS: number;
37
37
  export declare const ARCUS_MAX_PROTECTION_DEVIATION = 0.1;
38
38
  /** clientId max (36) minus the "-tp"/"-sl" suffix of entryTpsl child legs. */
39
39
  export declare const ARCUS_TPSL_CLIENT_ID_MAX = 33;
40
+ /** /v1/trades and /v1/fundingRates take `limit` ≤ 1000 (docs; the venue answered 1000 rows for more). */
41
+ export declare const ARCUS_HISTORY_CAP = 1000;
40
42
  export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
41
43
  readonly name: "arcus";
42
44
  readonly venue: VenueDefinition;
@@ -57,6 +59,12 @@ export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
57
59
  private accountQuery;
58
60
  private loadMarkets;
59
61
  private market;
62
+ /**
63
+ * A market-data read (book / trades / funding / candles) of a listed market that is not ONLINE
64
+ * is refused like `market info` / `market list` treat it (not listed): SYMBOL_NOT_FOUND naming
65
+ * the venue status — the venue answered `[]` or HTTP 404 ORDER_NOT_FOUND for it before.
66
+ */
67
+ private onlineMarket;
60
68
  private symbolOf;
61
69
  /** Protection price: slippage from `ref`, aligned toward crossing on the tier tick, ≤ 10 % from `ref`. */
62
70
  private protectionPrice;
@@ -144,11 +152,7 @@ export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
144
152
  asks: [string, string][];
145
153
  }>;
146
154
  getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
147
- getFundingHistory(symbol: string, limit?: number): Promise<{
148
- time: number;
149
- rate: string;
150
- price: string | null;
151
- }[]>;
155
+ getFundingHistory(symbol: string, limit?: number): Promise<FundingHistoryRow[]>;
152
156
  getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
153
157
  private rawPositions;
154
158
  getBalance(): Promise<ExchangeBalance>;
@@ -22,6 +22,7 @@
22
22
  * an `entryTpsl` bundle.
23
23
  */
24
24
  import { PerpError } from "../../errors.js";
25
+ import { isArcusOnlineMarket } from "../../ws/arcus-market.js";
25
26
  import { OwsEvmSigner } from "../../signer/ows-evm.js";
26
27
  import { OwsSolanaSigner } from "../../signer/ows-solana.js";
27
28
  import { alignProtectionPrice, floorSize, mulDecimal, requirePositive, slippagePrice, subDecimal, toUnitsExact, unitsToString } from "../precision.js";
@@ -90,6 +91,15 @@ function finalityOf(s) {
90
91
  return s === "filled" || s === "rejected" || s === "cancelled" ? "final" : "pending";
91
92
  }
92
93
  const usToMs = (us) => Math.floor(us / 1000);
94
+ /** /v1/trades and /v1/fundingRates take `limit` ≤ 1000 (docs; the venue answered 1000 rows for more). */
95
+ export const ARCUS_HISTORY_CAP = 1000;
96
+ function arcusLimit(what, limit) {
97
+ if (!Number.isInteger(limit) || limit < 1 || limit > ARCUS_HISTORY_CAP) {
98
+ throw new PerpError("INVALID_PARAMS", `Arcus: ${what} limit must be 1..${ARCUS_HISTORY_CAP} (got ${limit})`, {
99
+ remediation: `Arcus serves at most ${ARCUS_HISTORY_CAP} rows per request`,
100
+ });
101
+ }
102
+ }
93
103
  function sideLower(s) { return s === "BUY" ? "buy" : "sell"; }
94
104
  function tifFromRead(t) {
95
105
  // Reads report resting (GTT) orders as the legacy "GTC".
@@ -281,6 +291,21 @@ export class ArcusAdapter {
281
291
  }
282
292
  return m;
283
293
  }
294
+ /**
295
+ * A market-data read (book / trades / funding / candles) of a listed market that is not ONLINE
296
+ * is refused like `market info` / `market list` treat it (not listed): SYMBOL_NOT_FOUND naming
297
+ * the venue status — the venue answered `[]` or HTTP 404 ORDER_NOT_FOUND for it before.
298
+ */
299
+ async onlineMarket(symbol) {
300
+ const m = await this.market(symbol);
301
+ if (!isArcusOnlineMarket(m)) {
302
+ throw new PerpError("SYMBOL_NOT_FOUND", `Arcus: market ${m.marketDisplayName} is ${m.status} (not tradable; no live market data)`, {
303
+ exchange: "arcus", status: m.status, tradable: false,
304
+ remediation: "List live markets with: perp market list -e arcus",
305
+ });
306
+ }
307
+ return m;
308
+ }
284
309
  async symbolOf(marketId) {
285
310
  let m = (await this.loadMarkets(false)).byId.get(marketId);
286
311
  if (!m)
@@ -878,20 +903,22 @@ export class ArcusAdapter {
878
903
  return (await this.getVenueMarkets()).map(toLegacyMarketInfo);
879
904
  }
880
905
  async getOrderbook(symbol) {
881
- const m = await this.market(symbol);
906
+ const m = await this.onlineMarket(symbol);
882
907
  const ob = await this.client.get(`/v1/l2OrderBook/${encodeURIComponent(m.marketDisplayName)}`, "l2OrderBook", { nLevels: ORDERBOOK_LEVELS });
883
908
  return { bids: ob.bids, asks: ob.asks };
884
909
  }
885
910
  async getRecentTrades(symbol, limit = 50) {
886
- const m = await this.market(symbol);
911
+ arcusLimit("trades", limit);
912
+ const m = await this.onlineMarket(symbol);
887
913
  const res = await this.client.get("/v1/trades", "trades", { market: m.marketDisplayName, limit });
888
- // Public tape carries no fee; "0" follows the legacy ExchangeTrade convention for public trades.
914
+ // Public tape: no fee applies to the viewer → null, never "0".
889
915
  return res.trades.map(t => ({ time: usToMs(t.timestamp), symbol: m.baseAsset, side: sideLower(t.side), price: t.price, size: t.size, fee: null }));
890
916
  }
891
917
  async getFundingHistory(symbol, limit = 100) {
892
- const m = await this.market(symbol);
918
+ arcusLimit("funding history", limit);
919
+ const m = await this.onlineMarket(symbol);
893
920
  const res = await this.client.get("/v1/fundingRates", "fundingRates", { market: m.marketDisplayName, limit });
894
- return res.fundingRates.map(f => ({ time: usToMs(f.time), rate: f.fundingRate, price: null }));
921
+ return res.fundingRates.map(f => ({ time: usToMs(f.time), rate: f.fundingRate, price: null, intervalHours: 1, rateKind: "lastSettled" }));
895
922
  }
896
923
  async getKlines(symbol, interval, startTime, endTime) {
897
924
  if (!CANDLE_TIMEFRAMES.has(interval)) {
@@ -899,7 +926,7 @@ export class ArcusAdapter {
899
926
  remediation: `Use one of: ${[...CANDLE_TIMEFRAMES].join(", ")}`,
900
927
  });
901
928
  }
902
- const m = await this.market(symbol);
929
+ const m = await this.onlineMarket(symbol);
903
930
  const span = CANDLE_WINDOW_BARS * CANDLE_MS[interval];
904
931
  const windows = Math.max(1, Math.ceil((endTime - startTime) / span));
905
932
  if (windows > CANDLE_MAX_WINDOWS) {
@@ -24,6 +24,7 @@
24
24
  * - tradable: status === "ONLINE".
25
25
  */
26
26
  import { PerpError } from "../../errors.js";
27
+ import { isArcusOnlineMarket } from "../../ws/arcus-market.js";
27
28
  import { canonicalFor } from "../canonical-symbols.js";
28
29
  import { arcusVenue } from "../venues/arcus.js";
29
30
  import { ArcusClient } from "./client.js";
@@ -101,7 +102,7 @@ export function toArcusVenueMarket(m) {
101
102
  maxLeverage: maxLeverageOf(m),
102
103
  marginTiers: null,
103
104
  session: sessionOf(m),
104
- tradable: m.status === "ONLINE",
105
+ tradable: isArcusOnlineMarket(m),
105
106
  };
106
107
  }
107
108
  /**
@@ -110,7 +111,7 @@ export function toArcusVenueMarket(m) {
110
111
  * market / funding lists rather than listed at a $0 price and 0 % funding.
111
112
  */
112
113
  export function onlineArcusMarkets(raw) {
113
- return raw.filter(m => m.status === "ONLINE");
114
+ return raw.filter(isArcusOnlineMarket);
114
115
  }
115
116
  async function fetchMarkets(net) {
116
117
  const raw = await fetchArcusRawMarkets(new ArcusClient(net, null));