perp-cli 0.14.3 → 0.15.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 (311) hide show
  1. package/README.md +152 -13
  2. package/dist/agent-wallet/store.d.ts +3 -2
  3. package/dist/agent-wallet/store.js +2 -2
  4. package/dist/api/public/hyperliquid.js +4 -0
  5. package/dist/api/public/pacifica.js +83 -13
  6. package/dist/api/public/urls.d.ts +2 -0
  7. package/dist/api/public/urls.js +3 -0
  8. package/dist/arb/utils.d.ts +1 -2
  9. package/dist/arb/utils.js +1 -2
  10. package/dist/bot/apex/pulse.js +3 -0
  11. package/dist/bot/apex/radar.js +2 -1
  12. package/dist/bot/engine.d.ts +10 -1
  13. package/dist/bot/engine.js +14 -6
  14. package/dist/bot/indicators.d.ts +7 -0
  15. package/dist/bot/indicators.js +16 -0
  16. package/dist/bot/perp-order.d.ts +4 -4
  17. package/dist/bot/perp-order.js +5 -5
  18. package/dist/bot/quarantine.d.ts +1 -1
  19. package/dist/bot/quarantine.js +1 -1
  20. package/dist/bot/strategies/apex-strategy.js +3 -2
  21. package/dist/bot/strategies/engine-mm.js +6 -5
  22. package/dist/bot/strategies/funding-arb-strategy.js +2 -2
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
  24. package/dist/bot/strategies/funding-arb-utils.js +5 -5
  25. package/dist/bot/strategies/funding-auto.js +2 -2
  26. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  27. package/dist/bot/strategies/liquidation-mm.js +8 -2
  28. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  29. package/dist/bot/strategies/momentum-breakout.js +11 -3
  30. package/dist/bot/strategies/spot-perp-arb-strategy.js +2 -2
  31. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  32. package/dist/bot/strategies/spot-perp-executor.js +1 -1
  33. package/dist/bot/strategy-types.d.ts +2 -1
  34. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  35. package/dist/bot/tui/BotDashboard.js +1 -1
  36. package/dist/cache.js +12 -6
  37. package/dist/cli-program.d.ts +15 -0
  38. package/dist/cli-program.js +125 -5
  39. package/dist/commands/agent-import.d.ts +7 -0
  40. package/dist/commands/agent-import.js +30 -1
  41. package/dist/commands/agent.d.ts +6 -3
  42. package/dist/commands/agent.js +160 -12
  43. package/dist/commands/arb/index.js +4 -4
  44. package/dist/commands/arb-auto.js +21 -17
  45. package/dist/commands/arb.d.ts +12 -0
  46. package/dist/commands/arb.js +61 -143
  47. package/dist/commands/bot.js +1 -1
  48. package/dist/commands/builder.js +1 -1
  49. package/dist/commands/funds.js +113 -3
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +27 -7
  52. package/dist/commands/manage.js +23 -21
  53. package/dist/commands/market.js +167 -35
  54. package/dist/commands/options.d.ts +9 -0
  55. package/dist/commands/options.js +134 -0
  56. package/dist/commands/pacifica-earn.d.ts +5 -0
  57. package/dist/commands/pacifica-earn.js +468 -0
  58. package/dist/commands/pacifica-orders.d.ts +14 -0
  59. package/dist/commands/pacifica-orders.js +142 -0
  60. package/dist/commands/portfolio.js +32 -47
  61. package/dist/commands/rebalance.d.ts +1 -0
  62. package/dist/commands/rebalance.js +15 -0
  63. package/dist/commands/risk.d.ts +11 -2
  64. package/dist/commands/risk.js +39 -44
  65. package/dist/commands/trade.d.ts +1 -0
  66. package/dist/commands/trade.js +63 -58
  67. package/dist/commands/wallet.js +1 -1
  68. package/dist/config.d.ts +2 -2
  69. package/dist/config.js +15 -4
  70. package/dist/constants.js +6 -0
  71. package/dist/dashboard/server.js +1 -1
  72. package/dist/errors.d.ts +24 -1
  73. package/dist/errors.js +25 -3
  74. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  75. package/dist/exchanges/arcus/adapter.js +12 -12
  76. package/dist/exchanges/arcus/agent.d.ts +1 -1
  77. package/dist/exchanges/arcus/agent.js +1 -1
  78. package/dist/exchanges/arcus/client.js +1 -1
  79. package/dist/exchanges/arcus/signing.d.ts +2 -3
  80. package/dist/exchanges/arcus/signing.js +1 -1
  81. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  82. package/dist/exchanges/arcus/spot-chain.js +1 -1
  83. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  84. package/dist/exchanges/arcus/spot-router.js +2 -2
  85. package/dist/exchanges/arcus/spot.d.ts +2 -3
  86. package/dist/exchanges/arcus/spot.js +2 -3
  87. package/dist/exchanges/aster-spot.d.ts +3 -3
  88. package/dist/exchanges/aster-spot.js +6 -9
  89. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  90. package/dist/exchanges/aster-typed-data.js +1 -1
  91. package/dist/exchanges/aster.d.ts +4 -4
  92. package/dist/exchanges/aster.js +31 -13
  93. package/dist/exchanges/builder-fee.d.ts +1 -1
  94. package/dist/exchanges/builder-fee.js +13 -2
  95. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  96. package/dist/exchanges/canonical-symbols.js +26 -2
  97. package/dist/exchanges/derive/client.d.ts +37 -0
  98. package/dist/exchanges/derive/client.js +135 -0
  99. package/dist/exchanges/derive/options.d.ts +68 -0
  100. package/dist/exchanges/derive/options.js +360 -0
  101. package/dist/exchanges/extended/adapter.d.ts +82 -0
  102. package/dist/exchanges/extended/adapter.js +179 -0
  103. package/dist/exchanges/extended/client.d.ts +38 -0
  104. package/dist/exchanges/extended/client.js +88 -0
  105. package/dist/exchanges/extended/public.d.ts +130 -0
  106. package/dist/exchanges/extended/public.js +429 -0
  107. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  108. package/dist/exchanges/grvt/adapter.js +206 -0
  109. package/dist/exchanges/grvt/client.d.ts +25 -0
  110. package/dist/exchanges/grvt/client.js +86 -0
  111. package/dist/exchanges/grvt/public.d.ts +151 -0
  112. package/dist/exchanges/grvt/public.js +438 -0
  113. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  114. package/dist/exchanges/hyperliquid-spot.js +5 -5
  115. package/dist/exchanges/hyperliquid.d.ts +22 -12
  116. package/dist/exchanges/hyperliquid.js +80 -34
  117. package/dist/exchanges/index.d.ts +1 -1
  118. package/dist/exchanges/index.js +1 -1
  119. package/dist/exchanges/interface.d.ts +94 -6
  120. package/dist/exchanges/interface.js +16 -0
  121. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  122. package/dist/exchanges/ioc-outcome.js +1 -1
  123. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  124. package/dist/exchanges/legacy-trading-v2.js +7 -2
  125. package/dist/exchanges/lighter-public.d.ts +2 -2
  126. package/dist/exchanges/lighter-public.js +2 -2
  127. package/dist/exchanges/lighter-signer.d.ts +1 -1
  128. package/dist/exchanges/lighter-signer.js +1 -1
  129. package/dist/exchanges/lighter-spot.d.ts +3 -4
  130. package/dist/exchanges/lighter-spot.js +6 -7
  131. package/dist/exchanges/lighter.d.ts +11 -11
  132. package/dist/exchanges/lighter.js +28 -14
  133. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  134. package/dist/exchanges/ondo/adapter.js +16 -14
  135. package/dist/exchanges/ondo/agent.d.ts +1 -1
  136. package/dist/exchanges/ondo/agent.js +1 -1
  137. package/dist/exchanges/ondo/client.d.ts +1 -1
  138. package/dist/exchanges/ondo/client.js +2 -2
  139. package/dist/exchanges/ondo/public.d.ts +8 -1
  140. package/dist/exchanges/ondo/public.js +13 -2
  141. package/dist/exchanges/ondo/spot.d.ts +2 -2
  142. package/dist/exchanges/ondo/spot.js +3 -3
  143. package/dist/exchanges/options-interface.d.ts +158 -0
  144. package/dist/exchanges/options-interface.js +17 -0
  145. package/dist/exchanges/options-venues.d.ts +12 -0
  146. package/dist/exchanges/options-venues.js +35 -0
  147. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  148. package/dist/exchanges/pacifica-atomic.js +440 -0
  149. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  150. package/dist/exchanges/pacifica-decimal.js +51 -0
  151. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  152. package/dist/exchanges/pacifica-presigned.js +141 -0
  153. package/dist/exchanges/pacifica-print.d.ts +200 -0
  154. package/dist/exchanges/pacifica-print.js +417 -0
  155. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  156. package/dist/exchanges/pacifica-spot.js +3 -3
  157. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  158. package/dist/exchanges/pacifica-typed-data.js +1 -1
  159. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  160. package/dist/exchanges/pacifica-vaults.js +264 -0
  161. package/dist/exchanges/pacifica.d.ts +32 -5
  162. package/dist/exchanges/pacifica.js +114 -48
  163. package/dist/exchanges/precision.d.ts +3 -1
  164. package/dist/exchanges/precision.js +9 -2
  165. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  166. package/dist/exchanges/qfex/adapter.js +32 -12
  167. package/dist/exchanges/qfex/agent.d.ts +1 -1
  168. package/dist/exchanges/qfex/agent.js +1 -1
  169. package/dist/exchanges/qfex/client.d.ts +1 -1
  170. package/dist/exchanges/qfex/client.js +1 -1
  171. package/dist/exchanges/qfex/errors.d.ts +1 -1
  172. package/dist/exchanges/qfex/errors.js +2 -2
  173. package/dist/exchanges/qfex/public.d.ts +3 -3
  174. package/dist/exchanges/qfex/public.js +3 -3
  175. package/dist/exchanges/qfex/signing.d.ts +2 -2
  176. package/dist/exchanges/qfex/signing.js +4 -5
  177. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  178. package/dist/exchanges/qfex/ws-transport.js +3 -3
  179. package/dist/exchanges/rate-limiter.d.ts +1 -1
  180. package/dist/exchanges/rate-limiter.js +5 -2
  181. package/dist/exchanges/registry.d.ts +1 -1
  182. package/dist/exchanges/registry.js +1 -1
  183. package/dist/exchanges/risex/adapter.d.ts +148 -0
  184. package/dist/exchanges/risex/adapter.js +796 -0
  185. package/dist/exchanges/risex/agent.d.ts +154 -0
  186. package/dist/exchanges/risex/agent.js +384 -0
  187. package/dist/exchanges/risex/client.d.ts +47 -0
  188. package/dist/exchanges/risex/client.js +174 -0
  189. package/dist/exchanges/risex/nonce.d.ts +43 -0
  190. package/dist/exchanges/risex/nonce.js +212 -0
  191. package/dist/exchanges/risex/public.d.ts +64 -0
  192. package/dist/exchanges/risex/public.js +150 -0
  193. package/dist/exchanges/risex/session.d.ts +71 -0
  194. package/dist/exchanges/risex/session.js +171 -0
  195. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  196. package/dist/exchanges/risex/typed-data.js +240 -0
  197. package/dist/exchanges/risex/units.d.ts +29 -0
  198. package/dist/exchanges/risex/units.js +104 -0
  199. package/dist/exchanges/slippage.d.ts +1 -1
  200. package/dist/exchanges/slippage.js +1 -1
  201. package/dist/exchanges/spot-factory.d.ts +3 -3
  202. package/dist/exchanges/spot-factory.js +7 -7
  203. package/dist/exchanges/spot-interface.d.ts +5 -5
  204. package/dist/exchanges/spot-interface.js +2 -2
  205. package/dist/exchanges/spot-result.d.ts +3 -3
  206. package/dist/exchanges/spot-result.js +3 -3
  207. package/dist/exchanges/trading-v2.d.ts +3 -3
  208. package/dist/exchanges/trading-v2.js +9 -6
  209. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  210. package/dist/exchanges/txflow/adapter.js +118 -0
  211. package/dist/exchanges/txflow/client.d.ts +47 -0
  212. package/dist/exchanges/txflow/client.js +153 -0
  213. package/dist/exchanges/txflow/public.d.ts +76 -0
  214. package/dist/exchanges/txflow/public.js +284 -0
  215. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  216. package/dist/exchanges/unknown-symbol.js +51 -0
  217. package/dist/exchanges/variational/adapter.d.ts +71 -0
  218. package/dist/exchanges/variational/adapter.js +137 -0
  219. package/dist/exchanges/variational/client.d.ts +21 -0
  220. package/dist/exchanges/variational/client.js +66 -0
  221. package/dist/exchanges/variational/public.d.ts +126 -0
  222. package/dist/exchanges/variational/public.js +430 -0
  223. package/dist/exchanges/venue-types.d.ts +6 -7
  224. package/dist/exchanges/venue-types.js +2 -2
  225. package/dist/exchanges/venue.d.ts +51 -8
  226. package/dist/exchanges/venue.js +54 -4
  227. package/dist/exchanges/venues/arcus.d.ts +1 -2
  228. package/dist/exchanges/venues/arcus.js +5 -4
  229. package/dist/exchanges/venues/aster.d.ts +2 -2
  230. package/dist/exchanges/venues/aster.js +6 -4
  231. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  232. package/dist/exchanges/venues/create-helpers.js +3 -3
  233. package/dist/exchanges/venues/extended.d.ts +21 -0
  234. package/dist/exchanges/venues/extended.js +96 -0
  235. package/dist/exchanges/venues/grvt.d.ts +30 -0
  236. package/dist/exchanges/venues/grvt.js +98 -0
  237. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  238. package/dist/exchanges/venues/hyperliquid.js +10 -8
  239. package/dist/exchanges/venues/index.d.ts +7 -2
  240. package/dist/exchanges/venues/index.js +8 -2
  241. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  242. package/dist/exchanges/venues/lighter-family.js +9 -11
  243. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  244. package/dist/exchanges/venues/lighter-rh.js +7 -5
  245. package/dist/exchanges/venues/lighter.d.ts +1 -1
  246. package/dist/exchanges/venues/lighter.js +5 -3
  247. package/dist/exchanges/venues/ondo.d.ts +1 -1
  248. package/dist/exchanges/venues/ondo.js +5 -3
  249. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  250. package/dist/exchanges/venues/pacifica.js +6 -4
  251. package/dist/exchanges/venues/qfex.d.ts +1 -1
  252. package/dist/exchanges/venues/qfex.js +4 -2
  253. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  254. package/dist/exchanges/venues/risex-release.js +14 -0
  255. package/dist/exchanges/venues/risex.d.ts +31 -0
  256. package/dist/exchanges/venues/risex.js +116 -0
  257. package/dist/exchanges/venues/txflow.d.ts +18 -0
  258. package/dist/exchanges/venues/txflow.js +77 -0
  259. package/dist/exchanges/venues/variational.d.ts +21 -0
  260. package/dist/exchanges/venues/variational.js +85 -0
  261. package/dist/execution-log.d.ts +1 -1
  262. package/dist/funding/legacy-public.d.ts +1 -1
  263. package/dist/funding/legacy-public.js +9 -4
  264. package/dist/funding/normalize.d.ts +1 -1
  265. package/dist/funding/normalize.js +1 -1
  266. package/dist/funding/rates.d.ts +44 -4
  267. package/dist/funding/rates.js +56 -12
  268. package/dist/index.js +7 -6
  269. package/dist/jobs.d.ts +1 -0
  270. package/dist/jobs.js +25 -1
  271. package/dist/landing.d.ts +2 -0
  272. package/dist/landing.js +1 -1
  273. package/dist/mcp-pacifica-earn.d.ts +9 -0
  274. package/dist/mcp-pacifica-earn.js +58 -0
  275. package/dist/mcp-server.js +113 -59
  276. package/dist/pacifica/client.d.ts +67 -4
  277. package/dist/pacifica/client.js +170 -8
  278. package/dist/pacifica/rate-limit.d.ts +144 -0
  279. package/dist/pacifica/rate-limit.js +267 -0
  280. package/dist/pacifica/types/order.d.ts +2 -1
  281. package/dist/plan-executor.js +7 -2
  282. package/dist/rebalance.js +1 -1
  283. package/dist/risk.d.ts +2 -1
  284. package/dist/risk.js +2 -4
  285. package/dist/settings.d.ts +32 -3
  286. package/dist/signer/ows-evm.d.ts +1 -1
  287. package/dist/signer/ows-evm.js +1 -1
  288. package/dist/smart-order.d.ts +1 -1
  289. package/dist/smart-order.js +1 -1
  290. package/dist/strategies/funding-arb.d.ts +1 -1
  291. package/dist/strategies/funding-arb.js +1 -1
  292. package/dist/trade-validator.js +2 -2
  293. package/dist/utils.d.ts +12 -3
  294. package/dist/utils.js +11 -3
  295. package/dist/venue-health.d.ts +40 -0
  296. package/dist/venue-health.js +75 -0
  297. package/dist/withdraw-gate.d.ts +1 -1
  298. package/dist/withdraw-gate.js +1 -1
  299. package/dist/ws/index.d.ts +959 -0
  300. package/dist/ws/index.js +4295 -0
  301. package/dist/ws/node.d.ts +975 -0
  302. package/dist/ws/node.js +4366 -0
  303. package/dist/ws/pacifica-instrument.d.ts +8 -0
  304. package/dist/ws/pacifica-instrument.js +8 -0
  305. package/dist/ws-cli/credentials.d.ts +62 -0
  306. package/dist/ws-cli/credentials.js +126 -0
  307. package/package.json +58 -10
  308. package/skills/perp-cli/SKILL.md +62 -9
  309. package/skills/perp-cli/references/agent-operations.md +7 -0
  310. package/skills/perp-cli/references/commands.md +76 -5
  311. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Pacifica credit rate limit, read from the venue's own response headers.
3
+ *
4
+ * docs.pacifica.fi api-documentation/api/rate-limits (read 2026-10-19): credit-based, rolling
5
+ * 60 s window; 100 credits / 60 s per unidentified IP, 300 with a valid API Config Key (more for
6
+ * higher fee tiers); standard request 1, cancel 0.5, heavy GET 3-12 (1-4 with a key). Exhausted →
7
+ * HTTP 429. "All credit values are multiplied by 10 to support fractional costs":
8
+ *
9
+ * ratelimit: "credits";r=1200;t=32 r = remaining (×10), t = seconds until refresh
10
+ * ratelimit-policy: "credits";q=1250;w=60 q = quota per window (×10), w = window seconds
11
+ *
12
+ * Live mainnet GET /info/prices (2026-10-19, no key): `ratelimit: "credits";r=980;t=22`,
13
+ * `ratelimit-policy: "credits";q=1000;w=60`. Values below stay in those ×10 units.
14
+ *
15
+ * CloudFront caches some GETs (/info: `x-cache: Hit from cloudfront`, `age: 16`): a cache hit
16
+ * replays the headers of whoever filled the cache (live: q=3000, a keyed caller) — not this
17
+ * caller's quota, so a hit carries no information.
18
+ *
19
+ * Scope: one state per REST base URL (mainnet / testnet), credential tier (no key = IP quota;
20
+ * an API Config Key has its own) and request kind (signed POST vs unsigned GET). Process-local: other perp-cli processes (CLI + MCP + bots) on the
21
+ * same IP spend the same quota unseen, so a 429 can still happen — it is then surfaced as
22
+ * RATE_LIMITED, never retried here.
23
+ */
24
+ import { createHash } from "crypto";
25
+ import { PerpError } from "../errors.js";
26
+ /** `"credits";k=v;k=v` with non-negative integer values; null on any deviation. */
27
+ function parseCreditsItem(raw) {
28
+ if (raw === null)
29
+ return null;
30
+ const parts = raw.trim().split(";").map((p) => p.trim());
31
+ if (parts[0] !== '"credits"')
32
+ return null;
33
+ const out = new Map();
34
+ for (const p of parts.slice(1)) {
35
+ const m = /^([a-z][a-z0-9_-]*)=(\d{1,12})$/.exec(p);
36
+ if (m === null || out.has(m[1]))
37
+ return null;
38
+ out.set(m[1], Number(m[2]));
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * Strict parse of the `ratelimit` / `ratelimit-policy` header values. `ratelimit` absent or
44
+ * malformed (no `"credits"` item, missing / non-integer / duplicate r or t) → null: no info, never
45
+ * a guessed value. A malformed policy only nulls quota / windowSec.
46
+ */
47
+ export function parsePacificaRateLimit(ratelimit, policy) {
48
+ const rl = parseCreditsItem(ratelimit);
49
+ const r = rl?.get("r");
50
+ const t = rl?.get("t");
51
+ if (r === undefined || t === undefined)
52
+ return null;
53
+ const pol = parseCreditsItem(policy);
54
+ const q = pol?.get("q");
55
+ const w = pol?.get("w");
56
+ const policyOk = q !== undefined && w !== undefined && w > 0;
57
+ return { remaining: r, resetSec: t, quota: policyOk ? q : null, windowSec: policyOk ? w : null };
58
+ }
59
+ /** `Retry-After` in delta-seconds → ms; absent / HTTP-date / malformed → null. */
60
+ export function parseRetryAfterMs(raw) {
61
+ if (raw === null || !/^\d{1,9}$/.test(raw.trim()))
62
+ return null;
63
+ return Number(raw.trim()) * 1000;
64
+ }
65
+ function header(res, name) {
66
+ // Every real fetch Response has headers; a value without them carries no rate-limit info.
67
+ const h = res?.headers;
68
+ return h !== undefined && typeof h.get === "function" ? h.get(name) : null;
69
+ }
70
+ /** A CloudFront cache hit replays someone else's headers (see module doc). */
71
+ function isCacheHit(res) {
72
+ const xc = header(res, "x-cache");
73
+ return (xc !== null && /^(Hit|RefreshHit)\b/i.test(xc.trim())) || header(res, "age") !== null;
74
+ }
75
+ /** Parsed rate-limit info of a response this caller's request produced; null = no info. */
76
+ export function pacificaRateLimitOf(res) {
77
+ if (isCacheHit(res))
78
+ return null;
79
+ return parsePacificaRateLimit(header(res, "ratelimit"), header(res, "ratelimit-policy"));
80
+ }
81
+ /** Read reserve (credits ×10): the largest documented heavy-GET cost (12 credits) stays unspent after a read. */
82
+ export const PACIFICA_READ_RESERVE = 120;
83
+ /** Cost (credits ×10) of a standard request / action (docs: 1 credit). */
84
+ export const PACIFICA_COST_STANDARD = 10;
85
+ /** Cost (credits ×10) of an order cancellation (docs: 0.5 credits). */
86
+ export const PACIFICA_COST_CANCEL = 5;
87
+ /** Slack after the venue's `t` (whole seconds) before the window is treated as refreshed. */
88
+ const RESET_MARGIN_MS = 250;
89
+ /** Documented window (docs: "60-second rolling window") — only an upper clamp for `t` / Retry-After. */
90
+ const DOC_WINDOW_SEC = 60;
91
+ /** A reset / Retry-After longer than the window + this is clamped (a bogus value must not stall). */
92
+ const RESET_CLAMP_MARGIN_MS = 1_000;
93
+ /** Longest pre-send wait before failing fast with RATE_LIMITED. */
94
+ export const PACIFICA_MAX_THROTTLE_WAIT_MS = 15_000;
95
+ /** Longest wait between signing and sending (signed bodies expire after expiry_window = 5 s). */
96
+ export const PACIFICA_SIGNED_MAX_WAIT_MS = 2_000;
97
+ /** Monotonic: elapsed time never follows a wall-clock step (NTP, VM resume). */
98
+ const realClock = {
99
+ now: () => performance.now(),
100
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
101
+ };
102
+ export const PACIFICA_READ_NEED = { cost: PACIFICA_COST_STANDARD, reserve: PACIFICA_READ_RESERVE, maxWaitMs: PACIFICA_MAX_THROTTLE_WAIT_MS };
103
+ function clampResetMs(ms, windowSec) {
104
+ const cap = (windowSec !== null ? windowSec : DOC_WINDOW_SEC) * 1000 + RESET_CLAMP_MARGIN_MS;
105
+ return Math.min(ms, cap);
106
+ }
107
+ export class PacificaRateLimiter {
108
+ label;
109
+ clock;
110
+ obs = null;
111
+ /** Credits ×10 reserved by requests sent and not yet settled — not reset by observe(). */
112
+ inFlight = 0;
113
+ lastRaw = Number.NEGATIVE_INFINITY;
114
+ skew = 0;
115
+ constructor(label, clock = realClock) {
116
+ this.label = label;
117
+ this.clock = clock;
118
+ }
119
+ /** clock.now() made monotonic: a backwards step is absorbed instead of restarting elapsed time. */
120
+ now() {
121
+ const raw = this.clock.now();
122
+ if (raw < this.lastRaw)
123
+ this.skew += this.lastRaw - raw;
124
+ this.lastRaw = raw;
125
+ return raw + this.skew;
126
+ }
127
+ /** ms until the observed window refreshes; null = no live observation. */
128
+ resetWaitMs() {
129
+ if (this.obs === null)
130
+ return null;
131
+ const left = this.obs.resetInMs + RESET_MARGIN_MS - (this.now() - this.obs.observedAt);
132
+ if (left <= 0) {
133
+ this.obs = null;
134
+ return null;
135
+ } // window refreshed: the observation is spent
136
+ return left;
137
+ }
138
+ /** Current state for diagnostics (credits ×10); null = no live observation. */
139
+ snapshot() {
140
+ const wait = this.resetWaitMs();
141
+ if (wait === null || this.obs === null)
142
+ return null;
143
+ return { remaining: this.obs.remaining, inFlight: this.inFlight, effectiveRemaining: this.obs.remaining - this.inFlight, resetInMs: wait };
144
+ }
145
+ /** ms to wait before a request with `need` may be sent: 0 = now (no info, or enough credits left). */
146
+ waitMs(need) {
147
+ const wait = this.resetWaitMs();
148
+ if (wait === null || this.obs === null)
149
+ return 0;
150
+ return this.obs.remaining - this.inFlight - need.cost >= need.reserve ? 0 : wait;
151
+ }
152
+ ticket(cost) {
153
+ this.inFlight += cost;
154
+ let open = true;
155
+ const release = () => { if (open) {
156
+ open = false;
157
+ this.inFlight -= cost;
158
+ } };
159
+ return {
160
+ release,
161
+ settle: (res) => { release(); return this.observe(res); },
162
+ };
163
+ }
164
+ /**
165
+ * Pre-send throttle + reservation. Enough credits (or no info) → the cost is reserved
166
+ * synchronously, before any await, so concurrent callers see each other. Otherwise waits for the
167
+ * window to refresh (total ≤ need.maxWaitMs) or fails fast with RATE_LIMITED (sent: false).
168
+ */
169
+ async acquire(what, need) {
170
+ let budget = need.maxWaitMs;
171
+ for (;;) {
172
+ const wait = this.waitMs(need);
173
+ if (wait === 0)
174
+ return this.ticket(need.cost);
175
+ if (wait > budget)
176
+ throw this.throttled(what, wait);
177
+ const waitedOn = this.obs;
178
+ const start = this.now();
179
+ await this.clock.sleep(wait);
180
+ budget -= this.now() - start;
181
+ // The window that observation described has refreshed; a newer one (a response that landed
182
+ // meanwhile) is kept and re-checked.
183
+ if (this.obs === waitedOn)
184
+ this.obs = null;
185
+ }
186
+ }
187
+ /** Like acquire but never waits or refuses (health probe): reserves and records only. */
188
+ track(cost) {
189
+ return this.ticket(cost);
190
+ }
191
+ /** Wait (no reservation) until `need` would pass — used before signing, so the post-sign acquire finds credits. */
192
+ async awaitCapacity(what, need) {
193
+ (await this.acquire(what, need)).release();
194
+ }
195
+ throttled(what, wait) {
196
+ return new PerpError("RATE_LIMITED", `Pacifica ${what} not sent: rate-limit credits nearly exhausted (${this.label}), window refreshes in ${Math.ceil(wait / 1000)} s`, {
197
+ exchange: "pacifica", sent: false, retryAfterMs: Math.ceil(wait),
198
+ remediation: `Nothing was sent. Retry after ${Math.ceil(wait / 1000)} s; other perp-cli processes on this IP share the same Pacifica quota — reduce request frequency or use an API Config Key (300 credits / 60 s)`,
199
+ });
200
+ }
201
+ /**
202
+ * Record a response's headers (any status). No / malformed / cached headers → unchanged. Within
203
+ * the live window an observation with a higher `r` (a late response from before others landed)
204
+ * or a different `q` (another quota: account vs IP) is dropped.
205
+ */
206
+ observe(res) {
207
+ const info = pacificaRateLimitOf(res);
208
+ if (info === null)
209
+ return null;
210
+ const live = this.resetWaitMs() !== null && this.obs !== null ? this.obs : null;
211
+ if (live !== null) {
212
+ if (info.remaining > live.remaining)
213
+ return null;
214
+ if (live.quota !== null && info.quota !== null && info.quota !== live.quota)
215
+ return null;
216
+ }
217
+ this.obs = { remaining: info.remaining, resetInMs: clampResetMs(info.resetSec * 1000, info.windowSec), observedAt: this.now(), quota: info.quota };
218
+ return info;
219
+ }
220
+ /**
221
+ * HTTP 429 → RATE_LIMITED (structural, by status). retryAfterMs = `Retry-After`, else the
222
+ * ratelimit reset `t`, else absent (clamped to the window). The quota is recorded as exhausted.
223
+ *
224
+ * `sent: false` ("not executed") is OUR reading of the docs — "When credits are exhausted,
225
+ * requests return HTTP 429" — i.e. the request is refused before processing. Pacifica does not
226
+ * state explicitly that a 429'd write was not executed.
227
+ */
228
+ rateLimitedError(what, res, body) {
229
+ const info = pacificaRateLimitOf(res);
230
+ const fromHeader = parseRetryAfterMs(header(res, "retry-after"));
231
+ const raw = fromHeader !== null ? fromHeader : info !== null ? info.resetSec * 1000 : null;
232
+ const retryAfterMs = raw !== null ? clampResetMs(raw, info !== null ? info.windowSec : null) : null;
233
+ if (retryAfterMs !== null)
234
+ this.obs = { remaining: 0, resetInMs: retryAfterMs, observedAt: this.now(), quota: info !== null ? info.quota : null };
235
+ return new PerpError("RATE_LIMITED", `Pacifica ${what} rate limited (HTTP 429); nothing was executed: ${body.slice(0, 200)}`, {
236
+ exchange: "pacifica", httpStatus: 429, sent: false,
237
+ ...(retryAfterMs !== null ? { retryAfterMs } : {}),
238
+ remediation: `${retryAfterMs !== null ? `Retry after ${Math.ceil(retryAfterMs / 1000)} s` : "Wait for the 60 s window to refresh, then retry"}; other perp-cli processes on this IP share the same Pacifica quota — reduce request frequency or use an API Config Key (300 credits / 60 s)`,
239
+ });
240
+ }
241
+ }
242
+ /** A Pacifica RATE_LIMITED that sent nothing (pre-send throttle or HTTP 429): a definite refusal. */
243
+ export function isPacificaRateLimited(e) {
244
+ return e instanceof PerpError && e.structured.code === "RATE_LIMITED"
245
+ && e.structured.details?.exchange === "pacifica" && e.structured.details?.sent === false;
246
+ }
247
+ const limiters = new Map();
248
+ /**
249
+ * The process-local limiter for a REST base URL + credential tier (no key = IP quota) + request
250
+ * kind. Signed POSTs are kept apart from unsigned GETs: credits are shared per main account and
251
+ * scale with its fee tier, so signed responses can report an account quota (e.g. q=3000) while
252
+ * keyless GETs report the IP quota (q=1000).
253
+ */
254
+ export function pacificaRateLimiter(baseUrl, apiKey, kind) {
255
+ const tier = apiKey ? `key:${createHash("sha256").update(apiKey).digest("hex").slice(0, 12)}` : "ip";
256
+ const key = `${baseUrl}|${tier}|${kind}`;
257
+ let l = limiters.get(key);
258
+ if (l === undefined) {
259
+ l = new PacificaRateLimiter(`${baseUrl} ${apiKey ? "API Config Key" : "IP"} ${kind} quota`);
260
+ limiters.set(key, l);
261
+ }
262
+ return l;
263
+ }
264
+ /** Test hook: forget every limiter's state. */
265
+ export function resetPacificaRateLimiters() {
266
+ limiters.clear();
267
+ }
@@ -71,7 +71,8 @@ export interface TPSLParams {
71
71
  stop_price: string;
72
72
  };
73
73
  }
74
- export type BatchActionType = "Create" | "Cancel";
74
+ /** docs rest-api/orders/batch-order `type` (case sensitive; not the signing operation type). */
75
+ export type BatchActionType = "Create" | "CreateMarket" | "Cancel" | "Edit" | "SetPositionTpsl" | "CancelStopOrder";
75
76
  export interface BatchAction {
76
77
  type: BatchActionType;
77
78
  data: Record<string, unknown>;
@@ -1,4 +1,5 @@
1
1
  import { assertCanClosePositions } from "./exchanges/trading-v2.js";
2
+ import { getVenue, requireRole, tryResolveVenueId } from "./exchanges/venue.js";
2
3
  import { symbolMatch } from "./utils.js";
3
4
  import { logExecution } from "./execution-log.js";
4
5
  import { randomUUID } from "crypto";
@@ -83,6 +84,10 @@ export async function executePlan(adapter, plan, opts) {
83
84
  const completedSteps = new Map();
84
85
  const dryRun = opts?.dryRun ?? false;
85
86
  const log = opts?.log ?? (() => { });
87
+ // A venue without the trading role (TxFlow, release-gated RISEx) runs no plan — dry-run included.
88
+ const venueId = tryResolveVenueId(adapter.name);
89
+ if (venueId !== null)
90
+ requireRole(getVenue(venueId), "trading", "plan");
86
91
  // A plan that places orders must be able to close them (rollback / close_position are
87
92
  // reduce-only): refuse before the first step on a venue that cannot.
88
93
  if (!dryRun && plan.steps.some(s => PLAN_ORDER_ACTIONS.has(s.action))) {
@@ -155,7 +160,7 @@ export async function executePlan(adapter, plan, opts) {
155
160
  const onFailure = step.onFailure ?? "abort";
156
161
  if (ORDER_ACTIONS.has(step.action) && isOutcomeUnknown(err)) {
157
162
  // The order may have been placed: never treat it as failed, never resubmit or roll it
158
- // back. Stop the plan and quarantine the symbol (design §12.3).
163
+ // back. Stop the plan and quarantine the symbol.
159
164
  const sym = String(step.params.symbol ?? "").toUpperCase();
160
165
  log(`[plan:${planId}] Step ${step.id} (${step.action}): OUTCOME UNKNOWN — ${message}`);
161
166
  quarantine(sym, `step ${step.id} (${step.action}) outcome unknown: ${message}`);
@@ -300,7 +305,7 @@ async function executeStep(adapter, step, log, placed) {
300
305
  const closeSide = pos.side === "long" ? "sell" : "buy";
301
306
  // Size from the live position (abs), reduce-only: a stale / repeated close can never open
302
307
  // or flip a position — the venue rejects it instead. Fill measured (ORDER_PENDING when unconfirmed).
303
- // A partial close (venue-final IOC partial fills are accepted, §13.10 #4) gets ONE residual
308
+ // A partial close (venue-final IOC partial fills are accepted) gets ONE residual
304
309
  // close (closePerpVerified); a residual still open fails the step with ORDER_PARTIAL.
305
310
  const liveSize = pos.size.replace(/^-/, "");
306
311
  const c = await closePerpVerified(adapter, adapter.name, pos.symbol, closeSide, liveSize);
package/dist/rebalance.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { PerpError } from "./errors.js";
2
2
  import { getVenue, tryResolveVenueId } from "./exchanges/venue.js";
3
3
  import "./exchanges/venues/index.js";
4
- /** Settlement chains with no withdraw → bridge → deposit route in this CLI (design §10.10). */
4
+ /** Settlement chains with no withdraw → bridge → deposit route in this CLI. */
5
5
  const NO_ROUTE_CHAINS = new Set(["robinhood", "offchain"]);
6
6
  /** Throws NO_BRIDGE_ROUTE when the venue's funds live where no bridge route exists. */
7
7
  export function assertBridgeRoute(exchange) {
package/dist/risk.d.ts CHANGED
@@ -52,7 +52,8 @@ export interface RiskAssessment {
52
52
  marginUtilization: number;
53
53
  largestPositionUsd: number;
54
54
  maxLeverageUsed: number;
55
- minLiquidationDistancePct: number;
55
+ /** null = no position with a known liquidation price (no positions, or none reported) — never a -1 sentinel */
56
+ minLiquidationDistancePct: number | null;
56
57
  };
57
58
  liquidationDistances: LiquidationDistanceInfo[];
58
59
  limits: RiskLimits;
package/dist/risk.js CHANGED
@@ -159,9 +159,9 @@ export function assessRisk(balances, positions, limits) {
159
159
  }
160
160
  // Check liquidation distances
161
161
  const liquidationDistances = getLiquidationDistances(positions, lim);
162
- let minLiquidationDistancePct = Infinity;
162
+ let minLiquidationDistancePct = null;
163
163
  for (const ld of liquidationDistances) {
164
- if (ld.distancePct < minLiquidationDistancePct) {
164
+ if (minLiquidationDistancePct === null || ld.distancePct < minLiquidationDistancePct) {
165
165
  minLiquidationDistancePct = ld.distancePct;
166
166
  }
167
167
  if (ld.distancePct < lim.minLiquidationDistance) {
@@ -174,8 +174,6 @@ export function assessRisk(balances, positions, limits) {
174
174
  });
175
175
  }
176
176
  }
177
- if (minLiquidationDistancePct === Infinity)
178
- minLiquidationDistancePct = -1; // no positions
179
177
  // Determine overall risk level
180
178
  let level = "low";
181
179
  if (violations.some(v => v.severity === "critical"))
@@ -168,7 +168,36 @@ export interface QfexAgentMeta {
168
168
  approvedAt: string;
169
169
  source: "imported";
170
170
  }
171
- export type AnyAgentMeta = AgentMeta | OndoAgentMeta | ArcusAgentMeta | QfexAgentMeta;
171
+ /**
172
+ * RISEx session signer (EVM key). Registered by the master with RegisterSigner (+ the signer's
173
+ * VerifySigner) and then restricted ON-CHAIN to the trading permission (DisablePermission for every
174
+ * other permission, signed by the signer and sent by the master). The private key lives only in OWS wallet `agentWalletName`
175
+ * (SSOT Rule #3).
176
+ */
177
+ export interface RisexAgentMeta {
178
+ kind: "risex";
179
+ agentName: string;
180
+ agentWalletName: string;
181
+ /** Signer (session key) address. */
182
+ signerAddress: `0x${string}`;
183
+ /** The RISEx account (master wallet address) the signer trades for. */
184
+ accountAddress: `0x${string}`;
185
+ /** ISO timestamp of the on-chain expiration signed in RegisterSigner. */
186
+ expiresAt: string;
187
+ /**
188
+ * On-chain permission state proven after registration (Authorization.hasPermission):
189
+ * trading-only = only the router permission enabled. Never written from a guess.
190
+ */
191
+ permissions: {
192
+ tradingOnly: boolean;
193
+ disabled: number[];
194
+ verifiedAt: string | null;
195
+ };
196
+ network: "mainnet" | "testnet";
197
+ approvedAt: string;
198
+ source: "approved";
199
+ }
200
+ export type AnyAgentMeta = AgentMeta | OndoAgentMeta | ArcusAgentMeta | QfexAgentMeta | RisexAgentMeta;
172
201
  export interface AgentsByExchange {
173
202
  aster?: Record<string, AgentMeta>;
174
203
  hyperliquid?: Record<string, AgentMeta>;
@@ -178,6 +207,7 @@ export interface AgentsByExchange {
178
207
  ondo?: Record<string, OndoAgentMeta>;
179
208
  arcus?: Record<string, ArcusAgentMeta>;
180
209
  qfex?: Record<string, QfexAgentMeta>;
210
+ risex?: Record<string, RisexAgentMeta>;
181
211
  }
182
212
  export interface Settings {
183
213
  /** Default exchange when -e flag is omitted */
@@ -203,8 +233,7 @@ export interface Settings {
203
233
  /** OWS-native agent wallets registered per exchange (Phase 2a: Aster only) */
204
234
  agents?: AgentsByExchange;
205
235
  /**
206
- * Per-venue slippage bound override (basis points) for market-type orders
207
- * (design §13.2). Absent key = use `defaultSlippageBps`.
236
+ * Per-venue slippage bound override (basis points) for market-type orders. Absent key = use `defaultSlippageBps`.
208
237
  * Resolved only by resolveSlippage() (src/exchanges/slippage.ts).
209
238
  */
210
239
  slippageBps: {
@@ -17,7 +17,7 @@ export declare class OwsEvmSigner implements EvmSigner {
17
17
  }>>, value: Record<string, unknown>): Promise<string>;
18
18
  /**
19
19
  * Sign an unsigned EVM transaction (on-chain txs sent by the master wallet — Lighter RH
20
- * deposit, design §13). The digest is computed HERE from the transaction (`unsignedHash`),
20
+ * deposit). The digest is computed HERE from the transaction (`unsignedHash`),
21
21
  * so callers can never feed an arbitrary 32-byte hash to the master key. Returns the
22
22
  * 65-byte 0x signature; the caller attaches it and checks the recovered sender.
23
23
  */
@@ -44,7 +44,7 @@ export class OwsEvmSigner {
44
44
  }
45
45
  /**
46
46
  * Sign an unsigned EVM transaction (on-chain txs sent by the master wallet — Lighter RH
47
- * deposit, design §13). The digest is computed HERE from the transaction (`unsignedHash`),
47
+ * deposit). The digest is computed HERE from the transaction (`unsignedHash`),
48
48
  * so callers can never feed an arbitrary 32-byte hash to the master key. Returns the
49
49
  * 65-byte 0x signature; the caller attaches it and checks the recovered sender.
50
50
  */
@@ -16,7 +16,7 @@
16
16
  * - Saves 5-50+ bps vs raw market orders on thin books
17
17
  * - Predictable execution price
18
18
  *
19
- * Order-outcome typing (design §12.3): every failure BEFORE the IOC is sent (orderbook read,
19
+ * Order-outcome typing: every failure BEFORE the IOC is sent (orderbook read,
20
20
  * empty book, unusable price), an explicit venue rejection embedded in the response, and an
21
21
  * explicit IOC no-match (nothing filled, nothing rests) throw a definite ORDER_REJECTED
22
22
  * (orderNotSent), so placePerpVerified does not quarantine a leg that never traded. Anything
@@ -16,7 +16,7 @@
16
16
  * - Saves 5-50+ bps vs raw market orders on thin books
17
17
  * - Predictable execution price
18
18
  *
19
- * Order-outcome typing (design §12.3): every failure BEFORE the IOC is sent (orderbook read,
19
+ * Order-outcome typing: every failure BEFORE the IOC is sent (orderbook read,
20
20
  * empty book, unusable price), an explicit venue rejection embedded in the response, and an
21
21
  * explicit IOC no-match (nothing filled, nothing rests) throw a definite ORDER_REJECTED
22
22
  * (orderNotSent), so placePerpVerified does not quarantine a leg that never traded. Anything
@@ -13,7 +13,7 @@ export interface FundingArbParams {
13
13
  maxDrawdown?: number;
14
14
  }
15
15
  /**
16
- * `perp strategy funding-arb` loop. With autoExecute (design §12.3):
16
+ * `perp strategy funding-arb` loop. With autoExecute:
17
17
  * - entry = openPerpPairVerified (long, then the short sized to the long's VERIFIED fill;
18
18
  * definite short failure → reduce-only rollback; unknown outcome → quarantine). A lot-size
19
19
  * rejection is never resubmitted blindly: the long is re-sized only when it was definitely
@@ -72,7 +72,7 @@ async function fetchAllRates() {
72
72
  }
73
73
  // annualize: use computeAnnualSpread from funding.ts for cross-exchange comparison
74
74
  /**
75
- * `perp strategy funding-arb` loop. With autoExecute (design §12.3):
75
+ * `perp strategy funding-arb` loop. With autoExecute:
76
76
  * - entry = openPerpPairVerified (long, then the short sized to the long's VERIFIED fill;
77
77
  * definite short failure → reduce-only rollback; unknown outcome → quarantine). A lot-size
78
78
  * rejection is never resubmitted blindly: the long is re-sized only when it was definitely
@@ -12,7 +12,7 @@ export async function validateTrade(adapter, params) {
12
12
  const checks = [];
13
13
  const warnings = [];
14
14
  const sym = params.symbol.toUpperCase();
15
- // Liquidity threshold = the bound a market order on this venue would carry (design §13.10 #14).
15
+ // Liquidity threshold = the bound a market order on this venue would carry.
16
16
  const maxSlippage = resolveAdapterSlippage(adapter, null).bps / 10_000;
17
17
  // Fetch market list, balance, and positions in parallel.
18
18
  // Defer the orderbook call until symbol membership is confirmed so invalid
@@ -264,7 +264,7 @@ export async function enforceOrderRisk(adapter, p) {
264
264
  const markets = await adapter.getMarkets();
265
265
  const m = markets.find((mk) => symbolMatch(mk.symbol, p.symbol));
266
266
  if (!m) {
267
- throw new PerpError("SYMBOL_NOT_FOUND", `${p.symbol} not found on ${adapter.name}`, { exchange: adapter.name });
267
+ throw new PerpError("SYMBOL_NOT_FOUND", `${p.symbol} not found on ${adapter.name}`, { exchange: adapter.name, remediation: `List markets: perp -e ${adapter.name} market list` });
268
268
  }
269
269
  price = Number(m.markPrice);
270
270
  }
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import type { OrderResult } from "./exchanges/venue-types.js";
2
2
  export declare function symbolMatch(candidate: string, target: string): boolean;
3
3
  export declare function formatUsd(value: string | number): string;
4
+ /**
5
+ * Symbol argument as typed, normalized for display and lookup: upper case, except a HIP-3 dex
6
+ * prefix, which is lower case on the venue ("xyz:xyz100" / "XYZ:XYZ100" → "xyz:XYZ100").
7
+ */
8
+ export declare function normalizeSymbolArg(symbol: string): string;
4
9
  export declare function formatPnl(value: string | number): string;
5
10
  export declare function formatPercent(value: string | number): string;
6
11
  export declare function makeTable(head: string[], rows: string[][]): string;
@@ -26,6 +31,10 @@ export interface ApiResponse<T = unknown> {
26
31
  exchange?: string;
27
32
  timestamp: string;
28
33
  duration_ms?: number;
34
+ skippedRows?: {
35
+ count: number;
36
+ reason: string;
37
+ };
29
38
  };
30
39
  }
31
40
  /** Wrap successful result in standard envelope */
@@ -50,18 +59,18 @@ export declare function withJsonErrors<T>(isJson: boolean, fn: () => Promise<T>,
50
59
  /**
51
60
  * Central CLI error printer. JSON: the structured envelope incl. details and
52
61
  * remediation. Human: the uniform "not supported" line for CAPABILITY_UNSUPPORTED
53
- * (design §13.6) — `✗ <Venue>: <feature> not supported — <reason>. Alternative: <remediation>` —
62
+ * — `✗ <Venue>: <feature> not supported — <reason>. Alternative: <remediation>` —
54
63
  * otherwise `Error: <message>` plus the remediation.
55
64
  */
56
65
  export declare function printCliError(err: unknown, isJson: boolean, exchange?: string): void;
57
66
  /**
58
- * JSON payload of an order result (design §13.7): the uniform OrderResult (+ execution)
67
+ * JSON payload of an order result: the uniform OrderResult (+ execution)
59
68
  * with the venue's raw response duplicated under `legacy` for one release.
60
69
  */
61
70
  export declare function orderResultJson(r: OrderResult & {
62
71
  remediation?: string | null;
63
72
  }, extra?: Record<string, unknown>): Record<string, unknown>;
64
- /** Human lines for an order result — one format for every venue (design §13.7). */
73
+ /** Human lines for an order result — one format for every venue. */
65
74
  export declare function formatOrderResult(r: OrderResult & {
66
75
  remediation?: string | null;
67
76
  }): string[];
package/dist/utils.js CHANGED
@@ -30,6 +30,14 @@ export function formatUsd(value) {
30
30
  maximumFractionDigits: maxDecimals,
31
31
  });
32
32
  }
33
+ /**
34
+ * Symbol argument as typed, normalized for display and lookup: upper case, except a HIP-3 dex
35
+ * prefix, which is lower case on the venue ("xyz:xyz100" / "XYZ:XYZ100" → "xyz:XYZ100").
36
+ */
37
+ export function normalizeSymbolArg(symbol) {
38
+ const i = symbol.indexOf(":");
39
+ return i === -1 ? symbol.toUpperCase() : `${symbol.slice(0, i).toLowerCase()}:${symbol.slice(i + 1).toUpperCase()}`;
40
+ }
33
41
  export function formatPnl(value) {
34
42
  const num = Number(value);
35
43
  if (isNaN(num))
@@ -235,7 +243,7 @@ export async function withJsonErrors(isJson, fn, exchange) {
235
243
  /**
236
244
  * Central CLI error printer. JSON: the structured envelope incl. details and
237
245
  * remediation. Human: the uniform "not supported" line for CAPABILITY_UNSUPPORTED
238
- * (design §13.6) — `✗ <Venue>: <feature> not supported — <reason>. Alternative: <remediation>` —
246
+ * — `✗ <Venue>: <feature> not supported — <reason>. Alternative: <remediation>` —
239
247
  * otherwise `Error: <message>` plus the remediation.
240
248
  */
241
249
  export function printCliError(err, isJson, exchange) {
@@ -262,13 +270,13 @@ export function printCliError(err, isJson, exchange) {
262
270
  }
263
271
  }
264
272
  /**
265
- * JSON payload of an order result (design §13.7): the uniform OrderResult (+ execution)
273
+ * JSON payload of an order result: the uniform OrderResult (+ execution)
266
274
  * with the venue's raw response duplicated under `legacy` for one release.
267
275
  */
268
276
  export function orderResultJson(r, extra) {
269
277
  return { ...r, ...extra, legacy: r.venueRaw };
270
278
  }
271
- /** Human lines for an order result — one format for every venue (design §13.7). */
279
+ /** Human lines for an order result — one format for every venue. */
272
280
  export function formatOrderResult(r) {
273
281
  const lines = [];
274
282
  const statusColor = r.status === "filled" ? chalk.green
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Venue connectivity probe — the one implementation behind `perp health`,
3
+ * `perp portfolio --health` and the MCP `health_check` tool.
4
+ *
5
+ * Pacifica / Hyperliquid / Lighter / Aster / QFEX / GRVT: a REST ping (status = HTTP status, 0 = no
6
+ * response). Every other venue — including read-only (market-data-only) venues such as TxFlow —
7
+ * is probed with its public market-data read (status null). A venue with no environment on the
8
+ * selected network (TxFlow has no testnet) is not probed and is listed in `skipped`.
9
+ *
10
+ * The ping helpers (shared-api) read the shared API URLs, which follow the network the caller
11
+ * selected (setSharedApiNetwork); Aster / QFEX / market-data probes take `network` directly.
12
+ */
13
+ import type { Network, VenueId } from "./exchanges/venue-types.js";
14
+ import "./exchanges/venues/index.js";
15
+ export interface VenueProbe {
16
+ venue: VenueId;
17
+ ok: boolean;
18
+ latencyMs: number;
19
+ /** HTTP status of a REST ping (0 = no response); null for a market-data probe. */
20
+ status: number | null;
21
+ /** null when ok. */
22
+ error: string | null;
23
+ probe: "ping" | "markets";
24
+ /** "unofficial" = the venue is read through an undocumented API (VenueDefinition.apiStatus). */
25
+ apiStatus: "official" | "unofficial";
26
+ }
27
+ export interface VenueHealth {
28
+ probes: VenueProbe[];
29
+ /** Venues requested but not probed: no environment on `network`. */
30
+ skipped: Array<{
31
+ venue: VenueId;
32
+ reason: string;
33
+ }>;
34
+ allHealthy: boolean;
35
+ }
36
+ /**
37
+ * Probe `venueIds` (default: every registered venue) on `network`, in parallel. Never throws for a
38
+ * venue failure — each failure is a probe with ok:false and its message.
39
+ */
40
+ export declare function probeVenueHealth(network: Network, venueIds?: readonly string[]): Promise<VenueHealth>;