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
@@ -94,11 +94,13 @@ export function marginEntryBlocks(check, minMarginPct = 30) {
94
94
  * @param maxSlippagePct - Max acceptable slippage (default: 0.3%)
95
95
  * @returns USD size per leg, or 0 if not viable
96
96
  */
97
- export async function computeAutoSize(longAdapter, shortAdapter, symbol, maxSlippagePct = 0.3) {
97
+ export async function computeAutoSize(longAdapter, shortAdapter, symbol, maxSlippagePct = 0.3,
98
+ /** The short venue's own symbol when it differs (kPEPE ↔ 1000PEPE); default `symbol`. */
99
+ shortSymbol = symbol) {
98
100
  // Step 1: Get orderbooks from both sides
99
101
  const [longOB, shortOB, longBal, shortBal] = await Promise.all([
100
102
  longAdapter.getOrderbook(symbol),
101
- shortAdapter.getOrderbook(symbol),
103
+ shortAdapter.getOrderbook(shortSymbol),
102
104
  longAdapter.getBalance(),
103
105
  shortAdapter.getBalance(),
104
106
  ]);
@@ -44,11 +44,11 @@ async function pollArbData() {
44
44
  let exchangeStatus = {};
45
45
  let exchangeErrors = {};
46
46
  try {
47
- const { fetchAllFundingRates } = await import("../funding/rates.js");
47
+ const { fetchAllFundingRates, isRankedComparison } = await import("../funding/rates.js");
48
48
  const snapshot = await fetchAllFundingRates({ minSpread: 5 });
49
49
  exchangeStatus = snapshot.exchangeStatus;
50
50
  exchangeErrors = snapshot.exchangeErrors;
51
- for (const sym of snapshot.symbols.slice(0, 20)) {
51
+ for (const sym of snapshot.symbols.filter(isRankedComparison).slice(0, 20)) {
52
52
  opportunities.push({
53
53
  symbol: sym.symbol,
54
54
  longExchange: sym.longExchange,
@@ -100,7 +100,7 @@ async function pollArbData() {
100
100
  const row = {
101
101
  base,
102
102
  dexes: assets.map((a) => {
103
- // Venue ref: "hyperliquid" for native, "hl:<dex>" for HIP-3 (same basis, §10.1)
103
+ // Venue ref: "hyperliquid" for native, "hl:<dex>" for HIP-3 (same basis)
104
104
  const ann = annualizeRate(a.fundingRate, a.dex === "hl" ? "hyperliquid" : `hl:${a.dex}`);
105
105
  return { dex: a.dex, rate: a.fundingRate, annualizedPct: ann, markPrice: a.markPrice, oi: a.openInterest };
106
106
  }),
@@ -235,6 +235,8 @@ function connect() {
235
235
  // ── Helpers ──
236
236
  function fmt(v, d=2) { const n=Number(v)||0; return n.toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}); }
237
237
  function usdOrDash(v, d=2) { return v==null ? '-' : '$'+fmt(v,d); }
238
+ // Market row cells: prices in the row's quote currency (QFEX SAMSUNG-KRW: "220,150.00 KRW", never $); OI is BASE units (never $); funding null = "-" (not 0%).
239
+ function marketCells(m) { const q=(typeof m.quoteAsset==='string'&&m.quoteAsset!=='USD')?escHtml(m.quoteAsset):null; const px=(v,d=2)=>v==null?'-':(q===null?'$'+fmt(v,d):fmt(v,d)+' '+q); const vol=m.volume24h==null&&typeof m.volume24hQuote==='string'?px(m.volume24hQuote,0):usdOrDash(m.volume24h,0); const base=escHtml(String(m.symbol).split(':').pop().replace(/-PERP$/i,'')); return { mark: px(m.markPrice), index: px(m.indexPrice), funding: m.fundingRate==null?'-':(Number(m.fundingRate)*100).toFixed(4)+'%', volume: vol, oi: m.openInterest==null?'-':Number(m.openInterest).toLocaleString('en-US',{maximumFractionDigits:4})+' '+base }; }
238
240
  function pnlClass(v) { return Number(v)>=0?'pnl-pos':'pnl-neg'; }
239
241
  function pnlSign(v) { const n=Number(v)||0; return n>=0?'+$'+fmt(Math.abs(n)):'-$'+fmt(Math.abs(n)); }
240
242
  function rateClass(v) { const n=Number(v)||0; return n>0?'rate-positive':n<0?'rate-negative':'rate-neutral'; }
@@ -243,7 +245,8 @@ function spreadClass(v) { return v>=50?'spread-high':v>=20?'spread-mid':'spread-
243
245
  const NA = '<span style="color:var(--yellow,#e5c07b)">unavailable</span>';
244
246
  function usdOrNA(v, d=2) { return v==null ? NA : '$'+fmt(v,d); }
245
247
  function pnlOrNA(v) { return v==null ? NA : pnlSign(v); }
246
- function escHtml(v) { return String(v).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[c]); }
248
+ // Every venue-supplied string (symbol, quoteAsset, dex, exchange, order fields) goes through this before innerHTML.
249
+ function escHtml(v) { return String(v).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]); }
247
250
 
248
251
  // ── Portfolio Rendering ──
249
252
  function renderTotals(t) {
@@ -325,16 +328,16 @@ function renderPanels(exchanges) {
325
328
  const notional = p.markPrice == null ? null : Math.abs(Number(p.size)) * Number(p.markPrice);
326
329
  const margin = notional == null ? null : p.leverage > 0 ? notional / p.leverage : notional;
327
330
  const roe = margin != null && margin > 0 && p.unrealizedPnl != null ? (Number(p.unrealizedPnl) / margin * 100) : null;
328
- return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${p.side}">\${p.side.toUpperCase()}</td><td>\${p.size}</td><td>\${usdOrNA(p.entryPrice)}</td><td>\${usdOrNA(p.markPrice)}</td><td>\${(p.liquidationPrice==null||p.liquidationPrice==='N/A')?'-':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">\${usdOrNA(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlOrNA(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe==null?NA:(roe>=0?'+':'')+fmt(roe,1)+'%'}</td><td>\${p.leverage}x</td></tr>\`;
331
+ return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${escHtml(dex)}</td>\`:''}<td>\${escHtml(sym)}</td><td class="side-\${escHtml(p.side)}">\${escHtml(String(p.side).toUpperCase())}</td><td>\${escHtml(p.size)}</td><td>\${usdOrNA(p.entryPrice)}</td><td>\${usdOrNA(p.markPrice)}</td><td>\${(p.liquidationPrice==null||p.liquidationPrice==='N/A')?'-':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">\${usdOrNA(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlOrNA(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe==null?NA:(roe>=0?'+':'')+fmt(roe,1)+'%'}</td><td>\${p.leverage}x</td></tr>\`;
329
332
  }).join('')}</tbody></table>\`:(posKnown?'<div class="empty-msg">No open positions</div>':'<div class="empty-msg">Positions '+NA+'</div>')}
330
333
  <div class="section-title">Open Orders (\${ordKnown ? filteredOrders.length : '?'})</div>
331
334
  \${filteredOrders.length?\`<table><thead><tr>\${isHL?'<th>DEX</th>':''}<th>Symbol</th><th>Side</th><th>Type</th><th>Price</th><th>Size</th><th>Filled</th><th>Status</th></tr></thead><tbody>\${filteredOrders.map(o=>{
332
335
  const dex = getDex(o.symbol);
333
336
  const sym = o.symbol.includes(':') ? o.symbol.split(':').slice(1).join(':') : o.symbol;
334
- return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${o.side}">\${o.side.toUpperCase()}</td><td>\${o.type}</td><td>$\${fmt(o.price)}</td><td>\${o.size}</td><td>\${o.filled}</td><td>\${o.status}</td></tr>\`;
337
+ return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${escHtml(dex)}</td>\`:''}<td>\${escHtml(sym)}</td><td class="side-\${escHtml(o.side)}">\${escHtml(String(o.side).toUpperCase())}</td><td>\${escHtml(o.type)}</td><td>$\${fmt(o.price)}</td><td>\${escHtml(o.size)}</td><td>\${escHtml(o.filled)}</td><td>\${escHtml(o.status)}</td></tr>\`;
335
338
  }).join('')}</tbody></table>\`:(ordKnown?'<div class="empty-msg">No open orders</div>':'<div class="empty-msg">Open orders '+NA+'</div>')}
336
339
  <div class="section-title">Markets (Top 10)</div>
337
- \${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);return\`<tr><td>\${m.symbol}</td><td>$\${fmt(m.markPrice)}</td><td>\${usdOrDash(m.indexPrice)}</td><td class="\${rateClass(fr)}">\${(fr*100).toFixed(4)}%</td><td>\${usdOrDash(m.volume24h,0)}</td><td>\${usdOrDash(m.openInterest,0)}</td><td>\${m.maxLeverage==null?'-':m.maxLeverage+'x'}</td></tr>\`;}).join('')}</tbody></table>\`:'<div class="empty-msg">No market data</div>'}
340
+ \${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);const c=marketCells(m);return\`<tr><td>\${escHtml(m.symbol)}</td><td>\${c.mark}</td><td>\${c.index}</td><td class="\${rateClass(fr)}">\${c.funding}</td><td>\${c.volume}</td><td>\${c.oi}</td><td>\${m.maxLeverage==null?'-':m.maxLeverage+'x'}</td></tr>\`;}).join('')}</tbody></table>\`:'<div class="empty-msg">No market data</div>'}
338
341
  </div>\`;
339
342
  }).join('');
340
343
 
@@ -419,7 +422,7 @@ function renderArb() {
419
422
  const barColor = o.spreadAnnual>=50?'var(--green)':o.spreadAnnual>=20?'var(--yellow)':'var(--muted)';
420
423
  const bestMark = o.rates.reduce((best,r) => r.markPrice>0?r.markPrice:best, 0);
421
424
  return \`<tr>
422
- <td style="font-weight:600">\${o.symbol}</td>
425
+ <td style="font-weight:600">\${escHtml(o.symbol)}</td>
423
426
  <td>
424
427
  <div class="\${spreadClass(o.spreadAnnual)}">\${fmt(o.spreadAnnual,1)}%</div>
425
428
  <div class="spread-bar"><div class="spread-bar-fill" style="width:\${barW}%;background:\${barColor}"></div></div>
@@ -514,7 +517,7 @@ function renderEvents() {
514
517
  if (!events.length) { el.innerHTML='<div class="empty-msg">Waiting for events...</div>'; return; }
515
518
  el.innerHTML = events.map(e => {
516
519
  const cls = e.isCrit?'crit':e.isWarn?'warn':'';
517
- return \`<div class="event"><span class="event-time">\${e.time}</span> <span class="event-type \${cls}">[\${e.type}]</span> <span>\${e.exchange}</span> <span style="color:var(--muted)">\${JSON.stringify(e.data).slice(0,120)}</span></div>\`;
520
+ return \`<div class="event"><span class="event-time">\${e.time}</span> <span class="event-type \${cls}">[\${e.type}]</span> <span>\${escHtml(e.exchange)}</span> <span style="color:var(--muted)">\${escHtml(JSON.stringify(e.data).slice(0,120))}</span></div>\`;
518
521
  }).join('');
519
522
  }
520
523
 
package/dist/errors.d.ts CHANGED
@@ -158,7 +158,7 @@ export declare const ERROR_CODES: {
158
158
  readonly status: 202;
159
159
  readonly retryable: false;
160
160
  };
161
- /** The order was definitely NOT placed: the venue rejected it, or it failed before it was sent (design §12.3). */
161
+ /** The order was definitely NOT placed: the venue rejected it, or it failed before it was sent. */
162
162
  readonly ORDER_REJECTED: {
163
163
  readonly code: "ORDER_REJECTED";
164
164
  readonly status: 422;
@@ -173,6 +173,29 @@ export declare const ERROR_CODES: {
173
173
  readonly status: 422;
174
174
  readonly retryable: false;
175
175
  };
176
+ /** A venue frame could not be parsed or classified (never silently dropped). */
177
+ readonly WS_PROTOCOL_ERROR: {
178
+ readonly code: "WS_PROTOCOL_ERROR";
179
+ readonly status: 502;
180
+ readonly retryable: false;
181
+ };
182
+ readonly WS_AUTH_FAILED: {
183
+ readonly code: "WS_AUTH_FAILED";
184
+ readonly status: 401;
185
+ readonly retryable: false;
186
+ };
187
+ /** A subscription / connection budget (venue limit or caller budget) would be exceeded. */
188
+ readonly SUBSCRIPTION_LIMIT: {
189
+ readonly code: "SUBSCRIPTION_LIMIT";
190
+ readonly status: 400;
191
+ readonly retryable: false;
192
+ };
193
+ /** An event stream's bounded buffer overflowed; the subscription was closed. */
194
+ readonly STREAM_OVERFLOW: {
195
+ readonly code: "STREAM_OVERFLOW";
196
+ readonly status: 507;
197
+ readonly retryable: false;
198
+ };
176
199
  readonly EXCHANGE_UNREACHABLE: {
177
200
  readonly code: "EXCHANGE_UNREACHABLE";
178
201
  readonly status: 503;
@@ -297,3 +320,8 @@ export declare function unknownActionRemediation(venue: string, action: string):
297
320
  * and "1e3" are refused). Returns the trimmed string.
298
321
  */
299
322
  export declare function parsePositiveDecimal(raw: string, what: string): string;
323
+ /**
324
+ * A keyless account read (positions, balance, orders, history): the one typed refusal every venue
325
+ * returns — NO_SIGNER_AVAILABLE with the setup remediation (never an UNKNOWN 500).
326
+ */
327
+ export declare function noAccountKeyError(venue: string): PerpError;
package/dist/errors.js CHANGED
@@ -25,7 +25,7 @@ export const ERROR_CODES = {
25
25
  PASSPHRASE_REQUIRED: { code: "PASSPHRASE_REQUIRED", status: 401, retryable: false },
26
26
  INVALID_PASSPHRASE: { code: "INVALID_PASSPHRASE", status: 401, retryable: false },
27
27
  NOT_IMPLEMENTED: { code: "NOT_IMPLEMENTED", status: 501, retryable: false },
28
- // Venue abstraction (docs/DESIGN-venue-abstraction.md §10)
28
+ // Venue abstraction
29
29
  UNKNOWN_VENUE: { code: "UNKNOWN_VENUE", status: 400, retryable: false },
30
30
  TIF_REQUIRED: { code: "TIF_REQUIRED", status: 400, retryable: false },
31
31
  INVALID_TICK: { code: "INVALID_TICK", status: 400, retryable: false },
@@ -36,13 +36,21 @@ export const ERROR_CODES = {
36
36
  ORDER_PARTIAL: { code: "ORDER_PARTIAL", status: 500, retryable: false },
37
37
  /** The order was submitted but its outcome is not confirmed; retrying could double-trade. */
38
38
  ORDER_PENDING: { code: "ORDER_PENDING", status: 202, retryable: false },
39
- /** The order was definitely NOT placed: the venue rejected it, or it failed before it was sent (design §12.3). */
39
+ /** The order was definitely NOT placed: the venue rejected it, or it failed before it was sent. */
40
40
  ORDER_REJECTED: { code: "ORDER_REJECTED", status: 422, retryable: false },
41
41
  /**
42
42
  * The venue explicitly refused a non-order account action (withdraw / transfer / approval) and
43
43
  * executed nothing — distinct from a network / unknown outcome (EXCHANGE_ERROR, UNKNOWN).
44
44
  */
45
45
  ACTION_REJECTED: { code: "ACTION_REJECTED", status: 422, retryable: false },
46
+ // WebSocket library (perp-cli/ws, src/ws/errors.ts keeps a parity-tested copy)
47
+ /** A venue frame could not be parsed or classified (never silently dropped). */
48
+ WS_PROTOCOL_ERROR: { code: "WS_PROTOCOL_ERROR", status: 502, retryable: false },
49
+ WS_AUTH_FAILED: { code: "WS_AUTH_FAILED", status: 401, retryable: false },
50
+ /** A subscription / connection budget (venue limit or caller budget) would be exceeded. */
51
+ SUBSCRIPTION_LIMIT: { code: "SUBSCRIPTION_LIMIT", status: 400, retryable: false },
52
+ /** An event stream's bounded buffer overflowed; the subscription was closed. */
53
+ STREAM_OVERFLOW: { code: "STREAM_OVERFLOW", status: 507, retryable: false },
46
54
  // 5xx - System / transient errors
47
55
  EXCHANGE_UNREACHABLE: { code: "EXCHANGE_UNREACHABLE", status: 503, retryable: true },
48
56
  RATE_LIMITED: { code: "RATE_LIMITED", status: 429, retryable: true, retryAfterMs: 1000 },
@@ -92,6 +100,12 @@ export function classifyError(err, exchange) {
92
100
  const ex = (err.structured.exchange ?? exchange);
93
101
  return ex !== undefined ? { ...err.structured, exchange: ex } : { ...err.structured };
94
102
  }
103
+ // Structural PerpError (perp-cli/ws WsError is a separate leaf class): keep its registered code.
104
+ if (err instanceof Error && isStructuredLike(err)) {
105
+ const s = err.structured;
106
+ const ex = s.exchange ?? exchange;
107
+ return ex !== undefined ? { ...s, exchange: ex } : { ...s };
108
+ }
95
109
  const message = extractErrorMessage(err);
96
110
  const lower = message.toLowerCase();
97
111
  // Margin-specific checks first (before generic "insufficient" catch)
@@ -216,6 +230,14 @@ export function classifyError(err, exchange) {
216
230
  }
217
231
  return { ...ERROR_CODES.UNKNOWN, message };
218
232
  }
233
+ function isStructuredLike(err) {
234
+ const s = err.structured;
235
+ if (s === null || typeof s !== "object")
236
+ return false;
237
+ const { code, message, status, retryable } = s;
238
+ return typeof code === "string" && Object.prototype.hasOwnProperty.call(ERROR_CODES, code)
239
+ && typeof message === "string" && typeof status === "number" && typeof retryable === "boolean";
240
+ }
219
241
  /** Custom error class that carries a structured error code */
220
242
  export class PerpError extends Error {
221
243
  structured;
@@ -235,7 +257,7 @@ export class PerpError extends Error {
235
257
  };
236
258
  }
237
259
  }
238
- // ── Order outcome classification (design §12.3) ──
260
+ // ── Order outcome classification ──
239
261
  /**
240
262
  * Codes that mean "the order was definitely not placed / filled nothing": a validation
241
263
  * or policy failure before sending (incl. PRICE_STALE: no price to build the order), or an
@@ -366,3 +388,13 @@ export function parsePositiveDecimal(raw, what) {
366
388
  }
367
389
  return s;
368
390
  }
391
+ /**
392
+ * A keyless account read (positions, balance, orders, history): the one typed refusal every venue
393
+ * returns — NO_SIGNER_AVAILABLE with the setup remediation (never an UNKNOWN 500).
394
+ */
395
+ export function noAccountKeyError(venue) {
396
+ return new PerpError("NO_SIGNER_AVAILABLE", `${venue}: no account key configured — account data unavailable`, {
397
+ exchange: venue,
398
+ remediation: `Set up an account for ${venue}: perp setup (or perp -e ${venue} wallet show to check)`,
399
+ });
400
+ }
@@ -0,0 +1,8 @@
1
+ export type AddressKind = "evm" | "solana";
2
+ export declare function isEvmAddress(value: string): boolean;
3
+ /** A base58 string that decodes to a 32-byte public key. */
4
+ export declare function isSolanaAddress(value: string): boolean;
5
+ /** Pacifica account / Print order / vault address: base58 32 bytes, else INVALID_PARAMS. */
6
+ export declare function requireSolanaAddress(value: string, field?: string): string;
7
+ /** `address` in the form `kind` expects (trimmed), else INVALID_PARAMS naming the expected form. */
8
+ export declare function requireAddressOfKind(venue: string, kind: AddressKind, address: string): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Public account address formats (keyless reads by address): EVM `0x` + 40 hex (Hyperliquid,
3
+ * Lighter) and base58 Solana public keys (Pacifica). A wrong format is refused with
4
+ * INVALID_PARAMS before any request — the venues answer a foreign address with an empty success
5
+ * (Pacifica `success: true, []`), which would read as "no positions".
6
+ */
7
+ import bs58 from "bs58";
8
+ import { PerpError } from "../errors.js";
9
+ const EVM_RE = /^0x[0-9a-fA-F]{40}$/;
10
+ export function isEvmAddress(value) {
11
+ return EVM_RE.test(value.trim());
12
+ }
13
+ /** A base58 string that decodes to a 32-byte public key. */
14
+ export function isSolanaAddress(value) {
15
+ try {
16
+ return bs58.decode(value.trim()).length === 32;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /** Pacifica account / Print order / vault address: base58 32 bytes, else INVALID_PARAMS. */
23
+ export function requireSolanaAddress(value, field = "address") {
24
+ const a = value.trim();
25
+ if (!isSolanaAddress(a)) {
26
+ throw new PerpError("INVALID_PARAMS", `Not a Solana (base58) ${field}: "${value}"${isEvmAddress(a) ? " — that is an EVM address; Pacifica accounts are Solana addresses" : ""}`, {
27
+ exchange: "pacifica", remediation: "Pass the base58 Solana address you use on Pacifica",
28
+ });
29
+ }
30
+ return a;
31
+ }
32
+ /** `address` in the form `kind` expects (trimmed), else INVALID_PARAMS naming the expected form. */
33
+ export function requireAddressOfKind(venue, kind, address) {
34
+ const a = address.trim();
35
+ if (kind === "evm") {
36
+ if (isEvmAddress(a))
37
+ return a;
38
+ throw new PerpError("INVALID_PARAMS", `${venue} expects an EVM 0x address (0x + 40 hex)${isSolanaAddress(a) ? " — that is a Solana address" : ""}: "${address}"`, {
39
+ exchange: venue, remediation: `Pass the 0x address of the ${venue} account`,
40
+ });
41
+ }
42
+ if (isSolanaAddress(a))
43
+ return a;
44
+ throw new PerpError("INVALID_PARAMS", `${venue} expects a base58 Solana address${isEvmAddress(a) ? " — that is an EVM address" : ""}: "${address}"`, {
45
+ exchange: venue, remediation: `Pass the base58 Solana address of the ${venue} account`,
46
+ });
47
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Arcus adapter — native TradingV2 + legacy ExchangeAdapter shim (design §10.7).
2
+ * Arcus adapter — native TradingV2 + legacy ExchangeAdapter shim.
3
3
  *
4
4
  * Signing: the agent API key is an Ed25519 key in OWS wallet
5
5
  * `meta.agentWalletName` (raw signMessage on the Solana account — no prefix).
@@ -7,12 +7,12 @@
7
7
  * (master creds only, for spot swaps) every perp private call throws
8
8
  * AGENT_NOT_REGISTERED from requireAccount().
9
9
  *
10
- * Order entry rules (docs.arcus.xyz, design §10.10):
10
+ * Order entry rules (docs.arcus.xyz):
11
11
  * - every order carries goodTilTime ≥ 1 month ahead (ARCUS_GOOD_TIL_HORIZON_MS),
12
12
  * body in µs, signed `g` in ns;
13
13
  * - market orders = IOC LIMIT at a protection price derived from mark and an
14
14
  * explicit slippage bound, rejected locally when > 10 % from mark;
15
- * - TIF (design §13.3): common GTC is sent as GTT with goodTil = now +
15
+ * - TIF: common GTC is sent as GTT with goodTil = now +
16
16
  * ARCUS_GOOD_TIL_HORIZON_MS (recorded as execution.expiresAt); IOC / FOK / ALO
17
17
  * are native; a limit order without a TIF is GTC;
18
18
  * - clientId is always sent (orderAck async) and every OrderResult from a
@@ -22,7 +22,7 @@
22
22
  * an `entryTpsl` bundle.
23
23
  */
24
24
  import type { EvmSigner } from "../../signer/interface.js";
25
- import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade } from "../interface.js";
25
+ import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade, FundingHistoryRow } from "../interface.js";
26
26
  import type { ResolvedSlippage } from "../slippage.js";
27
27
  import { type TradingV2 } from "../trading-v2.js";
28
28
  import type { VenueCreateContext, VenueDefinition } from "../venue.js";
@@ -37,6 +37,8 @@ export declare const ARCUS_GOOD_TIL_HORIZON_MS: number;
37
37
  export declare const ARCUS_MAX_PROTECTION_DEVIATION = 0.1;
38
38
  /** clientId max (36) minus the "-tp"/"-sl" suffix of entryTpsl child legs. */
39
39
  export declare const ARCUS_TPSL_CLIENT_ID_MAX = 33;
40
+ /** /v1/trades and /v1/fundingRates take `limit` ≤ 1000 (docs; the venue answered 1000 rows for more). */
41
+ export declare const ARCUS_HISTORY_CAP = 1000;
40
42
  export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
41
43
  readonly name: "arcus";
42
44
  readonly venue: VenueDefinition;
@@ -57,6 +59,12 @@ export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
57
59
  private accountQuery;
58
60
  private loadMarkets;
59
61
  private market;
62
+ /**
63
+ * A market-data read (book / trades / funding / candles) of a listed market that is not ONLINE
64
+ * is refused like `market info` / `market list` treat it (not listed): SYMBOL_NOT_FOUND naming
65
+ * the venue status — the venue answered `[]` or HTTP 404 ORDER_NOT_FOUND for it before.
66
+ */
67
+ private onlineMarket;
60
68
  private symbolOf;
61
69
  /** Protection price: slippage from `ref`, aligned toward crossing on the tier tick, ≤ 10 % from `ref`. */
62
70
  private protectionPrice;
@@ -92,7 +100,7 @@ export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
92
100
  /**
93
101
  * modifyOrder (docs modify-order / authentication).
94
102
  *
95
- * Size: the CLI's `req.size` is the new OPEN (unfilled) size (design §13.4), while
103
+ * Size: the CLI's `req.size` is the new OPEN (unfilled) size, while
96
104
  * Arcus modifies take the order's new TOTAL size, filled quantity included
97
105
  * (OrderRejectionReason MODIFY_SIZE_ALREADY_FILLED: "the order's new TOTAL size,
98
106
  * filled quantity included"). So body `quantity` and signed `q` are
@@ -143,12 +151,8 @@ export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
143
151
  bids: [string, string][];
144
152
  asks: [string, string][];
145
153
  }>;
146
- getRecentTrades(symbol: string, limit?: number): Promise<ExchangeTrade[]>;
147
- getFundingHistory(symbol: string, limit?: number): Promise<{
148
- time: number;
149
- rate: string;
150
- price: string | null;
151
- }[]>;
154
+ getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
155
+ getFundingHistory(symbol: string, limit?: number): Promise<FundingHistoryRow[]>;
152
156
  getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
153
157
  private rawPositions;
154
158
  getBalance(): Promise<ExchangeBalance>;
@@ -182,7 +186,7 @@ export declare class ArcusAdapter implements ExchangeAdapter, TradingV2 {
182
186
  }): Promise<unknown>;
183
187
  private masterSigner;
184
188
  private requireMasterKey;
185
- /** Master EVM signer for on-chain actions outside the Arcus API (spot swaps, design §12.4 #7). */
189
+ /** Master EVM signer for on-chain actions outside the Arcus API (spot swaps). */
186
190
  masterEvmSigner(): Promise<EvmSigner>;
187
191
  /** Sign an unsigned EVM transaction with the master key (digest derived here). Returns a 65-byte 0x signature. */
188
192
  signMasterTransaction(tx: import("ethers").Transaction): Promise<string>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Arcus adapter — native TradingV2 + legacy ExchangeAdapter shim (design §10.7).
2
+ * Arcus adapter — native TradingV2 + legacy ExchangeAdapter shim.
3
3
  *
4
4
  * Signing: the agent API key is an Ed25519 key in OWS wallet
5
5
  * `meta.agentWalletName` (raw signMessage on the Solana account — no prefix).
@@ -7,12 +7,12 @@
7
7
  * (master creds only, for spot swaps) every perp private call throws
8
8
  * AGENT_NOT_REGISTERED from requireAccount().
9
9
  *
10
- * Order entry rules (docs.arcus.xyz, design §10.10):
10
+ * Order entry rules (docs.arcus.xyz):
11
11
  * - every order carries goodTilTime ≥ 1 month ahead (ARCUS_GOOD_TIL_HORIZON_MS),
12
12
  * body in µs, signed `g` in ns;
13
13
  * - market orders = IOC LIMIT at a protection price derived from mark and an
14
14
  * explicit slippage bound, rejected locally when > 10 % from mark;
15
- * - TIF (design §13.3): common GTC is sent as GTT with goodTil = now +
15
+ * - TIF: common GTC is sent as GTT with goodTil = now +
16
16
  * ARCUS_GOOD_TIL_HORIZON_MS (recorded as execution.expiresAt); IOC / FOK / ALO
17
17
  * are native; a limit order without a TIF is GTC;
18
18
  * - clientId is always sent (orderAck async) and every OrderResult from a
@@ -22,6 +22,7 @@
22
22
  * an `entryTpsl` bundle.
23
23
  */
24
24
  import { PerpError } from "../../errors.js";
25
+ import { isArcusOnlineMarket } from "../../ws/arcus-market.js";
25
26
  import { OwsEvmSigner } from "../../signer/ows-evm.js";
26
27
  import { OwsSolanaSigner } from "../../signer/ows-solana.js";
27
28
  import { alignProtectionPrice, floorSize, mulDecimal, requirePositive, slippagePrice, subDecimal, toUnitsExact, unitsToString } from "../precision.js";
@@ -90,6 +91,15 @@ function finalityOf(s) {
90
91
  return s === "filled" || s === "rejected" || s === "cancelled" ? "final" : "pending";
91
92
  }
92
93
  const usToMs = (us) => Math.floor(us / 1000);
94
+ /** /v1/trades and /v1/fundingRates take `limit` ≤ 1000 (docs; the venue answered 1000 rows for more). */
95
+ export const ARCUS_HISTORY_CAP = 1000;
96
+ function arcusLimit(what, limit) {
97
+ if (!Number.isInteger(limit) || limit < 1 || limit > ARCUS_HISTORY_CAP) {
98
+ throw new PerpError("INVALID_PARAMS", `Arcus: ${what} limit must be 1..${ARCUS_HISTORY_CAP} (got ${limit})`, {
99
+ remediation: `Arcus serves at most ${ARCUS_HISTORY_CAP} rows per request`,
100
+ });
101
+ }
102
+ }
93
103
  function sideLower(s) { return s === "BUY" ? "buy" : "sell"; }
94
104
  function tifFromRead(t) {
95
105
  // Reads report resting (GTT) orders as the legacy "GTC".
@@ -196,13 +206,13 @@ export class ArcusAdapter {
196
206
  const meta = ctx.noAgent ? null : ctx.credentials.agent;
197
207
  if (meta === null) {
198
208
  if (!hasMaster) {
199
- // §12.4 #7: no agent key and no master EVM key — nothing can sign.
209
+ // no agent key and no master EVM key — nothing can sign.
200
210
  throw new PerpError("NO_SIGNER_AVAILABLE", "No Arcus signer: no agent API key and no master EVM wallet / private key", {
201
211
  remediation: "Spot swaps: pass --wallet <ows-wallet> (master EVM key). Perp trading: perp wallet agent approve arcus --master <ows-wallet>",
202
212
  });
203
213
  }
204
214
  // Master-only (also --no-agent): spot swaps (master EVM signer) work; perp
205
- // private calls throw AGENT_NOT_REGISTERED in requireAccount() (design §12.4 #7).
215
+ // private calls throw AGENT_NOT_REGISTERED in requireAccount().
206
216
  this.client = new ArcusClient(ctx.network, null);
207
217
  this.account = null;
208
218
  return;
@@ -281,6 +291,21 @@ export class ArcusAdapter {
281
291
  }
282
292
  return m;
283
293
  }
294
+ /**
295
+ * A market-data read (book / trades / funding / candles) of a listed market that is not ONLINE
296
+ * is refused like `market info` / `market list` treat it (not listed): SYMBOL_NOT_FOUND naming
297
+ * the venue status — the venue answered `[]` or HTTP 404 ORDER_NOT_FOUND for it before.
298
+ */
299
+ async onlineMarket(symbol) {
300
+ const m = await this.market(symbol);
301
+ if (!isArcusOnlineMarket(m)) {
302
+ throw new PerpError("SYMBOL_NOT_FOUND", `Arcus: market ${m.marketDisplayName} is ${m.status} (not tradable; no live market data)`, {
303
+ exchange: "arcus", status: m.status, tradable: false,
304
+ remediation: "List live markets with: perp market list -e arcus",
305
+ });
306
+ }
307
+ return m;
308
+ }
284
309
  async symbolOf(marketId) {
285
310
  let m = (await this.loadMarkets(false)).byId.get(marketId);
286
311
  if (!m)
@@ -462,7 +487,7 @@ export class ArcusAdapter {
462
487
  return onlineArcusMarkets(raw).map(toArcusVenueMarket);
463
488
  }
464
489
  async placeOrder(input) {
465
- // §13.3: GTC is the uniform default for a limit order without a TIF.
490
+ // GTC is the uniform default for a limit order without a TIF.
466
491
  const tifRequested = input.tif;
467
492
  const req = input.type === "limit" && input.tif === null ? { ...input, tif: "GTC" } : input;
468
493
  validateOrderRequest(this.venue, this.caps, req);
@@ -609,7 +634,7 @@ export class ArcusAdapter {
609
634
  /**
610
635
  * modifyOrder (docs modify-order / authentication).
611
636
  *
612
- * Size: the CLI's `req.size` is the new OPEN (unfilled) size (design §13.4), while
637
+ * Size: the CLI's `req.size` is the new OPEN (unfilled) size, while
613
638
  * Arcus modifies take the order's new TOTAL size, filled quantity included
614
639
  * (OrderRejectionReason MODIFY_SIZE_ALREADY_FILLED: "the order's new TOTAL size,
615
640
  * filled quantity included"). So body `quantity` and signed `q` are
@@ -878,20 +903,22 @@ export class ArcusAdapter {
878
903
  return (await this.getVenueMarkets()).map(toLegacyMarketInfo);
879
904
  }
880
905
  async getOrderbook(symbol) {
881
- const m = await this.market(symbol);
906
+ const m = await this.onlineMarket(symbol);
882
907
  const ob = await this.client.get(`/v1/l2OrderBook/${encodeURIComponent(m.marketDisplayName)}`, "l2OrderBook", { nLevels: ORDERBOOK_LEVELS });
883
908
  return { bids: ob.bids, asks: ob.asks };
884
909
  }
885
910
  async getRecentTrades(symbol, limit = 50) {
886
- const m = await this.market(symbol);
911
+ arcusLimit("trades", limit);
912
+ const m = await this.onlineMarket(symbol);
887
913
  const res = await this.client.get("/v1/trades", "trades", { market: m.marketDisplayName, limit });
888
- // Public tape carries no fee; "0" follows the legacy ExchangeTrade convention for public trades.
889
- return res.trades.map(t => ({ time: usToMs(t.timestamp), symbol: m.baseAsset, side: sideLower(t.side), price: t.price, size: t.size, fee: "0" }));
914
+ // Public tape: no fee applies to the viewer → null, never "0".
915
+ return res.trades.map(t => ({ time: usToMs(t.timestamp), symbol: m.baseAsset, side: sideLower(t.side), price: t.price, size: t.size, fee: null }));
890
916
  }
891
917
  async getFundingHistory(symbol, limit = 100) {
892
- const m = await this.market(symbol);
918
+ arcusLimit("funding history", limit);
919
+ const m = await this.onlineMarket(symbol);
893
920
  const res = await this.client.get("/v1/fundingRates", "fundingRates", { market: m.marketDisplayName, limit });
894
- return res.fundingRates.map(f => ({ time: usToMs(f.time), rate: f.fundingRate, price: null }));
921
+ return res.fundingRates.map(f => ({ time: usToMs(f.time), rate: f.fundingRate, price: null, intervalHours: 1, rateKind: "lastSettled" }));
895
922
  }
896
923
  async getKlines(symbol, interval, startTime, endTime) {
897
924
  if (!CANDLE_TIMEFRAMES.has(interval)) {
@@ -899,7 +926,7 @@ export class ArcusAdapter {
899
926
  remediation: `Use one of: ${[...CANDLE_TIMEFRAMES].join(", ")}`,
900
927
  });
901
928
  }
902
- const m = await this.market(symbol);
929
+ const m = await this.onlineMarket(symbol);
903
930
  const span = CANDLE_WINDOW_BARS * CANDLE_MS[interval];
904
931
  const windows = Math.max(1, Math.ceil((endTime - startTime) / span));
905
932
  if (windows > CANDLE_MAX_WINDOWS) {
@@ -915,7 +942,7 @@ export class ArcusAdapter {
915
942
  const res = await this.client.get("/v1/candles", "candles", { market: m.marketDisplayName, timeframe: interval, from: from * 1000, to: to * 1000 });
916
943
  for (const c of res.candles) {
917
944
  const time = usToMs(c.openTime);
918
- byTime.set(time, { time, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume, trades: c.tradeCount });
945
+ byTime.set(time, { time, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume, quoteVolume: null, trades: c.tradeCount });
919
946
  }
920
947
  }
921
948
  return [...byTime.values()].sort((x, y) => x.time - y.time);
@@ -995,7 +1022,7 @@ export class ArcusAdapter {
995
1022
  out.push({ time: usToMs(p.time), symbol: await this.symbolOf(p.marketId), payment: p.payment });
996
1023
  return out;
997
1024
  }
998
- // ── Legacy: trading shims (§10.7) ──
1025
+ // ── Legacy: trading shims ──
999
1026
  /** `clientOrderId`: the caller's id when given; otherwise one is generated (orderAck async needs one). */
1000
1027
  async marketOrder(symbol, side, size, opts) {
1001
1028
  return this.placeOrder({
@@ -1036,7 +1063,7 @@ export class ArcusAdapter {
1036
1063
  tif: isLimit ? "GTC" : null, // sent as GTT at the max horizon
1037
1064
  reduceOnly: opts?.reduceOnly === true, // non-reduce-only → CAPABILITY_UNSUPPORTED in validation
1038
1065
  clientOrderId: newClientOrderId(),
1039
- // The caller's resolved bound (the adapter never reads settings, §13.10 #1);
1066
+ // The caller's resolved bound (the adapter never reads settings);
1040
1067
  // a stop-market without it → INVALID_PARAMS in placeOrder.
1041
1068
  slippage: isLimit ? null : opts?.slippage ?? null,
1042
1069
  tpsl: null,
@@ -1060,7 +1087,7 @@ export class ArcusAdapter {
1060
1087
  });
1061
1088
  }
1062
1089
  }
1063
- /** Master EVM signer for on-chain actions outside the Arcus API (spot swaps, design §12.4 #7). */
1090
+ /** Master EVM signer for on-chain actions outside the Arcus API (spot swaps). */
1064
1091
  async masterEvmSigner() {
1065
1092
  this.requireMasterKey();
1066
1093
  return this.masterSigner();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Arcus agent (API key) lifecycle — design §10.4.
2
+ * Arcus agent (API key) lifecycle.
3
3
  *
4
4
  * approve:
5
5
  * 1. Resolve the master EVM signer from OWS (`masterOwsWallet`).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Arcus agent (API key) lifecycle — design §10.4.
2
+ * Arcus agent (API key) lifecycle.
3
3
  *
4
4
  * approve:
5
5
  * 1. Resolve the master EVM signer from OWS (`masterOwsWallet`).
@@ -198,7 +198,7 @@ export class ArcusClient {
198
198
  async send(method, url, endpoint, headers, body) {
199
199
  // Timeout covers fetch + body read. A GET failure is definite (TIMEOUT /
200
200
  // EXCHANGE_UNREACHABLE); a POST failure may have reached the venue → outcome
201
- // unknown (ORDER_PENDING, never resubmit — design §12.3).
201
+ // unknown (ORDER_PENDING, never resubmit).
202
202
  const signal = AbortSignal.timeout(TIMEOUT_MS);
203
203
  let res;
204
204
  let text;
@@ -24,6 +24,7 @@
24
24
  * - tradable: status === "ONLINE".
25
25
  */
26
26
  import { PerpError } from "../../errors.js";
27
+ import { isArcusOnlineMarket } from "../../ws/arcus-market.js";
27
28
  import { canonicalFor } from "../canonical-symbols.js";
28
29
  import { arcusVenue } from "../venues/arcus.js";
29
30
  import { ArcusClient } from "./client.js";
@@ -101,7 +102,7 @@ export function toArcusVenueMarket(m) {
101
102
  maxLeverage: maxLeverageOf(m),
102
103
  marginTiers: null,
103
104
  session: sessionOf(m),
104
- tradable: m.status === "ONLINE",
105
+ tradable: isArcusOnlineMarket(m),
105
106
  };
106
107
  }
107
108
  /**
@@ -110,7 +111,7 @@ export function toArcusVenueMarket(m) {
110
111
  * market / funding lists rather than listed at a $0 price and 0 % funding.
111
112
  */
112
113
  export function onlineArcusMarkets(raw) {
113
- return raw.filter(m => m.status === "ONLINE");
114
+ return raw.filter(isArcusOnlineMarket);
114
115
  }
115
116
  async function fetchMarkets(net) {
116
117
  const raw = await fetchArcusRawMarkets(new ArcusClient(net, null));
@@ -14,7 +14,7 @@
14
14
  * "everything else" rule, scheduleCancel — unverified):
15
15
  * ed25519(timestampNs + action + canonicalJson(body))
16
16
  *
17
- * Time units (design §10.10): `ct` and X-Timestamp are Unix NANOseconds;
17
+ * Time units: `ct` and X-Timestamp are Unix NANOseconds;
18
18
  * signed `g` is NANOseconds; the request body `goodTilTime` is a MICROsecond
19
19
  * decimal string (g = goodTilTime × 1000).
20
20
  *
@@ -78,8 +78,7 @@ export declare function placePayload(f: ArcusPlaceFields, untriggered: boolean):
78
78
  * - authentication page: always `id`, plus `c` "only if the resting order was placed
79
79
  * with a client id … the engine rejects a mismatch".
80
80
  * Default follows the openapi spec ("idOnly": sign `id`, never `c`; the body sends
81
- * orderId only). "idAndClientId" is the authentication-page variant. Settled by the
82
- * docs/TEST-PLAN.md step "Arcus modify signature identity" on testnet.
81
+ * orderId only). "idAndClientId" is the authentication-page variant. Settled by the "Arcus modify signature identity" testnet check.
83
82
  */
84
83
  export type ArcusModifyIdentity = "idOnly" | "idAndClientId";
85
84
  export declare const ARCUS_MODIFY_SIGNED_IDENTITY: ArcusModifyIdentity;
@@ -14,7 +14,7 @@
14
14
  * "everything else" rule, scheduleCancel — unverified):
15
15
  * ed25519(timestampNs + action + canonicalJson(body))
16
16
  *
17
- * Time units (design §10.10): `ct` and X-Timestamp are Unix NANOseconds;
17
+ * Time units: `ct` and X-Timestamp are Unix NANOseconds;
18
18
  * signed `g` is NANOseconds; the request body `goodTilTime` is a MICROsecond
19
19
  * decimal string (g = goodTilTime × 1000).
20
20
  *