perp-cli 0.14.3 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +138 -405
- package/README.zh.md +243 -0
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +85 -15
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +12 -5
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
- package/dist/bot/strategies/funding-arb-utils.js +53 -20
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +3 -3
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +126 -6
- package/dist/commands/account.js +168 -24
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +23 -81
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +175 -97
- package/dist/commands/arb.d.ts +20 -0
- package/dist/commands/arb.js +107 -163
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +395 -100
- package/dist/commands/options.d.ts +14 -0
- package/dist/commands/options.js +151 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +455 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +83 -55
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +67 -62
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +3 -3
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +35 -3
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +16 -12
- package/dist/exchanges/arcus/adapter.js +45 -18
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +30 -10
- package/dist/exchanges/aster.js +153 -53
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +8 -2
- package/dist/exchanges/canonical-symbols.js +46 -2
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +73 -0
- package/dist/exchanges/derive/options.js +379 -0
- package/dist/exchanges/extended/adapter.d.ts +96 -0
- package/dist/exchanges/extended/adapter.js +241 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +133 -0
- package/dist/exchanges/extended/public.js +436 -0
- package/dist/exchanges/grvt/adapter.d.ts +100 -0
- package/dist/exchanges/grvt/adapter.js +283 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
- package/dist/exchanges/hyperliquid-spot.js +24 -16
- package/dist/exchanges/hyperliquid.d.ts +199 -22
- package/dist/exchanges/hyperliquid.js +550 -202
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +157 -13
- package/dist/exchanges/interface.js +47 -0
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +67 -12
- package/dist/exchanges/lighter-public.js +250 -82
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +10 -5
- package/dist/exchanges/lighter-spot.js +105 -97
- package/dist/exchanges/lighter.d.ts +80 -21
- package/dist/exchanges/lighter.js +312 -105
- package/dist/exchanges/ondo/adapter.d.ts +38 -13
- package/dist/exchanges/ondo/adapter.js +86 -27
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +35 -3
- package/dist/exchanges/ondo/public.js +75 -32
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +8 -2
- package/dist/exchanges/pacifica-spot.js +26 -18
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +130 -0
- package/dist/exchanges/pacifica-vaults.js +277 -0
- package/dist/exchanges/pacifica.d.ts +108 -10
- package/dist/exchanges/pacifica.js +428 -171
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +69 -13
- package/dist/exchanges/qfex/adapter.js +102 -17
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +65 -12
- package/dist/exchanges/qfex/public.js +131 -25
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +160 -0
- package/dist/exchanges/risex/adapter.js +842 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +8 -8
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +61 -0
- package/dist/exchanges/txflow/adapter.js +150 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +155 -0
- package/dist/exchanges/txflow/public.d.ts +87 -0
- package/dist/exchanges/txflow/public.js +355 -0
- package/dist/exchanges/unknown-symbol.d.ts +34 -0
- package/dist/exchanges/unknown-symbol.js +56 -0
- package/dist/exchanges/variational/adapter.d.ts +67 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +52 -10
- package/dist/exchanges/venue-types.js +82 -11
- package/dist/exchanges/venue.d.ts +150 -11
- package/dist/exchanges/venue.js +126 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +19 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +20 -8
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +18 -6
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +15 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +131 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +6 -4
- package/dist/funding/legacy-public.js +42 -41
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +95 -6
- package/dist/funding/rates.js +99 -19
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +64 -0
- package/dist/mcp-server.js +211 -68
- package/dist/pacifica/client.d.ts +69 -5
- package/dist/pacifica/client.js +181 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +36 -63
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +22 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +980 -0
- package/dist/ws/index.js +4460 -0
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +996 -0
- package/dist/ws/node.js +4554 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +71 -10
- package/skills/perp-cli/SKILL.md +153 -36
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +22 -12
- package/skills/perp-cli/references/commands.md +143 -17
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +523 -0
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { nonEmptyOrListed, symbolNotFound } from "./unknown-symbol.js";
|
|
2
|
+
import { assertCandleInterval, withTapeSkipped } from "./interface.js";
|
|
1
3
|
import { PacificaClient } from "../pacifica/index.js";
|
|
4
|
+
import { isPacificaRateLimited } from "../pacifica/rate-limit.js";
|
|
2
5
|
import { LocalSolanaSigner } from "../signer/index.js";
|
|
3
6
|
import { isExpired } from "../agent-wallet/expiry.js";
|
|
7
|
+
import { isPacificaPerpetual } from "../ws/pacifica-instrument.js";
|
|
4
8
|
import { randomUUID } from "crypto";
|
|
5
|
-
import { PerpError, extractErrorMessage, isDefiniteOrderFailure, orderNotSent, orderOutcomeUnknown } from "../errors.js";
|
|
9
|
+
import { PerpError, extractErrorMessage, isDefiniteOrderFailure, orderNotSent, orderOutcomeUnknown, noAccountKeyError } from "../errors.js";
|
|
6
10
|
import { parseFiniteVenueNumber, venueRateField } from "../utils/numeric.js";
|
|
7
11
|
import { unsupported, validateLegacyTif } from "./trading-v2.js";
|
|
8
12
|
import { floorSize, requireOnTick, subDecimal } from "./precision.js";
|
|
@@ -10,7 +14,7 @@ import { requireCapabilities } from "./venue.js";
|
|
|
10
14
|
import { PACIFICA_UUID_RE, pacificaVenue } from "./venues/pacifica.js";
|
|
11
15
|
import { approvalFromCeiling, builderGateOf, rateToBps, requireFeeBps, NO_BUILDER } from "./builder-fee.js";
|
|
12
16
|
/**
|
|
13
|
-
* CLI TIF → Pacifica TIF
|
|
17
|
+
* CLI TIF → Pacifica TIF. GTC / IOC / ALO / TOB are native; venue
|
|
14
18
|
* aliases GTT → GTC and GTX → ALO; no TIF → GTC (uniform default). FOK (never
|
|
15
19
|
* emulated) and unknown values → uniform CAPABILITY_UNSUPPORTED.
|
|
16
20
|
*/
|
|
@@ -20,7 +24,7 @@ export function pacificaTif(tif) {
|
|
|
20
24
|
return validateLegacyTif(pacificaVenue, requireCapabilities(pacificaVenue), tif);
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
|
-
* Generated client_order_id carrying the order's TIF (
|
|
27
|
+
* Generated client_order_id carrying the order's TIF (like Ondo's
|
|
24
28
|
* `pcli-<p|n>-…`): Pacifica takes a full UUID and never returns the TIF (GET /orders has no
|
|
25
29
|
* tif field), so amend recovers it from the id. Layout: a UUID v4 whose first 6 hex digits are
|
|
26
30
|
* `5ac1f` + TIF code (1 GTC, 2 IOC, 3 ALO, 4 TOB). The native edit keeps the client_order_id.
|
|
@@ -274,7 +278,10 @@ export function pacificaWithdrawRejection(e, amount) {
|
|
|
274
278
|
}
|
|
275
279
|
return new PerpError("ACTION_REJECTED", `Pacifica withdraw ${amount} USDC refused (nothing was withdrawn): ${body.error}`, details);
|
|
276
280
|
}
|
|
281
|
+
/** A POST the venue refused with HTTP 4xx — incl. RATE_LIMITED (429 / pre-send throttle, nothing sent). */
|
|
277
282
|
export function pacificaRefused(e) {
|
|
283
|
+
if (isPacificaRateLimited(e))
|
|
284
|
+
return true;
|
|
278
285
|
return !(e instanceof PerpError) && /^POST \S+ failed \(4\d\d\)/.test(extractErrorMessage(e));
|
|
279
286
|
}
|
|
280
287
|
/**
|
|
@@ -311,6 +318,24 @@ function pacificaBodyError(r) {
|
|
|
311
318
|
return String(o.error ?? "request rejected");
|
|
312
319
|
return null;
|
|
313
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
|
+
};
|
|
314
339
|
/** /info rows must carry instrument_type — a row without it cannot be classified perp vs spot. */
|
|
315
340
|
function requireInstrumentType(rows) {
|
|
316
341
|
for (const m of rows) {
|
|
@@ -320,13 +345,13 @@ function requireInstrumentType(rows) {
|
|
|
320
345
|
}
|
|
321
346
|
return rows;
|
|
322
347
|
}
|
|
323
|
-
/** Perp markets of /info (spot rows such as SOL-USDC
|
|
348
|
+
/** Perp markets of /info: instrument_type "perpetual" only (spot rows such as SOL-USDC, or any other type, excluded). */
|
|
324
349
|
function perpInfoRows(rows) {
|
|
325
|
-
return requireInstrumentType(rows).filter(
|
|
350
|
+
return requireInstrumentType(rows).filter(isPacificaPerpetual);
|
|
326
351
|
}
|
|
327
|
-
/** Wire symbols of /info spot
|
|
328
|
-
function
|
|
329
|
-
return new Set(requireInstrumentType(rows).filter((m) => m
|
|
352
|
+
/** Wire symbols of /info markets that are not perps (spot, any other instrument_type). */
|
|
353
|
+
function nonPerpInfoSymbols(rows) {
|
|
354
|
+
return new Set(requireInstrumentType(rows).filter((m) => !isPacificaPerpetual(m)).map((m) => m.symbol));
|
|
330
355
|
}
|
|
331
356
|
/** Pacifica order_type of a stop / TP / SL order (stop_market, stop_limit, take_profit_*, stop_loss_*). */
|
|
332
357
|
export function isPacificaStopType(type) {
|
|
@@ -335,6 +360,18 @@ export function isPacificaStopType(type) {
|
|
|
335
360
|
/** Fill sides of GET /trades/history (position effect) plus the order sides bid / ask. */
|
|
336
361
|
const PACIFICA_FILL_BUY_SIDES = new Set(["open_long", "close_short", "bid"]);
|
|
337
362
|
const PACIFICA_FILL_SELL_SIDES = new Set(["open_short", "close_long", "ask"]);
|
|
363
|
+
/**
|
|
364
|
+
* Trade direction of a Pacifica fill `side` (position effect): open_long / close_short buy,
|
|
365
|
+
* open_short / close_long sell; bid / ask for rows that carry an order side. Anything else →
|
|
366
|
+
* EXCHANGE_ERROR (never a guessed direction). Used by the public tape and the account fills.
|
|
367
|
+
*/
|
|
368
|
+
export function pacificaFillSide(side, endpoint) {
|
|
369
|
+
if (PACIFICA_FILL_BUY_SIDES.has(side))
|
|
370
|
+
return "buy";
|
|
371
|
+
if (PACIFICA_FILL_SELL_SIDES.has(side))
|
|
372
|
+
return "sell";
|
|
373
|
+
throw new PerpError("EXCHANGE_ERROR", `Pacifica ${endpoint} row with unknown side "${side}"`, { exchange: "pacifica" });
|
|
374
|
+
}
|
|
338
375
|
/** `created_at` (ms) of a Pacifica history row; non-numeric → EXCHANGE_ERROR (never NaN). */
|
|
339
376
|
function pacificaTime(row, endpoint) {
|
|
340
377
|
const t = Number(pacificaField(row, "created_at", endpoint));
|
|
@@ -349,6 +386,124 @@ function pacificaField(row, key, endpoint) {
|
|
|
349
386
|
return String(v);
|
|
350
387
|
throw new PerpError("EXCHANGE_ERROR", `Pacifica ${endpoint} row without a readable ${key}: ${JSON.stringify(row).slice(0, 160)}`, { exchange: "pacifica" });
|
|
351
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
|
+
}
|
|
352
507
|
export class PacificaAdapter {
|
|
353
508
|
name = "pacifica";
|
|
354
509
|
chain = "solana";
|
|
@@ -372,6 +527,7 @@ export class PacificaAdapter {
|
|
|
372
527
|
signMessage;
|
|
373
528
|
_marketsCache = null;
|
|
374
529
|
_marketsCacheTime = 0;
|
|
530
|
+
_infoCache = null;
|
|
375
531
|
static CACHE_TTL = 30_000;
|
|
376
532
|
/** Venue network — part of every account cache key (the same key exists on mainnet and testnet). */
|
|
377
533
|
_network;
|
|
@@ -491,6 +647,15 @@ export class PacificaAdapter {
|
|
|
491
647
|
remediation: "Select the master wallet (--ows <wallet> or perp wallet use <wallet>), or do it on the Pacifica website",
|
|
492
648
|
});
|
|
493
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* The active signer for an account request whose body the caller builds itself (Print / vault
|
|
652
|
+
* writes): `account` (master address) and `agentWallet` (agent pubkey, or null when the master
|
|
653
|
+
* signs). Resolving it signs nothing. NO_SIGNER_AVAILABLE / AGENT_EXPIRED as `_resolveSigner`.
|
|
654
|
+
*/
|
|
655
|
+
signingIdentity() {
|
|
656
|
+
const r = this._resolveSigner();
|
|
657
|
+
return { tier: r.tier, account: this.account, agentWallet: r.tier === "agent" ? r.address : null, signMessage: (msg) => r.signer.signMessage(msg) };
|
|
658
|
+
}
|
|
494
659
|
/** Access the underlying Keypair (only available with LocalSolanaSigner). */
|
|
495
660
|
get keypair() {
|
|
496
661
|
if (this._solanaSigner instanceof LocalSolanaSigner) {
|
|
@@ -541,18 +706,35 @@ export class PacificaAdapter {
|
|
|
541
706
|
get sdk() {
|
|
542
707
|
return this.client;
|
|
543
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
|
+
}
|
|
544
719
|
get signer() {
|
|
545
720
|
return this.signMessage;
|
|
546
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
|
+
}
|
|
547
729
|
async getMarkets() {
|
|
548
730
|
if (this._marketsCache && Date.now() - this._marketsCacheTime < PacificaAdapter.CACHE_TTL)
|
|
549
731
|
return this._marketsCache;
|
|
550
732
|
const [markets, prices] = await Promise.all([
|
|
551
|
-
this.
|
|
733
|
+
this.infoRows(),
|
|
552
734
|
this._getPrices(),
|
|
553
735
|
]);
|
|
554
736
|
const priceMap = new Map(prices.map((p) => [p.symbol, p]));
|
|
555
|
-
// /info also lists spot markets (e.g. SOL-USDC); those are not perps
|
|
737
|
+
// /info also lists spot markets (e.g. SOL-USDC); those are not perps.
|
|
556
738
|
const result = perpInfoRows(markets).map((m) => {
|
|
557
739
|
const p = priceMap.get(m.symbol);
|
|
558
740
|
// Derive sizeDecimals from lot_size (e.g. "0.001" → 3, "1" → 0)
|
|
@@ -563,14 +745,19 @@ export class PacificaAdapter {
|
|
|
563
745
|
symbol: m.symbol,
|
|
564
746
|
markPrice: p?.mark ?? "-",
|
|
565
747
|
indexPrice: p?.oracle ?? null,
|
|
566
|
-
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,
|
|
567
754
|
volume24h: p?.volume_24h ?? null,
|
|
568
755
|
openInterest: p?.open_interest ?? null,
|
|
569
756
|
maxLeverage: m.max_leverage,
|
|
570
757
|
sizeDecimals,
|
|
571
758
|
stepSize: validLot ? m.lot_size : null,
|
|
572
|
-
//
|
|
573
|
-
fundingHours:
|
|
759
|
+
// Uniform hourly funding (venue fundingRateBasis perHours 1); no margin tiers published.
|
|
760
|
+
fundingHours: 1,
|
|
574
761
|
marginTiers: null,
|
|
575
762
|
};
|
|
576
763
|
});
|
|
@@ -578,8 +765,69 @@ export class PacificaAdapter {
|
|
|
578
765
|
this._marketsCacheTime = Date.now();
|
|
579
766
|
return result;
|
|
580
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
|
+
}
|
|
581
823
|
async getOrderbook(symbol) {
|
|
582
|
-
|
|
824
|
+
symbol = await this.wireSymbol(symbol);
|
|
825
|
+
const book = await this.client.getBook(symbol).catch((e) => {
|
|
826
|
+
// GET /book for an unlisted symbol: HTTP 400 {"error":"Book not found: NOPE"} (live 2026-10-14).
|
|
827
|
+
if (e instanceof Error && /^GET \/book failed \(400\):.*Book not found/.test(e.message))
|
|
828
|
+
throw symbolNotFound("pacifica", symbol);
|
|
829
|
+
throw e;
|
|
830
|
+
});
|
|
583
831
|
return {
|
|
584
832
|
bids: book.l[0].map((e) => [e.p, e.a]),
|
|
585
833
|
asks: book.l[1].map((e) => [e.p, e.a]),
|
|
@@ -587,125 +835,25 @@ export class PacificaAdapter {
|
|
|
587
835
|
}
|
|
588
836
|
async getBalance() {
|
|
589
837
|
if (!this._hasRealKey)
|
|
590
|
-
throw
|
|
838
|
+
throw noAccountKeyError("pacifica");
|
|
591
839
|
const [info, positions, prices] = await Promise.all([
|
|
592
840
|
this.client.getAccount(this.account),
|
|
593
841
|
this._getPositions(),
|
|
594
842
|
this._getPrices(),
|
|
595
843
|
]);
|
|
596
|
-
|
|
597
|
-
const priceMap = new Map(prices.map((p) => [p.symbol, p]));
|
|
598
|
-
// API often returns unrealized_pnl=0 — compute from price delta when needed
|
|
599
|
-
let totalPnl = 0;
|
|
600
|
-
for (const p of positions) {
|
|
601
|
-
let upnl = parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "pacifica");
|
|
602
|
-
const amount = parseFiniteVenueNumber(p.amount, "position.amount", "pacifica");
|
|
603
|
-
if (upnl === 0 && amount > 0) {
|
|
604
|
-
const mark = parseFiniteVenueNumber(priceMap.get(p.symbol)?.mark ?? p.mark_price, "position.mark", "pacifica");
|
|
605
|
-
const entry = parseFiniteVenueNumber(p.entry_price, "position.entry_price", "pacifica");
|
|
606
|
-
const dir = p.side === "bid" ? 1 : -1;
|
|
607
|
-
if (mark > 0 && entry > 0)
|
|
608
|
-
upnl = (mark - entry) * amount * dir;
|
|
609
|
-
}
|
|
610
|
-
totalPnl += upnl;
|
|
611
|
-
}
|
|
612
|
-
// Unified margin: a negative USDC `balance` is a loan; `pending_interest` is accrued,
|
|
613
|
-
// unpaid loan interest (docs get-account-info). Surfaced, not folded into equity.
|
|
614
|
-
const usdcBalance = info.balance;
|
|
615
|
-
if (typeof usdcBalance !== "string") {
|
|
616
|
-
throw new PerpError("EXCHANGE_ERROR", "Pacifica account info has no USDC `balance`", { exchange: "pacifica", venueRaw: raw });
|
|
617
|
-
}
|
|
618
|
-
const balance = {
|
|
619
|
-
equity: info.account_equity,
|
|
620
|
-
available: info.available_to_spend,
|
|
621
|
-
marginUsed: String(raw.total_margin_used ?? raw.margin_used ?? "0"),
|
|
622
|
-
unrealizedPnl: totalPnl.toFixed(4),
|
|
623
|
-
usdcBalance,
|
|
624
|
-
borrowedUsdc: usdcBalance.startsWith("-") ? usdcBalance.slice(1) : "0",
|
|
625
|
-
pendingInterest: typeof info.pending_interest === "string" ? info.pending_interest : null,
|
|
626
|
-
availableToWithdraw: info.available_to_withdraw,
|
|
627
|
-
};
|
|
628
|
-
return balance;
|
|
844
|
+
return pacificaBalanceOf(info, positions, prices);
|
|
629
845
|
}
|
|
630
846
|
async getPositions() {
|
|
631
847
|
if (!this._hasRealKey)
|
|
632
|
-
throw
|
|
848
|
+
throw noAccountKeyError("pacifica");
|
|
633
849
|
const [positions, prices] = await Promise.all([
|
|
634
850
|
this._getPositions(),
|
|
635
851
|
this._getPrices(),
|
|
636
852
|
]);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const side = p.side === "bid" ? "long" : "short";
|
|
641
|
-
const size = parseFiniteVenueNumber(p.amount, "position.amount", "pacifica");
|
|
642
|
-
const entry = parseFiniteVenueNumber(p.entry_price, "position.entry_price", "pacifica");
|
|
643
|
-
const markNum = parseFiniteVenueNumber(mark, "position.mark", "pacifica");
|
|
644
|
-
// API often returns unrealized_pnl=0 — compute from price delta when needed
|
|
645
|
-
let upnl = parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "pacifica");
|
|
646
|
-
if (upnl === 0 && size > 0 && entry > 0 && markNum > 0) {
|
|
647
|
-
const dir = side === "long" ? 1 : -1;
|
|
648
|
-
upnl = (markNum - entry) * size * dir;
|
|
649
|
-
}
|
|
650
|
-
// Leverage, in order: (1) the API's own `leverage` when present; (2) for an ISOLATED
|
|
651
|
-
// position, entry-notional / margin — the docs define `margin` as "Amount of margin
|
|
652
|
-
// allocated to an isolated position (only shown when isolated)", so a cross position
|
|
653
|
-
// reports "0" here; (3)/(4) resolved below from GET /account/settings, then the market max.
|
|
654
|
-
const marginNum = parseFiniteVenueNumber(p.margin ?? p.margin_used, "position.margin", "pacifica", { defaultValue: 0 });
|
|
655
|
-
const apiLev = typeof p.leverage === "number" && p.leverage > 0 ? p.leverage : 0;
|
|
656
|
-
const entryNotional = size * entry;
|
|
657
|
-
const derivedLev = marginNum > 0 && entryNotional > 0 ? Math.round(entryNotional / marginNum) : 0;
|
|
658
|
-
return {
|
|
659
|
-
symbol: p.symbol,
|
|
660
|
-
side: side,
|
|
661
|
-
size: String(p.amount),
|
|
662
|
-
entryPrice: String(p.entry_price),
|
|
663
|
-
markPrice: mark,
|
|
664
|
-
liquidationPrice: p.liquidation_price != null ? String(p.liquidation_price) : null,
|
|
665
|
-
unrealizedPnl: upnl.toFixed(4),
|
|
666
|
-
// 0 = unresolved; resolved from the account settings / market max below.
|
|
667
|
-
leverage: apiLev > 0 ? apiLev : derivedLev,
|
|
668
|
-
};
|
|
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])),
|
|
669
856
|
});
|
|
670
|
-
// Unresolved (cross) positions: (3) the symbol's entry in GET /account/settings
|
|
671
|
-
// `margin_settings` — "Current leverage set by the user"; (4) no entry = the symbol is at its
|
|
672
|
-
// default, and the docs (get-account-settings) say "all markets have margin settings default
|
|
673
|
-
// to cross margin and leverage default to max ... will return blank" → the market's max
|
|
674
|
-
// leverage. Neither known → EXCHANGE_ERROR (never a fabricated value). Both reads happen ONLY
|
|
675
|
-
// when something is unresolved; a failure of either propagates.
|
|
676
|
-
if (mapped.some((m) => m.leverage <= 0)) {
|
|
677
|
-
const settings = await this.client.getAccountSettings(this.account);
|
|
678
|
-
if (settings === null || typeof settings !== "object" || !Array.isArray(settings.margin_settings)) {
|
|
679
|
-
throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings returned no margin_settings list (got ${JSON.stringify(settings).slice(0, 160)})`, { exchange: "pacifica" });
|
|
680
|
-
}
|
|
681
|
-
const setLev = new Map();
|
|
682
|
-
for (const s of settings.margin_settings) {
|
|
683
|
-
if (typeof s?.symbol !== "string" || typeof s.leverage !== "number" || !(s.leverage > 0)) {
|
|
684
|
-
throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings: malformed margin_settings entry ${JSON.stringify(s)}`, { exchange: "pacifica" });
|
|
685
|
-
}
|
|
686
|
-
setLev.set(s.symbol, s.leverage);
|
|
687
|
-
}
|
|
688
|
-
let maxLevMap = null;
|
|
689
|
-
for (const m of mapped) {
|
|
690
|
-
if (m.leverage > 0)
|
|
691
|
-
continue;
|
|
692
|
-
const configured = setLev.get(m.symbol);
|
|
693
|
-
if (configured !== undefined) {
|
|
694
|
-
m.leverage = configured;
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
697
|
-
if (maxLevMap === null)
|
|
698
|
-
maxLevMap = new Map((await this.getMarkets()).map((mk) => [mk.symbol, mk.maxLeverage]));
|
|
699
|
-
const max = maxLevMap.get(m.symbol);
|
|
700
|
-
if (typeof max !== "number" || !(max > 0)) {
|
|
701
|
-
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)`, {
|
|
702
|
-
exchange: "pacifica", remediation: "Check the market: perp -e pacifica market info " + m.symbol,
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
m.leverage = max;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
return mapped;
|
|
709
857
|
}
|
|
710
858
|
/**
|
|
711
859
|
* The leverage this account applies to `symbol`: its GET /account/settings `margin_settings`
|
|
@@ -733,11 +881,11 @@ export class PacificaAdapter {
|
|
|
733
881
|
}
|
|
734
882
|
async getOpenOrders() {
|
|
735
883
|
if (!this._hasRealKey)
|
|
736
|
-
throw
|
|
884
|
+
throw noAccountKeyError("pacifica");
|
|
737
885
|
// GET /orders covers every market; spot orders (SOL-USDC) belong to the spot adapter.
|
|
738
886
|
const [orders, info] = await Promise.all([this.client.getOrders(this.account), this.client.getInfo()]);
|
|
739
|
-
const
|
|
740
|
-
return orders.filter((o) => !
|
|
887
|
+
const nonPerp = nonPerpInfoSymbols(info);
|
|
888
|
+
return orders.filter((o) => !nonPerp.has(String(o.symbol))).map((o) => {
|
|
741
889
|
const raw = o;
|
|
742
890
|
return {
|
|
743
891
|
orderId: String(raw.order_id),
|
|
@@ -755,6 +903,7 @@ export class PacificaAdapter {
|
|
|
755
903
|
}
|
|
756
904
|
async marketOrder(symbol, side, size, opts) {
|
|
757
905
|
this.ensureSigner();
|
|
906
|
+
symbol = await this.orderSymbol(symbol, `market ${side} ${size}`);
|
|
758
907
|
// A caller's client order id is sent as client_order_id (validated UUID); none → none sent.
|
|
759
908
|
const cloid = opts.clientOrderId === undefined ? null : requirePacificaClientOrderId(opts.clientOrderId, null);
|
|
760
909
|
const created = await this.client.createMarketOrder({
|
|
@@ -786,7 +935,7 @@ export class PacificaAdapter {
|
|
|
786
935
|
const filled = parseFloat(String(latest.filled_amount ?? "0"));
|
|
787
936
|
if (status === "filled" || filled > 0) {
|
|
788
937
|
// Terminal row (filled / cancelled remainder) with a reported filled_amount = the
|
|
789
|
-
// order's final fill
|
|
938
|
+
// order's final fill. A non-terminal partial row is not final.
|
|
790
939
|
const reported = latest.filled_amount;
|
|
791
940
|
const terminal = status === "filled" || status === "cancelled" || status === "rejected";
|
|
792
941
|
if (terminal && reported !== undefined && reported !== null && String(reported).trim() !== ""
|
|
@@ -830,7 +979,7 @@ export class PacificaAdapter {
|
|
|
830
979
|
return result;
|
|
831
980
|
}
|
|
832
981
|
/**
|
|
833
|
-
* Venue-final outcome of a raw marketOrder() result (IocOutcomeCapable
|
|
982
|
+
* Venue-final outcome of a raw marketOrder() result (IocOutcomeCapable),
|
|
834
983
|
* parsed from the result's content (so copies of it work): its `fillReport` — the terminal
|
|
835
984
|
* history_by_id row (filled / cancelled / rejected) with a reported filled_amount — for the
|
|
836
985
|
* same order id as the create response. null otherwise — reduce-only orders (not polled),
|
|
@@ -891,16 +1040,18 @@ export class PacificaAdapter {
|
|
|
891
1040
|
*/
|
|
892
1041
|
async limitOrder(symbol, side, price, size, opts) {
|
|
893
1042
|
this.ensureSigner();
|
|
894
|
-
const tif = pacificaTif(opts?.tif); //
|
|
1043
|
+
const tif = pacificaTif(opts?.tif); // common mapping; FOK → CAPABILITY_UNSUPPORTED
|
|
895
1044
|
// The caller's client order id (validated UUID) is sent as given; without one a generated
|
|
896
1045
|
// client_order_id records the TIF for a later TIF-preserving amend. Either way the id sent
|
|
897
1046
|
// rides on the result (`clientOrderId`).
|
|
898
1047
|
const cloid = opts?.clientOrderId !== undefined ? requirePacificaClientOrderId(opts.clientOrderId, tif) : pacificaClientOrderId(tif);
|
|
1048
|
+
symbol = await this.orderSymbol(symbol, `limit ${tif} ${side} ${size}`);
|
|
899
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}`); });
|
|
900
1050
|
return withSentClientOrderId(await this.withOrderOutcome(raw, `limit ${tif} ${side} ${size} ${symbol} @ ${price}`, { terminalOnly: tif === "IOC", expect: { price, size } }), cloid);
|
|
901
1051
|
}
|
|
902
1052
|
async cancelOrder(symbol, orderId) {
|
|
903
1053
|
this.ensureSigner();
|
|
1054
|
+
symbol = await this.listedSymbol(symbol);
|
|
904
1055
|
return this.client.cancelOrder({ symbol, order_id: Number(orderId) }, this.account, this.signMessage);
|
|
905
1056
|
}
|
|
906
1057
|
/**
|
|
@@ -912,7 +1063,7 @@ export class PacificaAdapter {
|
|
|
912
1063
|
*/
|
|
913
1064
|
async orderByClientId(clientOrderId) {
|
|
914
1065
|
if (!this._hasRealKey)
|
|
915
|
-
throw
|
|
1066
|
+
throw noAccountKeyError("pacifica");
|
|
916
1067
|
const wanted = requirePacificaClientOrderId(clientOrderId, pacificaTifOf(clientOrderId.toLowerCase())).toLowerCase();
|
|
917
1068
|
const pick = (rows, open) => {
|
|
918
1069
|
if (!Array.isArray(rows)) {
|
|
@@ -1031,10 +1182,11 @@ export class PacificaAdapter {
|
|
|
1031
1182
|
*/
|
|
1032
1183
|
async editOrder(symbol, orderId, price, size) {
|
|
1033
1184
|
this.ensureSigner();
|
|
1185
|
+
symbol = await this.orderSymbol(symbol, `edit ${orderId}`);
|
|
1034
1186
|
return this.client.editOrder({ symbol, order_id: Number(orderId), price, amount: size }, this.account, this.signMessage);
|
|
1035
1187
|
}
|
|
1036
1188
|
/**
|
|
1037
|
-
* Amend that keeps the order's TIF
|
|
1189
|
+
* Amend that keeps the order's TIF. `size` = new open size.
|
|
1038
1190
|
* TIF = `tif` when the caller gives one, else the TIF encoded in the order's generated
|
|
1039
1191
|
* client_order_id; unknown → uniform CAPABILITY_UNSUPPORTED (never guessed).
|
|
1040
1192
|
* - ALO: native edit (the venue re-creates the order as ALO, keeping its client_order_id).
|
|
@@ -1059,6 +1211,8 @@ export class PacificaAdapter {
|
|
|
1059
1211
|
if (String(original.symbol).toUpperCase() !== sym) {
|
|
1060
1212
|
throw new PerpError("INVALID_PARAMS", `Order ${orderId} is on ${original.symbol}, not ${sym}`);
|
|
1061
1213
|
}
|
|
1214
|
+
// The venue's own spelling (kPEPE), for the cancel and the replacement.
|
|
1215
|
+
const venueSym = String(original.symbol);
|
|
1062
1216
|
if (original.order_type !== "limit") {
|
|
1063
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`);
|
|
1064
1218
|
}
|
|
@@ -1074,7 +1228,7 @@ export class PacificaAdapter {
|
|
|
1074
1228
|
}
|
|
1075
1229
|
const tifNote = tif !== null ? `TIF ${keep} (from --tif)` : `TIF ${keep} kept (from the order's client id)`;
|
|
1076
1230
|
if (keep === "ALO") {
|
|
1077
|
-
const raw = await this.editOrder(
|
|
1231
|
+
const raw = await this.editOrder(venueSym, orderId, price, size);
|
|
1078
1232
|
const err = pacificaBodyError(raw);
|
|
1079
1233
|
if (err !== null)
|
|
1080
1234
|
throw new PerpError("ORDER_REJECTED", `Pacifica edit of ${orderId} refused: ${err}`, { exchange: "pacifica" });
|
|
@@ -1119,7 +1273,7 @@ export class PacificaAdapter {
|
|
|
1119
1273
|
const cancelWhat = `amend: cancel of ${orderId}`;
|
|
1120
1274
|
let cancelRaw;
|
|
1121
1275
|
try {
|
|
1122
|
-
cancelRaw = await this.client.cancelOrder({ symbol:
|
|
1276
|
+
cancelRaw = await this.client.cancelOrder({ symbol: venueSym, order_id: Number(orderId) }, this.account, this.signMessage);
|
|
1123
1277
|
}
|
|
1124
1278
|
catch (e) {
|
|
1125
1279
|
if (pacificaRefused(e)) {
|
|
@@ -1180,7 +1334,7 @@ export class PacificaAdapter {
|
|
|
1180
1334
|
const cloid = pacificaClientOrderId(keep);
|
|
1181
1335
|
let placed;
|
|
1182
1336
|
try {
|
|
1183
|
-
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());
|
|
1184
1338
|
}
|
|
1185
1339
|
catch (e) {
|
|
1186
1340
|
if (pacificaRefused(e)) {
|
|
@@ -1215,7 +1369,8 @@ export class PacificaAdapter {
|
|
|
1215
1369
|
this.ensureSigner();
|
|
1216
1370
|
if (!/^\d+$/.test(orderId))
|
|
1217
1371
|
throw new PerpError("INVALID_PARAMS", `Pacifica stop order id must be numeric (got "${orderId}")`);
|
|
1218
|
-
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);
|
|
1219
1374
|
const err = pacificaBodyError(raw);
|
|
1220
1375
|
if (err !== null)
|
|
1221
1376
|
throw new PerpError("ORDER_REJECTED", `Pacifica cancel of stop order ${orderId} refused: ${err}`, { exchange: "pacifica" });
|
|
@@ -1239,7 +1394,7 @@ export class PacificaAdapter {
|
|
|
1239
1394
|
async setPositionTpSl(symbol, positionSide, tpsl) {
|
|
1240
1395
|
this.ensureSigner();
|
|
1241
1396
|
const params = {
|
|
1242
|
-
symbol:
|
|
1397
|
+
symbol: await this.listedSymbol(symbol),
|
|
1243
1398
|
side: positionSide === "long" ? "ask" : "bid",
|
|
1244
1399
|
};
|
|
1245
1400
|
if (tpsl.tp !== null)
|
|
@@ -1254,6 +1409,7 @@ export class PacificaAdapter {
|
|
|
1254
1409
|
}
|
|
1255
1410
|
async setLeverage(symbol, leverage, marginMode = "cross") {
|
|
1256
1411
|
this.ensureSigner();
|
|
1412
|
+
symbol = await this.orderSymbol(symbol, `leverage ${leverage}x`);
|
|
1257
1413
|
await this.client.updateLeverage({ symbol, leverage }, this.account, this.signMessage);
|
|
1258
1414
|
if (marginMode === "isolated") {
|
|
1259
1415
|
await this.client.updateMarginMode({ symbol, is_isolated: true }, this.account, this.signMessage);
|
|
@@ -1269,6 +1425,7 @@ export class PacificaAdapter {
|
|
|
1269
1425
|
async stopOrder(symbol, side, size, triggerPrice, opts) {
|
|
1270
1426
|
this.ensureSigner();
|
|
1271
1427
|
const triggerPriceType = opts?.triggerType ? PacificaAdapter.TRIGGER_TYPE[opts.triggerType] : undefined;
|
|
1428
|
+
symbol = await this.orderSymbol(symbol, `stop ${side} ${size}`);
|
|
1272
1429
|
const raw = await this.client.createStopOrder({
|
|
1273
1430
|
symbol,
|
|
1274
1431
|
side: side === "buy" ? "bid" : "ask",
|
|
@@ -1314,48 +1471,152 @@ export class PacificaAdapter {
|
|
|
1314
1471
|
return this.client.withdraw({ amount }, this.account, (msg) => master.signMessage(msg))
|
|
1315
1472
|
.catch((e) => { throw pacificaWithdrawRejection(e, amount); });
|
|
1316
1473
|
}
|
|
1317
|
-
|
|
1474
|
+
/**
|
|
1475
|
+
* Public tape (GET /trades). Every fill is listed TWICE — a `fulfill_taker` row and a
|
|
1476
|
+
* `fulfill_maker` row, each with that party's position effect as `side` (live 2026-10-14:
|
|
1477
|
+
* taker `open_short` paired with maker `close_short` at the same time / price / amount). The
|
|
1478
|
+
* tape is the taker rows, with the taker's direction: open_long / close_short buy, open_short /
|
|
1479
|
+
* close_long sell (the same mapping as the account fills, PACIFICA_FILL_*_SIDES). Was every row
|
|
1480
|
+
* of both parties mapped through `side === "bid"` → all "sell". No de-duplication: two taker
|
|
1481
|
+
* rows with equal time / price / amount are two fills (one order filling two makers).
|
|
1482
|
+
*/
|
|
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);
|
|
1318
1493
|
const trades = await this.client.getTrades(symbol);
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1494
|
+
if (!Array.isArray(trades)) {
|
|
1495
|
+
throw new PerpError("EXCHANGE_ERROR", `Pacifica /trades returned no trade list (${JSON.stringify(trades).slice(0, 120)})`, { exchange: "pacifica" });
|
|
1496
|
+
}
|
|
1497
|
+
// The docs list event_type fulfill_taker / fulfill_maker and side open_long / open_short /
|
|
1498
|
+
// close_long / close_short without declaring the sets closed: a row with any other value is
|
|
1499
|
+
// skipped (stderr warning + meta.skippedRows), never given a guessed direction, and does not
|
|
1500
|
+
// fail the rest of the tape.
|
|
1501
|
+
const skippedWhy = [];
|
|
1502
|
+
const rows = [];
|
|
1503
|
+
for (const t of trades) {
|
|
1504
|
+
const ev = pacificaField(t, "event_type", "/trades");
|
|
1505
|
+
if (ev === "fulfill_maker")
|
|
1506
|
+
continue;
|
|
1507
|
+
if (ev !== "fulfill_taker") {
|
|
1508
|
+
skippedWhy.push(`undocumented event_type "${ev}"`);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const side = pacificaField(t, "side", "/trades");
|
|
1512
|
+
if (!PACIFICA_FILL_BUY_SIDES.has(side) && !PACIFICA_FILL_SELL_SIDES.has(side)) {
|
|
1513
|
+
skippedWhy.push(`undocumented side "${side}"`);
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
if (rows.length >= limit)
|
|
1517
|
+
continue;
|
|
1518
|
+
rows.push({
|
|
1519
|
+
time: pacificaTime(t, "/trades"),
|
|
1520
|
+
symbol,
|
|
1521
|
+
side: pacificaFillSide(side, "/trades"),
|
|
1522
|
+
price: pacificaField(t, "price", "/trades"),
|
|
1523
|
+
size: pacificaField(t, "amount", "/trades"),
|
|
1524
|
+
fee: null, // the public tape carries no fee
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
const skipped = skippedWhy.length === 0 ? null : { count: skippedWhy.length, reason: [...new Set(skippedWhy)].join(", ") };
|
|
1528
|
+
if (skipped !== null)
|
|
1529
|
+
process.stderr.write(`[pacifica] /trades: skipped ${skipped.count} row(s) — ${skipped.reason}\n`);
|
|
1530
|
+
return withTapeSkipped(await nonEmptyOrListed(this, symbol, rows), skipped);
|
|
1336
1531
|
}
|
|
1337
1532
|
async getFundingHistory(symbol, limit = 10) {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
if (!
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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" });
|
|
1559
|
+
}
|
|
1560
|
+
// `200 []` for an unlisted symbol (live): checked against the market directory.
|
|
1561
|
+
return nonEmptyOrListed(this, symbol, data.slice(0, limit).map((h) => {
|
|
1562
|
+
const oracle = h.oracle_price;
|
|
1563
|
+
return {
|
|
1564
|
+
time: pacificaTime(h, "/funding_rate/history"),
|
|
1565
|
+
rate: venueRateField(h.funding_rate, "funding history funding_rate", "pacifica"),
|
|
1566
|
+
// absent → null (unknown), never "0"
|
|
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",
|
|
1570
|
+
};
|
|
1346
1571
|
}));
|
|
1347
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
|
+
*/
|
|
1348
1579
|
async getKlines(symbol, interval, startTime, endTime) {
|
|
1349
|
-
const
|
|
1350
|
-
|
|
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);
|
|
1609
|
+
// `200 []` for an unlisted symbol (live): checked against the market directory.
|
|
1610
|
+
return nonEmptyOrListed(this, symbol, klines.map((k) => ({
|
|
1351
1611
|
time: k.t,
|
|
1352
1612
|
open: k.o,
|
|
1353
1613
|
high: k.h,
|
|
1354
1614
|
low: k.l,
|
|
1355
1615
|
close: k.c,
|
|
1356
1616
|
volume: k.v,
|
|
1617
|
+
quoteVolume: null, // /kline carries base volume only
|
|
1357
1618
|
trades: k.n,
|
|
1358
|
-
}));
|
|
1619
|
+
})));
|
|
1359
1620
|
}
|
|
1360
1621
|
/**
|
|
1361
1622
|
* Recent orders, newest first (GET /orders/history; PacificaClient.get already unwraps the
|
|
@@ -1364,7 +1625,7 @@ export class PacificaAdapter {
|
|
|
1364
1625
|
*/
|
|
1365
1626
|
async getOrderHistory(limit = 30) {
|
|
1366
1627
|
if (!this._hasRealKey)
|
|
1367
|
-
throw
|
|
1628
|
+
throw noAccountKeyError("pacifica");
|
|
1368
1629
|
const rows = await this.client.getOrderHistory(this.account, { limit });
|
|
1369
1630
|
if (!Array.isArray(rows)) {
|
|
1370
1631
|
throw new PerpError("EXCHANGE_ERROR", `Pacifica /orders/history returned no order list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "pacifica" });
|
|
@@ -1396,21 +1657,17 @@ export class PacificaAdapter {
|
|
|
1396
1657
|
*/
|
|
1397
1658
|
async getTradeHistory(limit = 30) {
|
|
1398
1659
|
if (!this._hasRealKey)
|
|
1399
|
-
throw
|
|
1660
|
+
throw noAccountKeyError("pacifica");
|
|
1400
1661
|
const rows = await this.client.getTradeHistory(this.account, { limit });
|
|
1401
1662
|
if (!Array.isArray(rows)) {
|
|
1402
1663
|
throw new PerpError("EXCHANGE_ERROR", `Pacifica /trades/history returned no list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "pacifica" });
|
|
1403
1664
|
}
|
|
1404
1665
|
return rows.slice(0, limit).map((t) => {
|
|
1405
|
-
const side = pacificaField(t, "side", "/trades/history");
|
|
1406
|
-
const buy = PACIFICA_FILL_BUY_SIDES.has(side);
|
|
1407
|
-
if (!buy && !PACIFICA_FILL_SELL_SIDES.has(side)) {
|
|
1408
|
-
throw new PerpError("EXCHANGE_ERROR", `Pacifica /trades/history row with unknown side "${side}"`, { exchange: "pacifica" });
|
|
1409
|
-
}
|
|
1666
|
+
const side = pacificaFillSide(pacificaField(t, "side", "/trades/history"), "/trades/history");
|
|
1410
1667
|
return {
|
|
1411
1668
|
time: pacificaTime(t, "/trades/history"),
|
|
1412
1669
|
symbol: pacificaField(t, "symbol", "/trades/history"),
|
|
1413
|
-
side
|
|
1670
|
+
side,
|
|
1414
1671
|
price: pacificaField(t, "price", "/trades/history"),
|
|
1415
1672
|
size: pacificaField(t, "amount", "/trades/history"),
|
|
1416
1673
|
fee: pacificaField(t, "fee", "/trades/history"),
|
|
@@ -1420,7 +1677,7 @@ export class PacificaAdapter {
|
|
|
1420
1677
|
/** Funding paid / received per position (GET /funding/history, `payout` signed). Strict like getTradeHistory. */
|
|
1421
1678
|
async getFundingPayments(limit = 200) {
|
|
1422
1679
|
if (!this._hasRealKey)
|
|
1423
|
-
throw
|
|
1680
|
+
throw noAccountKeyError("pacifica");
|
|
1424
1681
|
const rows = await this.client.getFundingAccountHistory(this.account, { limit });
|
|
1425
1682
|
if (!Array.isArray(rows)) {
|
|
1426
1683
|
throw new PerpError("EXCHANGE_ERROR", `Pacifica /funding/history returned no list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "pacifica" });
|