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,12 +1,12 @@
1
1
  import { nonEmptyOrListed, symbolNotFound } from "./unknown-symbol.js";
2
- import { withTapeSkipped } from "./interface.js";
2
+ import { assertCandleInterval, withTapeSkipped } from "./interface.js";
3
3
  import { PacificaClient } from "../pacifica/index.js";
4
4
  import { isPacificaRateLimited } from "../pacifica/rate-limit.js";
5
5
  import { LocalSolanaSigner } from "../signer/index.js";
6
6
  import { isExpired } from "../agent-wallet/expiry.js";
7
7
  import { isPacificaPerpetual } from "../ws/pacifica-instrument.js";
8
8
  import { randomUUID } from "crypto";
9
- import { PerpError, extractErrorMessage, isDefiniteOrderFailure, orderNotSent, orderOutcomeUnknown } from "../errors.js";
9
+ import { PerpError, extractErrorMessage, isDefiniteOrderFailure, orderNotSent, orderOutcomeUnknown, noAccountKeyError } from "../errors.js";
10
10
  import { parseFiniteVenueNumber, venueRateField } from "../utils/numeric.js";
11
11
  import { unsupported, validateLegacyTif } from "./trading-v2.js";
12
12
  import { floorSize, requireOnTick, subDecimal } from "./precision.js";
@@ -318,6 +318,24 @@ function pacificaBodyError(r) {
318
318
  return String(o.error ?? "request rejected");
319
319
  return null;
320
320
  }
321
+ /** GET /trades answers the last 20 fills (40 taker + maker rows); no limit or cursor. */
322
+ export const PACIFICA_TRADES_CAP = 20;
323
+ /** GET /funding_rate/history `limit` maximum per request (docs; live 4001 → 4000). */
324
+ export const PACIFICA_FUNDING_PAGE = 4000;
325
+ /** GET /kline: "Max range: 4000 candles" per request (HTTP 400 beyond, live 2026-10-19). */
326
+ export const PACIFICA_KLINE_MAX_BARS = 4000;
327
+ /** Requests one funding / kline read may send (the venue meters reads by credits). */
328
+ export const PACIFICA_MAX_PAGES = 10;
329
+ /**
330
+ * GET /kline intervals (venue error text, live 2026-10-19: "Must be one of 1m, 3m, 5m, 15m, 30m,
331
+ * 1h, 2h, 4h, 8h, 12h, 1d, 1w, 1M"; each answered 200 with rows labelled `i` = the interval).
332
+ * The ms length sizes the 4000-bar request windows (1M: 31 days, the longest month).
333
+ */
334
+ export const PACIFICA_KLINE_INTERVALS = {
335
+ "1m": 60_000, "3m": 180_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000,
336
+ "1h": 3_600_000, "2h": 7_200_000, "4h": 14_400_000, "8h": 28_800_000, "12h": 43_200_000,
337
+ "1d": 86_400_000, "1w": 604_800_000, "1M": 2_678_400_000,
338
+ };
321
339
  /** /info rows must carry instrument_type — a row without it cannot be classified perp vs spot. */
322
340
  function requireInstrumentType(rows) {
323
341
  for (const m of rows) {
@@ -368,6 +386,124 @@ function pacificaField(row, key, endpoint) {
368
386
  return String(v);
369
387
  throw new PerpError("EXCHANGE_ERROR", `Pacifica ${endpoint} row without a readable ${key}: ${JSON.stringify(row).slice(0, 160)}`, { exchange: "pacifica" });
370
388
  }
389
+ /**
390
+ * Balance of a Pacifica account from GET /account, its positions and /info/prices — the one
391
+ * implementation behind PacificaAdapter.getBalance() and the keyless `account balance --address`.
392
+ */
393
+ export function pacificaBalanceOf(info, positions, prices) {
394
+ const raw = info;
395
+ const priceMap = new Map(prices.map((p) => [p.symbol, p]));
396
+ // API often returns unrealized_pnl=0 — compute from price delta when needed
397
+ let totalPnl = 0;
398
+ for (const p of positions) {
399
+ let upnl = parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "pacifica");
400
+ const amount = parseFiniteVenueNumber(p.amount, "position.amount", "pacifica");
401
+ if (upnl === 0 && amount > 0) {
402
+ const mark = parseFiniteVenueNumber(priceMap.get(p.symbol)?.mark ?? p.mark_price, "position.mark", "pacifica");
403
+ const entry = parseFiniteVenueNumber(p.entry_price, "position.entry_price", "pacifica");
404
+ const dir = p.side === "bid" ? 1 : -1;
405
+ if (mark > 0 && entry > 0)
406
+ upnl = (mark - entry) * amount * dir;
407
+ }
408
+ totalPnl += upnl;
409
+ }
410
+ // Unified margin: a negative USDC `balance` is a loan; `pending_interest` is accrued,
411
+ // unpaid loan interest (docs get-account-info). Surfaced, not folded into equity.
412
+ const usdcBalance = info.balance;
413
+ if (typeof usdcBalance !== "string") {
414
+ throw new PerpError("EXCHANGE_ERROR", "Pacifica account info has no USDC `balance`", { exchange: "pacifica", venueRaw: raw });
415
+ }
416
+ const balance = {
417
+ equity: info.account_equity,
418
+ available: info.available_to_spend,
419
+ marginUsed: String(raw.total_margin_used ?? raw.margin_used ?? "0"),
420
+ unrealizedPnl: totalPnl.toFixed(4),
421
+ usdcBalance,
422
+ borrowedUsdc: usdcBalance.startsWith("-") ? usdcBalance.slice(1) : "0",
423
+ pendingInterest: typeof info.pending_interest === "string" ? info.pending_interest : null,
424
+ availableToWithdraw: info.available_to_withdraw,
425
+ };
426
+ return balance;
427
+ }
428
+ /**
429
+ * Open positions of one Pacifica account (`GET /positions?account=`), marked with `/info/prices`.
430
+ * The ONE mapper of the keyed `getPositions` and the public-address read: unresolved leverage is
431
+ * read from the account's `/account/settings` (public, by address), then the market max.
432
+ */
433
+ export async function pacificaPositionRows(positions, prices, deps) {
434
+ const priceMap = new Map(prices.map((p) => [p.symbol, p]));
435
+ const mapped = positions.map((p) => {
436
+ const mark = priceMap.get(p.symbol)?.mark ?? p.mark_price ?? "0";
437
+ const side = p.side === "bid" ? "long" : "short";
438
+ const size = parseFiniteVenueNumber(p.amount, "position.amount", "pacifica");
439
+ const entry = parseFiniteVenueNumber(p.entry_price, "position.entry_price", "pacifica");
440
+ const markNum = parseFiniteVenueNumber(mark, "position.mark", "pacifica");
441
+ // API often returns unrealized_pnl=0 — compute from price delta when needed
442
+ let upnl = parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "pacifica");
443
+ if (upnl === 0 && size > 0 && entry > 0 && markNum > 0) {
444
+ const dir = side === "long" ? 1 : -1;
445
+ upnl = (markNum - entry) * size * dir;
446
+ }
447
+ // Leverage, in order: (1) the API's own `leverage` when present; (2) for an ISOLATED
448
+ // position, entry-notional / margin — the docs define `margin` as "Amount of margin
449
+ // allocated to an isolated position (only shown when isolated)", so a cross position
450
+ // reports "0" here; (3)/(4) resolved below from GET /account/settings, then the market max.
451
+ const marginNum = parseFiniteVenueNumber(p.margin ?? p.margin_used, "position.margin", "pacifica", { defaultValue: 0 });
452
+ const apiLev = typeof p.leverage === "number" && p.leverage > 0 ? p.leverage : 0;
453
+ const entryNotional = size * entry;
454
+ const derivedLev = marginNum > 0 && entryNotional > 0 ? Math.round(entryNotional / marginNum) : 0;
455
+ return {
456
+ symbol: p.symbol,
457
+ side: side,
458
+ size: String(p.amount),
459
+ entryPrice: String(p.entry_price),
460
+ markPrice: mark,
461
+ liquidationPrice: p.liquidation_price != null ? String(p.liquidation_price) : null,
462
+ unrealizedPnl: upnl.toFixed(4),
463
+ // 0 = unresolved; resolved from the account settings / market max below.
464
+ leverage: apiLev > 0 ? apiLev : derivedLev,
465
+ };
466
+ });
467
+ // Unresolved (cross) positions: (3) the symbol's entry in GET /account/settings
468
+ // `margin_settings` — "Current leverage set by the user"; (4) no entry = the symbol is at its
469
+ // default, and the docs (get-account-settings) say "all markets have margin settings default
470
+ // to cross margin and leverage default to max ... will return blank" → the market's max
471
+ // leverage. Neither known → EXCHANGE_ERROR (never a fabricated value). Both reads happen ONLY
472
+ // when something is unresolved; a failure of either propagates.
473
+ if (mapped.some((m) => m.leverage <= 0)) {
474
+ const settings = await deps.accountSettings();
475
+ if (settings === null || typeof settings !== "object" || !Array.isArray(settings.margin_settings)) {
476
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings returned no margin_settings list (got ${JSON.stringify(settings).slice(0, 160)})`, { exchange: "pacifica" });
477
+ }
478
+ const setLev = new Map();
479
+ for (const s of settings.margin_settings) {
480
+ if (typeof s?.symbol !== "string" || typeof s.leverage !== "number" || !(s.leverage > 0)) {
481
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings: malformed margin_settings entry ${JSON.stringify(s)}`, { exchange: "pacifica" });
482
+ }
483
+ setLev.set(s.symbol, s.leverage);
484
+ }
485
+ let maxLevMap = null;
486
+ for (const m of mapped) {
487
+ if (m.leverage > 0)
488
+ continue;
489
+ const configured = setLev.get(m.symbol);
490
+ if (configured !== undefined) {
491
+ m.leverage = configured;
492
+ continue;
493
+ }
494
+ if (maxLevMap === null)
495
+ maxLevMap = await deps.maxLeverage();
496
+ const max = maxLevMap.get(m.symbol);
497
+ if (typeof max !== "number" || !(max > 0)) {
498
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica ${m.symbol}: the position's leverage cannot be determined (no leverage on the position, no /account/settings entry, and no max leverage for the market)`, {
499
+ exchange: "pacifica", remediation: "Check the market: perp -e pacifica market info " + m.symbol,
500
+ });
501
+ }
502
+ m.leverage = max;
503
+ }
504
+ }
505
+ return mapped;
506
+ }
371
507
  export class PacificaAdapter {
372
508
  name = "pacifica";
373
509
  chain = "solana";
@@ -391,6 +527,7 @@ export class PacificaAdapter {
391
527
  signMessage;
392
528
  _marketsCache = null;
393
529
  _marketsCacheTime = 0;
530
+ _infoCache = null;
394
531
  static CACHE_TTL = 30_000;
395
532
  /** Venue network — part of every account cache key (the same key exists on mainnet and testnet). */
396
533
  _network;
@@ -569,14 +706,31 @@ export class PacificaAdapter {
569
706
  get sdk() {
570
707
  return this.client;
571
708
  }
709
+ /**
710
+ * The configured account for an account read (settings, TWAP orders, …): no Pacifica key
711
+ * configured → NO_SIGNER_AVAILABLE. A keyless adapter holds a generated throwaway keypair, whose
712
+ * empty account would otherwise read as "no settings / no orders".
713
+ */
714
+ accountForReads() {
715
+ if (!this._hasRealKey)
716
+ throw noAccountKeyError("pacifica");
717
+ return this.account;
718
+ }
572
719
  get signer() {
573
720
  return this.signMessage;
574
721
  }
722
+ /** GET /info rows (every instrument type), one CACHE_TTL cache for symbol resolution and getMarkets. */
723
+ async infoRows() {
724
+ if (!this._infoCache || Date.now() - this._infoCache.at >= PacificaAdapter.CACHE_TTL) {
725
+ this._infoCache = { rows: requireInstrumentType(await this.client.getInfo()), at: Date.now() };
726
+ }
727
+ return this._infoCache.rows;
728
+ }
575
729
  async getMarkets() {
576
730
  if (this._marketsCache && Date.now() - this._marketsCacheTime < PacificaAdapter.CACHE_TTL)
577
731
  return this._marketsCache;
578
732
  const [markets, prices] = await Promise.all([
579
- this.client.getInfo(),
733
+ this.infoRows(),
580
734
  this._getPrices(),
581
735
  ]);
582
736
  const priceMap = new Map(prices.map((p) => [p.symbol, p]));
@@ -591,14 +745,19 @@ export class PacificaAdapter {
591
745
  symbol: m.symbol,
592
746
  markPrice: p?.mark ?? "-",
593
747
  indexPrice: p?.oracle ?? null,
594
- fundingRate: m.next_funding_rate ?? m.funding_rate,
748
+ // `fundingRate` = the LAST SETTLED hourly rate (/info funding_rate — the WS prices `funding`,
749
+ // = the newest /funding_rate/history print); the rate expected at the next settlement
750
+ // (next_funding_rate, WS `next_funding` / predictedRate) is `predictedFundingRate`.
751
+ fundingRate: m.funding_rate ?? null,
752
+ fundingRateKind: m.funding_rate != null ? "lastSettled" : null,
753
+ predictedFundingRate: m.next_funding_rate ?? null,
595
754
  volume24h: p?.volume_24h ?? null,
596
755
  openInterest: p?.open_interest ?? null,
597
756
  maxLeverage: m.max_leverage,
598
757
  sizeDecimals,
599
758
  stepSize: validLot ? m.lot_size : null,
600
- // Pacifica publishes neither a per-market funding interval nor margin tiers.
601
- fundingHours: null,
759
+ // Uniform hourly funding (venue fundingRateBasis perHours 1); no margin tiers published.
760
+ fundingHours: 1,
602
761
  marginTiers: null,
603
762
  };
604
763
  });
@@ -606,7 +765,63 @@ export class PacificaAdapter {
606
765
  this._marketsCacheTime = Date.now();
607
766
  return result;
608
767
  }
768
+ /**
769
+ * The venue's own spelling of a perp market typed by the user. Pacifica's API is
770
+ * case-sensitive (kBONK / kPEPE / kSHIB: `KPEPE` answers `200 []` on /trades, /kline and
771
+ * /funding_rate/history and 400 "Book not found" on /book — live 2026-10-19), and the CLI
772
+ * upper-cases the argument. Matched against /info exactly first, then case-insensitively
773
+ * (a "-PERP" display suffix ignored), as `market info` does. A listed non-perp (spot SOL-USDC)
774
+ * is refused before any request: this adapter serves perps only (isPacificaPerpetual).
775
+ */
776
+ async wireSymbol(symbol) {
777
+ const rows = await this.infoRows();
778
+ const want = symbol.replace(/-PERP$/i, "");
779
+ const exact = rows.filter((m) => m.symbol === want);
780
+ const matches = exact.length > 0 ? exact : rows.filter((m) => m.symbol.toUpperCase() === want.toUpperCase());
781
+ if (matches.length === 0)
782
+ throw symbolNotFound("pacifica", symbol);
783
+ if (matches.length > 1) {
784
+ throw new PerpError("INVALID_PARAMS", `Pacifica lists several markets matching ${symbol} (${matches.map((m) => m.symbol).join(", ")}); type the exact symbol`, { exchange: "pacifica" });
785
+ }
786
+ const m = matches[0];
787
+ if (!isPacificaPerpetual(m)) {
788
+ throw symbolNotFound("pacifica", symbol, `Pacifica ${m.symbol} is a ${m.instrument_type} market, not a perp — the perp market data reads serve perpetuals only`);
789
+ }
790
+ return m.symbol;
791
+ }
792
+ /**
793
+ * wireSymbol for an order entry point (market / limit / stop / edit): a resolution failure is
794
+ * a definite "not sent" (orderNotSent re-types a read error), never a retryable code.
795
+ */
796
+ async orderSymbol(symbol, what) {
797
+ try {
798
+ return await this.wireSymbol(symbol);
799
+ }
800
+ catch (e) {
801
+ throw orderNotSent("pacifica", `${what} ${symbol}`, e);
802
+ }
803
+ }
804
+ /**
805
+ * The venue's spelling for a cancel / TP-SL on an existing order or position: resolved as in
806
+ * wireSymbol when /info lists it (any instrument type, case-insensitive); otherwise — not listed
807
+ * (a delisted market) or /info unreadable — sent as typed: a cancel must still reach the venue,
808
+ * whose own answer is final.
809
+ */
810
+ async listedSymbol(symbol) {
811
+ let rows;
812
+ try {
813
+ rows = await this.infoRows();
814
+ }
815
+ catch {
816
+ return symbol;
817
+ }
818
+ const want = symbol.replace(/-PERP$/i, "");
819
+ const exact = rows.filter((m) => m.symbol === want);
820
+ const hits = exact.length > 0 ? exact : rows.filter((m) => m.symbol.toUpperCase() === want.toUpperCase());
821
+ return hits.length === 1 ? hits[0].symbol : symbol;
822
+ }
609
823
  async getOrderbook(symbol) {
824
+ symbol = await this.wireSymbol(symbol);
610
825
  const book = await this.client.getBook(symbol).catch((e) => {
611
826
  // GET /book for an unlisted symbol: HTTP 400 {"error":"Book not found: NOPE"} (live 2026-10-14).
612
827
  if (e instanceof Error && /^GET \/book failed \(400\):.*Book not found/.test(e.message))
@@ -620,125 +835,25 @@ export class PacificaAdapter {
620
835
  }
621
836
  async getBalance() {
622
837
  if (!this._hasRealKey)
623
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
838
+ throw noAccountKeyError("pacifica");
624
839
  const [info, positions, prices] = await Promise.all([
625
840
  this.client.getAccount(this.account),
626
841
  this._getPositions(),
627
842
  this._getPrices(),
628
843
  ]);
629
- const raw = info;
630
- const priceMap = new Map(prices.map((p) => [p.symbol, p]));
631
- // API often returns unrealized_pnl=0 — compute from price delta when needed
632
- let totalPnl = 0;
633
- for (const p of positions) {
634
- let upnl = parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "pacifica");
635
- const amount = parseFiniteVenueNumber(p.amount, "position.amount", "pacifica");
636
- if (upnl === 0 && amount > 0) {
637
- const mark = parseFiniteVenueNumber(priceMap.get(p.symbol)?.mark ?? p.mark_price, "position.mark", "pacifica");
638
- const entry = parseFiniteVenueNumber(p.entry_price, "position.entry_price", "pacifica");
639
- const dir = p.side === "bid" ? 1 : -1;
640
- if (mark > 0 && entry > 0)
641
- upnl = (mark - entry) * amount * dir;
642
- }
643
- totalPnl += upnl;
644
- }
645
- // Unified margin: a negative USDC `balance` is a loan; `pending_interest` is accrued,
646
- // unpaid loan interest (docs get-account-info). Surfaced, not folded into equity.
647
- const usdcBalance = info.balance;
648
- if (typeof usdcBalance !== "string") {
649
- throw new PerpError("EXCHANGE_ERROR", "Pacifica account info has no USDC `balance`", { exchange: "pacifica", venueRaw: raw });
650
- }
651
- const balance = {
652
- equity: info.account_equity,
653
- available: info.available_to_spend,
654
- marginUsed: String(raw.total_margin_used ?? raw.margin_used ?? "0"),
655
- unrealizedPnl: totalPnl.toFixed(4),
656
- usdcBalance,
657
- borrowedUsdc: usdcBalance.startsWith("-") ? usdcBalance.slice(1) : "0",
658
- pendingInterest: typeof info.pending_interest === "string" ? info.pending_interest : null,
659
- availableToWithdraw: info.available_to_withdraw,
660
- };
661
- return balance;
844
+ return pacificaBalanceOf(info, positions, prices);
662
845
  }
663
846
  async getPositions() {
664
847
  if (!this._hasRealKey)
665
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
848
+ throw noAccountKeyError("pacifica");
666
849
  const [positions, prices] = await Promise.all([
667
850
  this._getPositions(),
668
851
  this._getPrices(),
669
852
  ]);
670
- const priceMap = new Map(prices.map((p) => [p.symbol, p]));
671
- const mapped = positions.map((p) => {
672
- const mark = priceMap.get(p.symbol)?.mark ?? p.mark_price ?? "0";
673
- const side = p.side === "bid" ? "long" : "short";
674
- const size = parseFiniteVenueNumber(p.amount, "position.amount", "pacifica");
675
- const entry = parseFiniteVenueNumber(p.entry_price, "position.entry_price", "pacifica");
676
- const markNum = parseFiniteVenueNumber(mark, "position.mark", "pacifica");
677
- // API often returns unrealized_pnl=0 — compute from price delta when needed
678
- let upnl = parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "pacifica");
679
- if (upnl === 0 && size > 0 && entry > 0 && markNum > 0) {
680
- const dir = side === "long" ? 1 : -1;
681
- upnl = (markNum - entry) * size * dir;
682
- }
683
- // Leverage, in order: (1) the API's own `leverage` when present; (2) for an ISOLATED
684
- // position, entry-notional / margin — the docs define `margin` as "Amount of margin
685
- // allocated to an isolated position (only shown when isolated)", so a cross position
686
- // reports "0" here; (3)/(4) resolved below from GET /account/settings, then the market max.
687
- const marginNum = parseFiniteVenueNumber(p.margin ?? p.margin_used, "position.margin", "pacifica", { defaultValue: 0 });
688
- const apiLev = typeof p.leverage === "number" && p.leverage > 0 ? p.leverage : 0;
689
- const entryNotional = size * entry;
690
- const derivedLev = marginNum > 0 && entryNotional > 0 ? Math.round(entryNotional / marginNum) : 0;
691
- return {
692
- symbol: p.symbol,
693
- side: side,
694
- size: String(p.amount),
695
- entryPrice: String(p.entry_price),
696
- markPrice: mark,
697
- liquidationPrice: p.liquidation_price != null ? String(p.liquidation_price) : null,
698
- unrealizedPnl: upnl.toFixed(4),
699
- // 0 = unresolved; resolved from the account settings / market max below.
700
- leverage: apiLev > 0 ? apiLev : derivedLev,
701
- };
853
+ return pacificaPositionRows(positions, prices, {
854
+ accountSettings: () => this.client.getAccountSettings(this.account),
855
+ maxLeverage: async () => new Map((await this.getMarkets()).map((mk) => [mk.symbol, mk.maxLeverage])),
702
856
  });
703
- // Unresolved (cross) positions: (3) the symbol's entry in GET /account/settings
704
- // `margin_settings` — "Current leverage set by the user"; (4) no entry = the symbol is at its
705
- // default, and the docs (get-account-settings) say "all markets have margin settings default
706
- // to cross margin and leverage default to max ... will return blank" → the market's max
707
- // leverage. Neither known → EXCHANGE_ERROR (never a fabricated value). Both reads happen ONLY
708
- // when something is unresolved; a failure of either propagates.
709
- if (mapped.some((m) => m.leverage <= 0)) {
710
- const settings = await this.client.getAccountSettings(this.account);
711
- if (settings === null || typeof settings !== "object" || !Array.isArray(settings.margin_settings)) {
712
- throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings returned no margin_settings list (got ${JSON.stringify(settings).slice(0, 160)})`, { exchange: "pacifica" });
713
- }
714
- const setLev = new Map();
715
- for (const s of settings.margin_settings) {
716
- if (typeof s?.symbol !== "string" || typeof s.leverage !== "number" || !(s.leverage > 0)) {
717
- throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings: malformed margin_settings entry ${JSON.stringify(s)}`, { exchange: "pacifica" });
718
- }
719
- setLev.set(s.symbol, s.leverage);
720
- }
721
- let maxLevMap = null;
722
- for (const m of mapped) {
723
- if (m.leverage > 0)
724
- continue;
725
- const configured = setLev.get(m.symbol);
726
- if (configured !== undefined) {
727
- m.leverage = configured;
728
- continue;
729
- }
730
- if (maxLevMap === null)
731
- maxLevMap = new Map((await this.getMarkets()).map((mk) => [mk.symbol, mk.maxLeverage]));
732
- const max = maxLevMap.get(m.symbol);
733
- if (typeof max !== "number" || !(max > 0)) {
734
- throw new PerpError("EXCHANGE_ERROR", `Pacifica ${m.symbol}: the position's leverage cannot be determined (no leverage on the position, no /account/settings entry, and no max leverage for the market)`, {
735
- exchange: "pacifica", remediation: "Check the market: perp -e pacifica market info " + m.symbol,
736
- });
737
- }
738
- m.leverage = max;
739
- }
740
- }
741
- return mapped;
742
857
  }
743
858
  /**
744
859
  * The leverage this account applies to `symbol`: its GET /account/settings `margin_settings`
@@ -766,7 +881,7 @@ export class PacificaAdapter {
766
881
  }
767
882
  async getOpenOrders() {
768
883
  if (!this._hasRealKey)
769
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
884
+ throw noAccountKeyError("pacifica");
770
885
  // GET /orders covers every market; spot orders (SOL-USDC) belong to the spot adapter.
771
886
  const [orders, info] = await Promise.all([this.client.getOrders(this.account), this.client.getInfo()]);
772
887
  const nonPerp = nonPerpInfoSymbols(info);
@@ -788,6 +903,7 @@ export class PacificaAdapter {
788
903
  }
789
904
  async marketOrder(symbol, side, size, opts) {
790
905
  this.ensureSigner();
906
+ symbol = await this.orderSymbol(symbol, `market ${side} ${size}`);
791
907
  // A caller's client order id is sent as client_order_id (validated UUID); none → none sent.
792
908
  const cloid = opts.clientOrderId === undefined ? null : requirePacificaClientOrderId(opts.clientOrderId, null);
793
909
  const created = await this.client.createMarketOrder({
@@ -929,11 +1045,13 @@ export class PacificaAdapter {
929
1045
  // client_order_id records the TIF for a later TIF-preserving amend. Either way the id sent
930
1046
  // rides on the result (`clientOrderId`).
931
1047
  const cloid = opts?.clientOrderId !== undefined ? requirePacificaClientOrderId(opts.clientOrderId, tif) : pacificaClientOrderId(tif);
1048
+ symbol = await this.orderSymbol(symbol, `limit ${tif} ${side} ${size}`);
932
1049
  const raw = await this.client.createLimitOrder({ symbol, price, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: opts?.reduceOnly ?? false, tif, client_order_id: cloid }, this.account, this.signMessage, await this.builderCode()).catch((e) => { throw pacificaCreateRejection(e, `limit ${tif} ${side} ${size} ${symbol} @ ${price}`); });
933
1050
  return withSentClientOrderId(await this.withOrderOutcome(raw, `limit ${tif} ${side} ${size} ${symbol} @ ${price}`, { terminalOnly: tif === "IOC", expect: { price, size } }), cloid);
934
1051
  }
935
1052
  async cancelOrder(symbol, orderId) {
936
1053
  this.ensureSigner();
1054
+ symbol = await this.listedSymbol(symbol);
937
1055
  return this.client.cancelOrder({ symbol, order_id: Number(orderId) }, this.account, this.signMessage);
938
1056
  }
939
1057
  /**
@@ -945,7 +1063,7 @@ export class PacificaAdapter {
945
1063
  */
946
1064
  async orderByClientId(clientOrderId) {
947
1065
  if (!this._hasRealKey)
948
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1066
+ throw noAccountKeyError("pacifica");
949
1067
  const wanted = requirePacificaClientOrderId(clientOrderId, pacificaTifOf(clientOrderId.toLowerCase())).toLowerCase();
950
1068
  const pick = (rows, open) => {
951
1069
  if (!Array.isArray(rows)) {
@@ -1064,6 +1182,7 @@ export class PacificaAdapter {
1064
1182
  */
1065
1183
  async editOrder(symbol, orderId, price, size) {
1066
1184
  this.ensureSigner();
1185
+ symbol = await this.orderSymbol(symbol, `edit ${orderId}`);
1067
1186
  return this.client.editOrder({ symbol, order_id: Number(orderId), price, amount: size }, this.account, this.signMessage);
1068
1187
  }
1069
1188
  /**
@@ -1092,6 +1211,8 @@ export class PacificaAdapter {
1092
1211
  if (String(original.symbol).toUpperCase() !== sym) {
1093
1212
  throw new PerpError("INVALID_PARAMS", `Order ${orderId} is on ${original.symbol}, not ${sym}`);
1094
1213
  }
1214
+ // The venue's own spelling (kPEPE), for the cancel and the replacement.
1215
+ const venueSym = String(original.symbol);
1095
1216
  if (original.order_type !== "limit") {
1096
1217
  throw unsupported("Pacifica", "amend", `only open limit orders can be amended (order is ${original.order_type})`, `Cancel it (perp trade cancel ${sym} ${orderId} -e pacifica) and place a new order`);
1097
1218
  }
@@ -1107,7 +1228,7 @@ export class PacificaAdapter {
1107
1228
  }
1108
1229
  const tifNote = tif !== null ? `TIF ${keep} (from --tif)` : `TIF ${keep} kept (from the order's client id)`;
1109
1230
  if (keep === "ALO") {
1110
- const raw = await this.editOrder(sym, orderId, price, size);
1231
+ const raw = await this.editOrder(venueSym, orderId, price, size);
1111
1232
  const err = pacificaBodyError(raw);
1112
1233
  if (err !== null)
1113
1234
  throw new PerpError("ORDER_REJECTED", `Pacifica edit of ${orderId} refused: ${err}`, { exchange: "pacifica" });
@@ -1152,7 +1273,7 @@ export class PacificaAdapter {
1152
1273
  const cancelWhat = `amend: cancel of ${orderId}`;
1153
1274
  let cancelRaw;
1154
1275
  try {
1155
- cancelRaw = await this.client.cancelOrder({ symbol: sym, order_id: Number(orderId) }, this.account, this.signMessage);
1276
+ cancelRaw = await this.client.cancelOrder({ symbol: venueSym, order_id: Number(orderId) }, this.account, this.signMessage);
1156
1277
  }
1157
1278
  catch (e) {
1158
1279
  if (pacificaRefused(e)) {
@@ -1213,7 +1334,7 @@ export class PacificaAdapter {
1213
1334
  const cloid = pacificaClientOrderId(keep);
1214
1335
  let placed;
1215
1336
  try {
1216
- placed = await this.client.createLimitOrder({ symbol: sym, price, amount: replaceSize, side: original.side, reduce_only: original.reduce_only === true, tif: keep, client_order_id: cloid }, this.account, this.signMessage, await this.builderCode());
1337
+ placed = await this.client.createLimitOrder({ symbol: venueSym, price, amount: replaceSize, side: original.side, reduce_only: original.reduce_only === true, tif: keep, client_order_id: cloid }, this.account, this.signMessage, await this.builderCode());
1217
1338
  }
1218
1339
  catch (e) {
1219
1340
  if (pacificaRefused(e)) {
@@ -1248,7 +1369,8 @@ export class PacificaAdapter {
1248
1369
  this.ensureSigner();
1249
1370
  if (!/^\d+$/.test(orderId))
1250
1371
  throw new PerpError("INVALID_PARAMS", `Pacifica stop order id must be numeric (got "${orderId}")`);
1251
- const raw = await this.client.cancelStopOrder({ symbol: symbol.toUpperCase(), order_id: Number(orderId) }, this.account, this.signMessage);
1372
+ const venueSym = await this.listedSymbol(symbol);
1373
+ const raw = await this.client.cancelStopOrder({ symbol: venueSym, order_id: Number(orderId) }, this.account, this.signMessage);
1252
1374
  const err = pacificaBodyError(raw);
1253
1375
  if (err !== null)
1254
1376
  throw new PerpError("ORDER_REJECTED", `Pacifica cancel of stop order ${orderId} refused: ${err}`, { exchange: "pacifica" });
@@ -1272,7 +1394,7 @@ export class PacificaAdapter {
1272
1394
  async setPositionTpSl(symbol, positionSide, tpsl) {
1273
1395
  this.ensureSigner();
1274
1396
  const params = {
1275
- symbol: symbol.toUpperCase(),
1397
+ symbol: await this.listedSymbol(symbol),
1276
1398
  side: positionSide === "long" ? "ask" : "bid",
1277
1399
  };
1278
1400
  if (tpsl.tp !== null)
@@ -1287,6 +1409,7 @@ export class PacificaAdapter {
1287
1409
  }
1288
1410
  async setLeverage(symbol, leverage, marginMode = "cross") {
1289
1411
  this.ensureSigner();
1412
+ symbol = await this.orderSymbol(symbol, `leverage ${leverage}x`);
1290
1413
  await this.client.updateLeverage({ symbol, leverage }, this.account, this.signMessage);
1291
1414
  if (marginMode === "isolated") {
1292
1415
  await this.client.updateMarginMode({ symbol, is_isolated: true }, this.account, this.signMessage);
@@ -1302,6 +1425,7 @@ export class PacificaAdapter {
1302
1425
  async stopOrder(symbol, side, size, triggerPrice, opts) {
1303
1426
  this.ensureSigner();
1304
1427
  const triggerPriceType = opts?.triggerType ? PacificaAdapter.TRIGGER_TYPE[opts.triggerType] : undefined;
1428
+ symbol = await this.orderSymbol(symbol, `stop ${side} ${size}`);
1305
1429
  const raw = await this.client.createStopOrder({
1306
1430
  symbol,
1307
1431
  side: side === "buy" ? "bid" : "ask",
@@ -1356,7 +1480,16 @@ export class PacificaAdapter {
1356
1480
  * of both parties mapped through `side === "bid"` → all "sell". No de-duplication: two taker
1357
1481
  * rows with equal time / price / amount are two fills (one order filling two makers).
1358
1482
  */
1359
- async getRecentTrades(symbol, _limit = 20) {
1483
+ async getRecentTrades(symbol, limit = 20) {
1484
+ // GET /trades has no limit / cursor: it answers the last 40 tape rows = 20 fills (taker +
1485
+ // maker row each; live 2026-10-19 on BTC, ETH, SOL, kPEPE, XRP). A larger limit is refused,
1486
+ // never silently answered with 20.
1487
+ if (!Number.isInteger(limit) || limit < 1 || limit > PACIFICA_TRADES_CAP) {
1488
+ throw new PerpError("INVALID_PARAMS", `Pacifica trades limit must be 1..${PACIFICA_TRADES_CAP} (got ${limit})`, {
1489
+ exchange: "pacifica", remediation: `Pacifica publishes only the last ${PACIFICA_TRADES_CAP} trades (GET /trades has no limit or cursor)`,
1490
+ });
1491
+ }
1492
+ symbol = await this.wireSymbol(symbol);
1360
1493
  const trades = await this.client.getTrades(symbol);
1361
1494
  if (!Array.isArray(trades)) {
1362
1495
  throw new PerpError("EXCHANGE_ERROR", `Pacifica /trades returned no trade list (${JSON.stringify(trades).slice(0, 120)})`, { exchange: "pacifica" });
@@ -1380,7 +1513,7 @@ export class PacificaAdapter {
1380
1513
  skippedWhy.push(`undocumented side "${side}"`);
1381
1514
  continue;
1382
1515
  }
1383
- if (rows.length >= _limit)
1516
+ if (rows.length >= limit)
1384
1517
  continue;
1385
1518
  rows.push({
1386
1519
  time: pacificaTime(t, "/trades"),
@@ -1397,24 +1530,82 @@ export class PacificaAdapter {
1397
1530
  return withTapeSkipped(await nonEmptyOrListed(this, symbol, rows), skipped);
1398
1531
  }
1399
1532
  async getFundingHistory(symbol, limit = 10) {
1400
- // The client unwraps the {success, data} envelope: the answer is the row list itself.
1401
- const data = await this.client.getFundingHistory(symbol, { limit });
1402
- if (!Array.isArray(data)) {
1403
- throw new PerpError("EXCHANGE_ERROR", `Pacifica /funding_rate/history returned no row list (${JSON.stringify(data).slice(0, 120)})`, { exchange: "pacifica" });
1533
+ // `limit` is at most 4000 per request (docs; live: limit 4001 / 5000 → 4000 rows, has_more
1534
+ // true): deeper history is paged with `cursor` / `next_cursor`, newest first.
1535
+ if (!Number.isInteger(limit) || limit < 1 || limit > PACIFICA_FUNDING_PAGE * PACIFICA_MAX_PAGES) {
1536
+ throw new PerpError("INVALID_PARAMS", `Pacifica funding history limit must be 1..${PACIFICA_FUNDING_PAGE * PACIFICA_MAX_PAGES} (got ${limit})`, { exchange: "pacifica" });
1537
+ }
1538
+ symbol = await this.wireSymbol(symbol);
1539
+ const data = [];
1540
+ let cursor = null;
1541
+ let hasMore = false;
1542
+ for (let page = 0; data.length < limit && page < PACIFICA_MAX_PAGES; page++) {
1543
+ const params = { symbol, limit: String(Math.min(PACIFICA_FUNDING_PAGE, limit - data.length)) };
1544
+ if (cursor !== null)
1545
+ params.cursor = cursor;
1546
+ const env = await this.client.getEnvelope("/funding_rate/history", params);
1547
+ if (env.success !== true || !Array.isArray(env.data)) {
1548
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /funding_rate/history returned no row list (${JSON.stringify(env).slice(0, 120)})`, { exchange: "pacifica" });
1549
+ }
1550
+ data.push(...env.data);
1551
+ hasMore = env.has_more === true;
1552
+ if (!hasMore || typeof env.next_cursor !== "string" || env.next_cursor === "" || env.data.length === 0)
1553
+ break;
1554
+ cursor = env.next_cursor;
1555
+ }
1556
+ // Short of `limit` while the venue says there is more (no cursor / empty page / page budget): never a silent short history.
1557
+ if (data.length < limit && hasMore) {
1558
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /funding_rate/history: ${data.length} of ${limit} rows read and the venue reports more (has_more) without a usable next page`, { exchange: "pacifica" });
1404
1559
  }
1405
1560
  // `200 []` for an unlisted symbol (live): checked against the market directory.
1406
- return nonEmptyOrListed(this, symbol, data.map((h) => {
1561
+ return nonEmptyOrListed(this, symbol, data.slice(0, limit).map((h) => {
1407
1562
  const oracle = h.oracle_price;
1408
1563
  return {
1409
1564
  time: pacificaTime(h, "/funding_rate/history"),
1410
1565
  rate: venueRateField(h.funding_rate, "funding history funding_rate", "pacifica"),
1411
1566
  // absent → null (unknown), never "0"
1412
1567
  price: (typeof oracle === "string" && oracle !== "") || (typeof oracle === "number" && Number.isFinite(oracle)) ? String(oracle) : null,
1568
+ intervalHours: 1, // hourly settlement (venue fundingRateBasis perHours 1)
1569
+ rateKind: "lastSettled",
1413
1570
  };
1414
1571
  }));
1415
1572
  }
1573
+ /**
1574
+ * GET /kline over [startTime, endTime]. The venue refuses a range over 4000 candles (HTTP 400
1575
+ * "Time range too large … Max range: 4000 candles"), so a longer window is read in consecutive
1576
+ * 4000-bar windows (at most PACIFICA_MAX_PAGES), merged by open time. Intervals outside
1577
+ * PACIFICA_KLINE_INTERVALS are refused locally with INVALID_PARAMS (the venue: HTTP 400).
1578
+ */
1416
1579
  async getKlines(symbol, interval, startTime, endTime) {
1417
- const klines = await this.client.getKline(symbol, interval, startTime, endTime);
1580
+ const ivMs = PACIFICA_KLINE_INTERVALS[interval];
1581
+ if (ivMs === undefined) {
1582
+ throw new PerpError("INVALID_PARAMS", `Pacifica kline interval ${JSON.stringify(interval)} is not served (supported: ${Object.keys(PACIFICA_KLINE_INTERVALS).join(", ")})`, {
1583
+ exchange: "pacifica", remediation: `Use one of: ${Object.keys(PACIFICA_KLINE_INTERVALS).join(", ")}`,
1584
+ });
1585
+ }
1586
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime > endTime) {
1587
+ throw new PerpError("INVALID_PARAMS", `Pacifica kline window must be start ≤ end (unix ms; got ${startTime}..${endTime})`, { exchange: "pacifica" });
1588
+ }
1589
+ const span = PACIFICA_KLINE_MAX_BARS * ivMs;
1590
+ const pages = Math.ceil((endTime - startTime + 1) / span);
1591
+ if (pages > PACIFICA_MAX_PAGES) {
1592
+ throw new PerpError("INVALID_PARAMS", `Pacifica kline window spans more than ${PACIFICA_KLINE_MAX_BARS * PACIFICA_MAX_PAGES} ${interval} bars (${PACIFICA_MAX_PAGES} requests of ${PACIFICA_KLINE_MAX_BARS})`, {
1593
+ exchange: "pacifica", remediation: "Narrow --start / --end or use a longer interval",
1594
+ });
1595
+ }
1596
+ symbol = await this.wireSymbol(symbol);
1597
+ const byTime = new Map();
1598
+ for (let from = startTime; from <= endTime; from += span) {
1599
+ const to = Math.min(endTime, from + span - 1);
1600
+ const raw = await this.client.getKline(symbol, interval, from, to);
1601
+ if (!Array.isArray(raw)) {
1602
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /kline returned no candle list (${JSON.stringify(raw).slice(0, 120)})`, { exchange: "pacifica" });
1603
+ }
1604
+ // A row of another interval → EXCHANGE_ERROR (assertCandleInterval), never returned.
1605
+ for (const k of raw)
1606
+ byTime.set(k.t, (assertCandleInterval("pacifica", interval, k.i), k));
1607
+ }
1608
+ const klines = [...byTime.values()].sort((a, b) => a.t - b.t);
1418
1609
  // `200 []` for an unlisted symbol (live): checked against the market directory.
1419
1610
  return nonEmptyOrListed(this, symbol, klines.map((k) => ({
1420
1611
  time: k.t,
@@ -1434,7 +1625,7 @@ export class PacificaAdapter {
1434
1625
  */
1435
1626
  async getOrderHistory(limit = 30) {
1436
1627
  if (!this._hasRealKey)
1437
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1628
+ throw noAccountKeyError("pacifica");
1438
1629
  const rows = await this.client.getOrderHistory(this.account, { limit });
1439
1630
  if (!Array.isArray(rows)) {
1440
1631
  throw new PerpError("EXCHANGE_ERROR", `Pacifica /orders/history returned no order list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "pacifica" });
@@ -1466,7 +1657,7 @@ export class PacificaAdapter {
1466
1657
  */
1467
1658
  async getTradeHistory(limit = 30) {
1468
1659
  if (!this._hasRealKey)
1469
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1660
+ throw noAccountKeyError("pacifica");
1470
1661
  const rows = await this.client.getTradeHistory(this.account, { limit });
1471
1662
  if (!Array.isArray(rows)) {
1472
1663
  throw new PerpError("EXCHANGE_ERROR", `Pacifica /trades/history returned no list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "pacifica" });
@@ -1486,7 +1677,7 @@ export class PacificaAdapter {
1486
1677
  /** Funding paid / received per position (GET /funding/history, `payout` signed). Strict like getTradeHistory. */
1487
1678
  async getFundingPayments(limit = 200) {
1488
1679
  if (!this._hasRealKey)
1489
- throw new Error("No private key configured — account data unavailable. Run: perp setup");
1680
+ throw noAccountKeyError("pacifica");
1490
1681
  const rows = await this.client.getFundingAccountHistory(this.account, { limit });
1491
1682
  if (!Array.isArray(rows)) {
1492
1683
  throw new PerpError("EXCHANGE_ERROR", `Pacifica /funding/history returned no list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "pacifica" });