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.
Files changed (363) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +138 -405
  4. package/README.zh.md +243 -0
  5. package/dist/agent-wallet/store.d.ts +3 -2
  6. package/dist/agent-wallet/store.js +2 -2
  7. package/dist/api/public/hyperliquid.js +4 -0
  8. package/dist/api/public/lighter.d.ts +5 -0
  9. package/dist/api/public/lighter.js +5 -0
  10. package/dist/api/public/pacifica.js +85 -15
  11. package/dist/api/public/urls.d.ts +2 -0
  12. package/dist/api/public/urls.js +3 -0
  13. package/dist/arb/sizing.js +4 -2
  14. package/dist/arb/state.d.ts +6 -0
  15. package/dist/arb/utils.d.ts +1 -2
  16. package/dist/arb/utils.js +1 -2
  17. package/dist/balance-by-address.d.ts +39 -0
  18. package/dist/balance-by-address.js +44 -0
  19. package/dist/bot/apex/pulse.js +3 -0
  20. package/dist/bot/apex/radar.js +2 -1
  21. package/dist/bot/engine.d.ts +10 -1
  22. package/dist/bot/engine.js +14 -6
  23. package/dist/bot/indicators.d.ts +7 -0
  24. package/dist/bot/indicators.js +16 -0
  25. package/dist/bot/perp-order.d.ts +4 -4
  26. package/dist/bot/perp-order.js +5 -5
  27. package/dist/bot/quarantine.d.ts +1 -1
  28. package/dist/bot/quarantine.js +1 -1
  29. package/dist/bot/quote-rules.js +1 -1
  30. package/dist/bot/strategies/apex-strategy.js +3 -2
  31. package/dist/bot/strategies/engine-mm.js +6 -5
  32. package/dist/bot/strategies/funding-arb-strategy.js +12 -5
  33. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
  34. package/dist/bot/strategies/funding-arb-utils.js +53 -20
  35. package/dist/bot/strategies/funding-auto.js +2 -2
  36. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  37. package/dist/bot/strategies/liquidation-mm.js +8 -2
  38. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  39. package/dist/bot/strategies/momentum-breakout.js +11 -3
  40. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  41. package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
  42. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  43. package/dist/bot/strategies/spot-perp-executor.js +3 -3
  44. package/dist/bot/strategy-types.d.ts +2 -1
  45. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  46. package/dist/bot/tui/BotDashboard.js +1 -1
  47. package/dist/cache.js +12 -6
  48. package/dist/cli-program.d.ts +15 -0
  49. package/dist/cli-program.js +126 -6
  50. package/dist/commands/account.js +168 -24
  51. package/dist/commands/agent-import.d.ts +7 -0
  52. package/dist/commands/agent-import.js +30 -1
  53. package/dist/commands/agent.d.ts +6 -3
  54. package/dist/commands/agent.js +160 -12
  55. package/dist/commands/arb/index.js +23 -81
  56. package/dist/commands/arb-auto.d.ts +24 -0
  57. package/dist/commands/arb-auto.js +175 -97
  58. package/dist/commands/arb.d.ts +20 -0
  59. package/dist/commands/arb.js +107 -163
  60. package/dist/commands/backtest.js +23 -21
  61. package/dist/commands/bot.js +1 -1
  62. package/dist/commands/builder.js +1 -1
  63. package/dist/commands/funds.js +113 -3
  64. package/dist/commands/init.d.ts +1 -1
  65. package/dist/commands/init.js +27 -7
  66. package/dist/commands/manage.js +23 -21
  67. package/dist/commands/market.d.ts +48 -0
  68. package/dist/commands/market.js +395 -100
  69. package/dist/commands/options.d.ts +14 -0
  70. package/dist/commands/options.js +151 -0
  71. package/dist/commands/pacifica-earn.d.ts +5 -0
  72. package/dist/commands/pacifica-earn.js +455 -0
  73. package/dist/commands/pacifica-orders.d.ts +14 -0
  74. package/dist/commands/pacifica-orders.js +142 -0
  75. package/dist/commands/portfolio.d.ts +16 -0
  76. package/dist/commands/portfolio.js +83 -55
  77. package/dist/commands/rebalance.d.ts +1 -0
  78. package/dist/commands/rebalance.js +15 -0
  79. package/dist/commands/risk.d.ts +11 -2
  80. package/dist/commands/risk.js +39 -44
  81. package/dist/commands/trade.d.ts +1 -0
  82. package/dist/commands/trade.js +67 -62
  83. package/dist/commands/wallet.js +1 -1
  84. package/dist/config.d.ts +2 -2
  85. package/dist/config.js +15 -4
  86. package/dist/constants.js +6 -0
  87. package/dist/cross-chain-margin.d.ts +3 -1
  88. package/dist/cross-chain-margin.js +4 -2
  89. package/dist/dashboard/server.js +3 -3
  90. package/dist/dashboard/ui.js +9 -6
  91. package/dist/errors.d.ts +29 -1
  92. package/dist/errors.js +35 -3
  93. package/dist/exchanges/address.d.ts +8 -0
  94. package/dist/exchanges/address.js +47 -0
  95. package/dist/exchanges/arcus/adapter.d.ts +16 -12
  96. package/dist/exchanges/arcus/adapter.js +45 -18
  97. package/dist/exchanges/arcus/agent.d.ts +1 -1
  98. package/dist/exchanges/arcus/agent.js +1 -1
  99. package/dist/exchanges/arcus/client.js +1 -1
  100. package/dist/exchanges/arcus/public.js +3 -2
  101. package/dist/exchanges/arcus/signing.d.ts +2 -3
  102. package/dist/exchanges/arcus/signing.js +1 -1
  103. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  104. package/dist/exchanges/arcus/spot-chain.js +1 -1
  105. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  106. package/dist/exchanges/arcus/spot-router.js +2 -2
  107. package/dist/exchanges/arcus/spot.d.ts +2 -3
  108. package/dist/exchanges/arcus/spot.js +2 -3
  109. package/dist/exchanges/aster-spot.d.ts +3 -3
  110. package/dist/exchanges/aster-spot.js +6 -9
  111. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  112. package/dist/exchanges/aster-typed-data.js +1 -1
  113. package/dist/exchanges/aster.d.ts +30 -10
  114. package/dist/exchanges/aster.js +153 -53
  115. package/dist/exchanges/builder-fee.d.ts +1 -1
  116. package/dist/exchanges/builder-fee.js +13 -2
  117. package/dist/exchanges/canonical-symbols.d.ts +8 -2
  118. package/dist/exchanges/canonical-symbols.js +46 -2
  119. package/dist/exchanges/capabilities.d.ts +2 -0
  120. package/dist/exchanges/capabilities.js +2 -1
  121. package/dist/exchanges/derive/client.d.ts +37 -0
  122. package/dist/exchanges/derive/client.js +135 -0
  123. package/dist/exchanges/derive/options.d.ts +73 -0
  124. package/dist/exchanges/derive/options.js +379 -0
  125. package/dist/exchanges/extended/adapter.d.ts +96 -0
  126. package/dist/exchanges/extended/adapter.js +241 -0
  127. package/dist/exchanges/extended/client.d.ts +38 -0
  128. package/dist/exchanges/extended/client.js +88 -0
  129. package/dist/exchanges/extended/public.d.ts +133 -0
  130. package/dist/exchanges/extended/public.js +436 -0
  131. package/dist/exchanges/grvt/adapter.d.ts +100 -0
  132. package/dist/exchanges/grvt/adapter.js +283 -0
  133. package/dist/exchanges/grvt/client.d.ts +25 -0
  134. package/dist/exchanges/grvt/client.js +86 -0
  135. package/dist/exchanges/grvt/public.d.ts +151 -0
  136. package/dist/exchanges/grvt/public.js +438 -0
  137. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  138. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  139. package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
  140. package/dist/exchanges/hyperliquid-spot.js +24 -16
  141. package/dist/exchanges/hyperliquid.d.ts +199 -22
  142. package/dist/exchanges/hyperliquid.js +550 -202
  143. package/dist/exchanges/index.d.ts +1 -1
  144. package/dist/exchanges/index.js +1 -1
  145. package/dist/exchanges/interface.d.ts +157 -13
  146. package/dist/exchanges/interface.js +47 -0
  147. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  148. package/dist/exchanges/ioc-outcome.js +1 -1
  149. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  150. package/dist/exchanges/legacy-trading-v2.js +7 -2
  151. package/dist/exchanges/lighter-public.d.ts +67 -12
  152. package/dist/exchanges/lighter-public.js +250 -82
  153. package/dist/exchanges/lighter-signer.d.ts +1 -1
  154. package/dist/exchanges/lighter-signer.js +1 -1
  155. package/dist/exchanges/lighter-spot.d.ts +10 -5
  156. package/dist/exchanges/lighter-spot.js +105 -97
  157. package/dist/exchanges/lighter.d.ts +80 -21
  158. package/dist/exchanges/lighter.js +312 -105
  159. package/dist/exchanges/ondo/adapter.d.ts +38 -13
  160. package/dist/exchanges/ondo/adapter.js +86 -27
  161. package/dist/exchanges/ondo/agent.d.ts +1 -1
  162. package/dist/exchanges/ondo/agent.js +1 -1
  163. package/dist/exchanges/ondo/client.d.ts +1 -1
  164. package/dist/exchanges/ondo/client.js +2 -2
  165. package/dist/exchanges/ondo/public.d.ts +35 -3
  166. package/dist/exchanges/ondo/public.js +75 -32
  167. package/dist/exchanges/ondo/spot.d.ts +2 -2
  168. package/dist/exchanges/ondo/spot.js +3 -3
  169. package/dist/exchanges/options-interface.d.ts +158 -0
  170. package/dist/exchanges/options-interface.js +17 -0
  171. package/dist/exchanges/options-venues.d.ts +12 -0
  172. package/dist/exchanges/options-venues.js +35 -0
  173. package/dist/exchanges/outcome-interface.d.ts +4 -0
  174. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  175. package/dist/exchanges/pacifica-atomic.js +440 -0
  176. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  177. package/dist/exchanges/pacifica-decimal.js +51 -0
  178. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  179. package/dist/exchanges/pacifica-presigned.js +141 -0
  180. package/dist/exchanges/pacifica-print.d.ts +200 -0
  181. package/dist/exchanges/pacifica-print.js +417 -0
  182. package/dist/exchanges/pacifica-spot.d.ts +8 -2
  183. package/dist/exchanges/pacifica-spot.js +26 -18
  184. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  185. package/dist/exchanges/pacifica-typed-data.js +1 -1
  186. package/dist/exchanges/pacifica-vaults.d.ts +130 -0
  187. package/dist/exchanges/pacifica-vaults.js +277 -0
  188. package/dist/exchanges/pacifica.d.ts +108 -10
  189. package/dist/exchanges/pacifica.js +428 -171
  190. package/dist/exchanges/precision.d.ts +3 -1
  191. package/dist/exchanges/precision.js +9 -2
  192. package/dist/exchanges/public-balances.d.ts +25 -0
  193. package/dist/exchanges/public-balances.js +158 -0
  194. package/dist/exchanges/public-positions.d.ts +43 -0
  195. package/dist/exchanges/public-positions.js +154 -0
  196. package/dist/exchanges/qfex/adapter.d.ts +69 -13
  197. package/dist/exchanges/qfex/adapter.js +102 -17
  198. package/dist/exchanges/qfex/agent.d.ts +1 -1
  199. package/dist/exchanges/qfex/agent.js +1 -1
  200. package/dist/exchanges/qfex/client.d.ts +1 -1
  201. package/dist/exchanges/qfex/client.js +1 -1
  202. package/dist/exchanges/qfex/errors.d.ts +1 -1
  203. package/dist/exchanges/qfex/errors.js +2 -2
  204. package/dist/exchanges/qfex/public.d.ts +65 -12
  205. package/dist/exchanges/qfex/public.js +131 -25
  206. package/dist/exchanges/qfex/signing.d.ts +2 -2
  207. package/dist/exchanges/qfex/signing.js +4 -5
  208. package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
  209. package/dist/exchanges/qfex/ws-transport.js +3 -3
  210. package/dist/exchanges/rate-limiter.d.ts +1 -1
  211. package/dist/exchanges/rate-limiter.js +5 -2
  212. package/dist/exchanges/registry.d.ts +1 -1
  213. package/dist/exchanges/registry.js +1 -1
  214. package/dist/exchanges/risex/adapter.d.ts +160 -0
  215. package/dist/exchanges/risex/adapter.js +842 -0
  216. package/dist/exchanges/risex/agent.d.ts +154 -0
  217. package/dist/exchanges/risex/agent.js +384 -0
  218. package/dist/exchanges/risex/client.d.ts +47 -0
  219. package/dist/exchanges/risex/client.js +174 -0
  220. package/dist/exchanges/risex/nonce.d.ts +43 -0
  221. package/dist/exchanges/risex/nonce.js +212 -0
  222. package/dist/exchanges/risex/public.d.ts +64 -0
  223. package/dist/exchanges/risex/public.js +150 -0
  224. package/dist/exchanges/risex/session.d.ts +71 -0
  225. package/dist/exchanges/risex/session.js +171 -0
  226. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  227. package/dist/exchanges/risex/typed-data.js +240 -0
  228. package/dist/exchanges/risex/units.d.ts +29 -0
  229. package/dist/exchanges/risex/units.js +104 -0
  230. package/dist/exchanges/slippage.d.ts +1 -1
  231. package/dist/exchanges/slippage.js +1 -1
  232. package/dist/exchanges/spot-factory.d.ts +3 -3
  233. package/dist/exchanges/spot-factory.js +8 -8
  234. package/dist/exchanges/spot-interface.d.ts +5 -5
  235. package/dist/exchanges/spot-interface.js +2 -2
  236. package/dist/exchanges/spot-result.d.ts +3 -3
  237. package/dist/exchanges/spot-result.js +3 -3
  238. package/dist/exchanges/trading-v2.d.ts +3 -3
  239. package/dist/exchanges/trading-v2.js +9 -6
  240. package/dist/exchanges/txflow/adapter.d.ts +61 -0
  241. package/dist/exchanges/txflow/adapter.js +150 -0
  242. package/dist/exchanges/txflow/client.d.ts +47 -0
  243. package/dist/exchanges/txflow/client.js +155 -0
  244. package/dist/exchanges/txflow/public.d.ts +87 -0
  245. package/dist/exchanges/txflow/public.js +355 -0
  246. package/dist/exchanges/unknown-symbol.d.ts +34 -0
  247. package/dist/exchanges/unknown-symbol.js +56 -0
  248. package/dist/exchanges/variational/adapter.d.ts +67 -0
  249. package/dist/exchanges/variational/adapter.js +137 -0
  250. package/dist/exchanges/variational/client.d.ts +21 -0
  251. package/dist/exchanges/variational/client.js +66 -0
  252. package/dist/exchanges/variational/public.d.ts +126 -0
  253. package/dist/exchanges/variational/public.js +430 -0
  254. package/dist/exchanges/venue-types.d.ts +52 -10
  255. package/dist/exchanges/venue-types.js +82 -11
  256. package/dist/exchanges/venue.d.ts +150 -11
  257. package/dist/exchanges/venue.js +126 -4
  258. package/dist/exchanges/venues/arcus.d.ts +1 -2
  259. package/dist/exchanges/venues/arcus.js +5 -4
  260. package/dist/exchanges/venues/aster.d.ts +2 -2
  261. package/dist/exchanges/venues/aster.js +6 -4
  262. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  263. package/dist/exchanges/venues/create-helpers.js +3 -3
  264. package/dist/exchanges/venues/extended.d.ts +21 -0
  265. package/dist/exchanges/venues/extended.js +96 -0
  266. package/dist/exchanges/venues/grvt.d.ts +30 -0
  267. package/dist/exchanges/venues/grvt.js +98 -0
  268. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  269. package/dist/exchanges/venues/hyperliquid.js +19 -8
  270. package/dist/exchanges/venues/index.d.ts +7 -2
  271. package/dist/exchanges/venues/index.js +8 -2
  272. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  273. package/dist/exchanges/venues/lighter-family.js +9 -11
  274. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  275. package/dist/exchanges/venues/lighter-rh.js +20 -8
  276. package/dist/exchanges/venues/lighter.d.ts +1 -1
  277. package/dist/exchanges/venues/lighter.js +18 -6
  278. package/dist/exchanges/venues/ondo.d.ts +1 -1
  279. package/dist/exchanges/venues/ondo.js +5 -3
  280. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  281. package/dist/exchanges/venues/pacifica.js +15 -4
  282. package/dist/exchanges/venues/qfex.d.ts +1 -1
  283. package/dist/exchanges/venues/qfex.js +4 -2
  284. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  285. package/dist/exchanges/venues/risex-release.js +14 -0
  286. package/dist/exchanges/venues/risex.d.ts +31 -0
  287. package/dist/exchanges/venues/risex.js +131 -0
  288. package/dist/exchanges/venues/txflow.d.ts +18 -0
  289. package/dist/exchanges/venues/txflow.js +77 -0
  290. package/dist/exchanges/venues/variational.d.ts +21 -0
  291. package/dist/exchanges/venues/variational.js +85 -0
  292. package/dist/execution-log.d.ts +1 -1
  293. package/dist/funding/legacy-public.d.ts +6 -4
  294. package/dist/funding/legacy-public.js +42 -41
  295. package/dist/funding/normalize.d.ts +1 -1
  296. package/dist/funding/normalize.js +1 -1
  297. package/dist/funding/rates.d.ts +95 -6
  298. package/dist/funding/rates.js +99 -19
  299. package/dist/index.js +7 -6
  300. package/dist/jobs.d.ts +1 -0
  301. package/dist/jobs.js +25 -1
  302. package/dist/landing.d.ts +2 -0
  303. package/dist/landing.js +1 -1
  304. package/dist/liquidity.d.ts +3 -1
  305. package/dist/liquidity.js +8 -4
  306. package/dist/mcp-pacifica-earn.d.ts +9 -0
  307. package/dist/mcp-pacifica-earn.js +64 -0
  308. package/dist/mcp-server.js +211 -68
  309. package/dist/pacifica/client.d.ts +69 -5
  310. package/dist/pacifica/client.js +181 -8
  311. package/dist/pacifica/rate-limit.d.ts +144 -0
  312. package/dist/pacifica/rate-limit.js +267 -0
  313. package/dist/pacifica/types/order.d.ts +2 -1
  314. package/dist/plan-executor.js +7 -2
  315. package/dist/positions-by-address.d.ts +34 -0
  316. package/dist/positions-by-address.js +46 -0
  317. package/dist/rebalance.js +1 -1
  318. package/dist/risk.d.ts +2 -1
  319. package/dist/risk.js +2 -4
  320. package/dist/settings.d.ts +32 -3
  321. package/dist/signer/ows-evm.d.ts +1 -1
  322. package/dist/signer/ows-evm.js +1 -1
  323. package/dist/smart-order.d.ts +1 -1
  324. package/dist/smart-order.js +1 -1
  325. package/dist/strategies/funding-arb.d.ts +1 -1
  326. package/dist/strategies/funding-arb.js +36 -63
  327. package/dist/trade-validator.js +2 -2
  328. package/dist/utils.d.ts +22 -3
  329. package/dist/utils.js +11 -3
  330. package/dist/venue-health.d.ts +40 -0
  331. package/dist/venue-health.js +75 -0
  332. package/dist/withdraw-gate.d.ts +1 -1
  333. package/dist/withdraw-gate.js +1 -1
  334. package/dist/ws/arcus-market.d.ts +9 -0
  335. package/dist/ws/arcus-market.js +9 -0
  336. package/dist/ws/aster-market.d.ts +33 -0
  337. package/dist/ws/aster-market.js +30 -0
  338. package/dist/ws/index.d.ts +980 -0
  339. package/dist/ws/index.js +4460 -0
  340. package/dist/ws/lighter-market.d.ts +14 -0
  341. package/dist/ws/lighter-market.js +13 -0
  342. package/dist/ws/node.d.ts +996 -0
  343. package/dist/ws/node.js +4554 -0
  344. package/dist/ws/pacifica-instrument.d.ts +8 -0
  345. package/dist/ws/pacifica-instrument.js +8 -0
  346. package/dist/ws/unclassified.d.ts +16 -0
  347. package/dist/ws/unclassified.js +43 -0
  348. package/dist/ws-cli/credentials.d.ts +62 -0
  349. package/dist/ws-cli/credentials.js +126 -0
  350. package/package.json +71 -10
  351. package/skills/perp-cli/SKILL.md +153 -36
  352. package/skills/perp-cli/package.json +15 -2
  353. package/skills/perp-cli/references/agent-operations.md +22 -12
  354. package/skills/perp-cli/references/commands.md +143 -17
  355. package/skills/perp-cli/references/strategies.md +38 -3
  356. package/skills/perp-cli/references/ws-library.md +523 -0
  357. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  358. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  359. package/skills/perp-cli/scripts/preflight.sh +27 -19
  360. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  361. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  362. package/dist/cli-spec.d.ts +0 -46
  363. 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 (design §13.3). GTC / IOC / ALO / TOB are native; venue
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 (design §13.4, §13.10 #10, like Ondo's
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 excluded, design §12). */
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((m) => m.instrument_type !== "spot");
350
+ return requireInstrumentType(rows).filter(isPacificaPerpetual);
326
351
  }
327
- /** Wire symbols of /info spot markets. */
328
- function spotInfoSymbols(rows) {
329
- return new Set(requireInstrumentType(rows).filter((m) => m.instrument_type === "spot").map((m) => m.symbol));
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.client.getInfo(),
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 (§12).
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: m.next_funding_rate ?? m.funding_rate,
748
+ // `fundingRate` = the LAST SETTLED hourly rate (/info funding_rate — the WS prices `funding`,
749
+ // = the newest /funding_rate/history print); the rate expected at the next settlement
750
+ // (next_funding_rate, WS `next_funding` / predictedRate) is `predictedFundingRate`.
751
+ fundingRate: m.funding_rate ?? null,
752
+ fundingRateKind: m.funding_rate != null ? "lastSettled" : null,
753
+ predictedFundingRate: m.next_funding_rate ?? null,
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
- // Pacifica publishes neither a per-market funding interval nor margin tiers.
573
- fundingHours: null,
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
- const book = await this.client.getBook(symbol);
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 new Error("No private key configured — account data unavailable. Run: perp setup");
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
- const raw = info;
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 new Error("No private key configured — account data unavailable. Run: perp setup");
848
+ throw noAccountKeyError("pacifica");
633
849
  const [positions, prices] = await Promise.all([
634
850
  this._getPositions(),
635
851
  this._getPrices(),
636
852
  ]);
637
- const priceMap = new Map(prices.map((p) => [p.symbol, p]));
638
- const mapped = positions.map((p) => {
639
- const mark = priceMap.get(p.symbol)?.mark ?? p.mark_price ?? "0";
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 new Error("No private key configured — account data unavailable. Run: perp setup");
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 spotSymbols = spotInfoSymbols(info);
740
- return orders.filter((o) => !spotSymbols.has(String(o.symbol))).map((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 (design §13.10 #4). A non-terminal partial row is not final.
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, design §13.10 #4),
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); // §13.3 common mapping; FOK → CAPABILITY_UNSUPPORTED
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 new Error("No private key configured — account data unavailable. Run: perp setup");
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 (design §13.4, §13.10 #10). `size` = new open size.
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(sym, orderId, price, size);
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: sym, order_id: Number(orderId) }, this.account, this.signMessage);
1276
+ cancelRaw = await this.client.cancelOrder({ symbol: venueSym, order_id: Number(orderId) }, this.account, this.signMessage);
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: sym, price, amount: replaceSize, side: original.side, reduce_only: original.reduce_only === true, tif: keep, client_order_id: cloid }, this.account, this.signMessage, await this.builderCode());
1337
+ placed = await this.client.createLimitOrder({ symbol: venueSym, price, amount: replaceSize, side: original.side, reduce_only: original.reduce_only === true, tif: keep, client_order_id: cloid }, this.account, this.signMessage, await this.builderCode());
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 raw = await this.client.cancelStopOrder({ symbol: symbol.toUpperCase(), order_id: Number(orderId) }, this.account, this.signMessage);
1372
+ const venueSym = await this.listedSymbol(symbol);
1373
+ const raw = await this.client.cancelStopOrder({ symbol: venueSym, order_id: Number(orderId) }, this.account, this.signMessage);
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: symbol.toUpperCase(),
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
- async getRecentTrades(symbol, _limit = 20) {
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
- // Deduplicate by time+price+size+side (API sometimes returns duplicate entries)
1320
- const seen = new Set();
1321
- const unique = trades.filter((t) => {
1322
- const key = `${t.created_at}:${t.price}:${t.amount}:${t.side}`;
1323
- if (seen.has(key))
1324
- return false;
1325
- seen.add(key);
1326
- return true;
1327
- });
1328
- return unique.slice(0, _limit).map((t) => ({
1329
- time: Number(t.created_at ?? 0),
1330
- symbol,
1331
- side: t.side === "bid" ? "buy" : "sell",
1332
- price: t.price,
1333
- size: t.amount,
1334
- fee: String(t.fee ?? "0"),
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
- const result = await this.client.getFundingHistory(symbol, { limit });
1339
- const data = (result.data ?? result);
1340
- if (!Array.isArray(data))
1341
- return [];
1342
- return data.map((h) => ({
1343
- time: Number(h.created_at ?? 0),
1344
- rate: venueRateField(h.funding_rate, "funding history funding_rate", "pacifica"),
1345
- price: String(h.oracle_price ?? "0"),
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 klines = await this.client.getKline(symbol, interval, startTime, endTime);
1350
- return klines.map((k) => ({
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 new Error("No private key configured — account data unavailable. Run: perp setup");
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 new Error("No private key configured — account data unavailable. Run: perp setup");
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: buy ? "buy" : "sell",
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 new Error("No private key configured — account data unavailable. Run: perp setup");
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" });