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.
- package/README.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +129 -535
- package/README.zh.md +243 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +2 -2
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +10 -3
- package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
- package/dist/bot/strategies/funding-arb-utils.js +48 -15
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
- package/dist/bot/strategies/spot-perp-executor.js +2 -2
- package/dist/cli-program.js +1 -1
- package/dist/commands/account.js +168 -24
- package/dist/commands/arb/index.js +19 -77
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +155 -81
- package/dist/commands/arb.d.ts +8 -0
- package/dist/commands/arb.js +49 -23
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +241 -78
- package/dist/commands/options.d.ts +5 -0
- package/dist/commands/options.js +20 -3
- package/dist/commands/pacifica-earn.js +8 -21
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +51 -8
- package/dist/commands/trade.js +4 -4
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +2 -2
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +10 -0
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +10 -6
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/aster.d.ts +27 -7
- package/dist/exchanges/aster.js +124 -42
- package/dist/exchanges/canonical-symbols.d.ts +7 -0
- package/dist/exchanges/canonical-symbols.js +20 -0
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/options.d.ts +5 -0
- package/dist/exchanges/derive/options.js +20 -1
- package/dist/exchanges/extended/adapter.d.ts +16 -2
- package/dist/exchanges/extended/adapter.js +96 -34
- package/dist/exchanges/extended/public.d.ts +3 -0
- package/dist/exchanges/extended/public.js +9 -2
- package/dist/exchanges/grvt/adapter.d.ts +16 -2
- package/dist/exchanges/grvt/adapter.js +98 -21
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
- package/dist/exchanges/hyperliquid-spot.js +20 -12
- package/dist/exchanges/hyperliquid.d.ts +180 -13
- package/dist/exchanges/hyperliquid.js +487 -185
- package/dist/exchanges/interface.d.ts +64 -8
- package/dist/exchanges/interface.js +31 -0
- package/dist/exchanges/lighter-public.d.ts +66 -11
- package/dist/exchanges/lighter-public.js +249 -81
- package/dist/exchanges/lighter-spot.d.ts +7 -1
- package/dist/exchanges/lighter-spot.js +99 -90
- package/dist/exchanges/lighter.d.ts +70 -11
- package/dist/exchanges/lighter.js +285 -92
- package/dist/exchanges/ondo/adapter.d.ts +34 -8
- package/dist/exchanges/ondo/adapter.js +73 -16
- package/dist/exchanges/ondo/public.d.ts +27 -2
- package/dist/exchanges/ondo/public.js +63 -31
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-spot.d.ts +6 -0
- package/dist/exchanges/pacifica-spot.js +23 -15
- package/dist/exchanges/pacifica-vaults.d.ts +9 -0
- package/dist/exchanges/pacifica-vaults.js +14 -1
- package/dist/exchanges/pacifica.d.ts +77 -6
- package/dist/exchanges/pacifica.js +321 -130
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +52 -9
- package/dist/exchanges/qfex/adapter.js +74 -9
- package/dist/exchanges/qfex/public.d.ts +62 -9
- package/dist/exchanges/qfex/public.js +128 -22
- package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
- package/dist/exchanges/risex/adapter.d.ts +19 -7
- package/dist/exchanges/risex/adapter.js +58 -12
- package/dist/exchanges/spot-factory.js +1 -1
- package/dist/exchanges/txflow/adapter.d.ts +14 -7
- package/dist/exchanges/txflow/adapter.js +51 -19
- package/dist/exchanges/txflow/client.js +3 -1
- package/dist/exchanges/txflow/public.d.ts +12 -1
- package/dist/exchanges/txflow/public.js +82 -11
- package/dist/exchanges/unknown-symbol.d.ts +6 -1
- package/dist/exchanges/unknown-symbol.js +7 -2
- package/dist/exchanges/variational/adapter.d.ts +2 -6
- package/dist/exchanges/venue-types.d.ts +46 -3
- package/dist/exchanges/venue-types.js +80 -9
- package/dist/exchanges/venue.d.ts +99 -3
- package/dist/exchanges/venue.js +72 -0
- package/dist/exchanges/venues/hyperliquid.js +9 -0
- package/dist/exchanges/venues/lighter-rh.js +13 -3
- package/dist/exchanges/venues/lighter.js +13 -3
- package/dist/exchanges/venues/pacifica.js +9 -0
- package/dist/exchanges/venues/risex.js +15 -0
- package/dist/funding/legacy-public.d.ts +5 -3
- package/dist/funding/legacy-public.js +33 -37
- package/dist/funding/rates.d.ts +54 -5
- package/dist/funding/rates.js +49 -13
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.js +11 -5
- package/dist/mcp-server.js +104 -15
- package/dist/pacifica/client.d.ts +2 -1
- package/dist/pacifica/client.js +11 -0
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/strategies/funding-arb.js +35 -62
- package/dist/utils.d.ts +11 -1
- package/dist/venue-health.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +30 -9
- package/dist/ws/index.js +180 -15
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +30 -9
- package/dist/ws/node.js +204 -16
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/package.json +15 -2
- package/skills/perp-cli/SKILL.md +97 -33
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +15 -12
- package/skills/perp-cli/references/commands.md +67 -12
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +55 -24
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
|
@@ -1,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.
|
|
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
|
|
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
|
-
//
|
|
601
|
-
fundingHours:
|
|
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
|
|
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
|
-
|
|
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
|
|
848
|
+
throw noAccountKeyError("pacifica");
|
|
666
849
|
const [positions, prices] = await Promise.all([
|
|
667
850
|
this._getPositions(),
|
|
668
851
|
this._getPrices(),
|
|
669
852
|
]);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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,
|
|
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 >=
|
|
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
|
-
//
|
|
1401
|
-
|
|
1402
|
-
if (!
|
|
1403
|
-
throw new PerpError("
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" });
|